@custral/sdk
v1.0.2
Published
Official server-side TypeScript SDK for the Custral developer platform.
Maintainers
Readme
@custral/sdk
The official server-side TypeScript SDK for the Custral developer platform. Read and write every object — records, conversations, workflows — from your own code, with the same model the product uses.
Modeled after the Stripe Node SDK: one client, resource namespaces, typed params and returns, automatic retries + rate-limit awareness, and Stripe-style webhook signature verification.
- Zero runtime dependencies — native
fetch(Node 18+) andnode:crypto. - ESM + CJS, fully typed.
- Server-side only — a secret key (
sk_…) must never ship to a browser.
Install
npm install @custral/sdk
# or: pnpm add @custral/sdk / bun add @custral/sdk / yarn add @custral/sdkQuickstart
import {Custral} from "@custral/sdk";
const custral = new Custral({apiKey: process.env.CUSTRAL_API_KEY!}); // sk_...
// create a record on any object
const deal = await custral.records.create({
object: "deals",
fields: {name: "Globex", stage: "proposal"},
});
// react to changes with a webhook (see "Webhooks" below)
custral.on("record.updated", (event) => {
// …your logic, running on your infra
});Create an API key in Settings → Applications and grant it the scopes each
endpoint needs (records:read, records:write, objects:read, mcp:read, …).
Configuration
const custral = new Custral({
apiKey: process.env.CUSTRAL_API_KEY!, // required — a secret key (sk_...)
baseUrl: "https://api.custral.com", // default; use http://localhost:8080 locally
maxRetries: 2, // retry safe failures (rate limits, 5xx on reads, connection errors)
timeout: 30_000, // per-request timeout in ms
webhookSecret: process.env.CUSTRAL_WEBHOOK_SECRET, // default secret for webhook verification
headers: {"X-My-Trace": "…"}, // sent on every request
fetch: myFetch, // bring your own fetch (proxy, instrumentation, polyfill)
});Resources
Records
// Create (requires records:write). v1 ingest is create-only.
const {id, ignoredFields} = await custral.records.create({
object: "contacts", // object id, key, or name
fields: {email: "[email protected]", full_name: "Jane Doe"}, // keys may be a property id, key, or name
});
// `ignoredFields` lists any keys that didn't match a property on the object.
// List (requires records:read) — offset pagination, limit 1–200.
const page = await custral.records.list({object: "contacts", limit: 50, offset: 0});
page.data; // CustralRecord[]
page.total; // number | null
page.hasMore; // boolean
// Retrieve one (requires records:read).
const record = await custral.records.retrieve("contacts", "rec_123");
// or: custral.records.retrieve({object: "contacts", id: "rec_123"})Objects
const objects = await custral.objects.list(); // requires objects:read
const object = await custral.objects.retrieve("obj_123");Identity
const me = await custral.me(); // "does my key work, and what can it do?"
me.orgId;
me.environment; // "live" | "test" | null
me.scopes; // ApiKeyScope[]MCP tools
// The exact tools/list an MCP client would see for this key (requires mcp:read).
const {tools, count} = await custral.mcp.tools();Errors
Every failure throws a typed CustralError carrying a machine-readable code,
an effective HTTP statusCode, the requestId (quote it to support), and the
rateLimit budget at the time.
import {
Custral,
CustralError,
CustralAuthenticationError, // 401 — bad / missing / expired key
CustralPermissionError, // 403 — key lacks the required scope
CustralNotFoundError, // 404 — object / record not found
CustralInvalidRequestError, // 400 — validation failed (see .validation)
CustralRateLimitError, // 429 — rate limited (see .retryAfter)
CustralAPIError, // 5xx — server-side / gateway failure
CustralConnectionError, // network failure or timeout
} from "@custral/sdk";
try {
await custral.records.create({object: "deals", fields: {name: "Globex"}});
} catch (err) {
if (err instanceof CustralRateLimitError) {
console.warn(`rate limited; retry after ${err.retryAfter}s`);
} else if (err instanceof CustralError) {
console.error(`${err.code} (${err.statusCode}) — request ${err.requestId}`);
}
}Note on status codes. The Custral API returns failures inside a response envelope (
{error: {code}, reqId}), so the SDK detects an error from the body and synthesizes a conventionalstatusCodefrom thecode(e.g.insufficient_scope→ 403). It also honours a real non-2xx status when the transport itself fails (a gateway 502, a timeout), soerr.statusCodeis always meaningful.
Retries
maxRetries (default 2) retries only safe failures with exponential
backoff + jitter:
- Rate limits (429) — always retried (the request was rejected before
processing), honouring
Retry-After/RateLimit-Reset. - 5xx and connection errors — retried only for idempotent GETs. A failed
records.create(POST) is not auto-retried, so a request that actually succeeded server-side can't double-create. Override per call with{maxRetries}, or setmaxRetries: 0to disable.
Webhooks
Custral delivers events to your endpoint as signed HTTP POSTs
(X-Custral-Signature: t=<ts>,v1=<hmac>). Verify each one before trusting it.
Verify + dispatch with Express
on(...) registers listeners; webhooks.express() verifies deliveries and fans
them out to those listeners. Mount it with a raw body parser so the exact
signed bytes survive:
import express from "express";
import {Custral} from "@custral/sdk";
const custral = new Custral({
apiKey: process.env.CUSTRAL_API_KEY!,
webhookSecret: process.env.CUSTRAL_WEBHOOK_SECRET!,
});
custral.on("record.created", (event) => saveLead(event.data));
custral.on("record.*", (event) => audit(event)); // prefix glob
custral.on("*", (event) => log(event)); // all events
const app = express();
app.post("/webhooks/custral", express.raw({type: "application/json"}), custral.webhooks.express());.express() responds 200 {received: true} on success, 400 on a bad
signature, and 500 when a listener throws (so Custral retries the delivery).
Make listeners idempotent — webhook delivery is at-least-once.
Verify manually (Stripe-style)
// req.body must be the raw Buffer/string (NOT a JSON-parsed object).
const event = custral.webhooks.constructEvent(req.body, req.headers["x-custral-signature"], secret);
// throws CustralSignatureVerificationError if the signature is missing, stale, or wrong.Or without a client instance:
import {constructEvent} from "@custral/sdk";
const event = constructEvent(rawBody, signatureHeader, secret);The signing scheme is HMAC-SHA256(secret, "<timestamp>.<rawBody>") with a
300-second replay window (configurable via the tolerance option).
License
MIT
