API Reference
Sending data reliably
How the SDK sends signals — buffered by default, with retries, idempotency, and the flush() you must call before a serverless function exits.
This page is about the @clocknext/sdk client. If you post to
the /api/v1/signal/* endpoints over raw HTTP, none of
the buffering below applies — each call is its own request, and retries,
batching, and idempotency are yours to implement. That request can still come
back 202 (queued) rather than a final result — see the endpoint's own docs.
The client is async by default: signals.* buffers the signal in memory and
returns immediately. In a serverless function you must await cnk.flush()
before returning, or signals still in the buffer are lost when the process
freezes.
Async by default
Client-side only. cnk.signals.* doesn't send a request — it queues the
signal in memory and returns immediately:
// returns { queued: true } instantly — nothing sent yet
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input: 1200, output: 480 },
});A background timer flushes the queue when either fires first:
batch.maxSizesignals queued (default 20)batch.maxIntervalMselapsed (default 2000 ms)
Each flush sends up to batch.maxConcurrency requests in parallel (default
5) — one POST /api/v1/signal/* request per signal, no bulk endpoint.
A rejection (e.g. 422) can't throw back to your call — it already returned.
Catch it via onError or flush(). Need the result inline? Use
{ wait: true } or sync mode.
Get the priced result back
Await the send to get the computed usageLog back, or to catch a real-time
rejection. Per call:
const res = await cnk.signals.credit(
{ customerId: "cus_abc123", model: "gpt-4o", agentKey: "pro_credit", tokens: { input: 1200, output: 480 } },
{ wait: true },
);
res.usageLog?.customerCost;
res.usageLog?.balance?.remaining;…or globally, so every call awaits its send:
const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY!, mode: "sync" });Flush before you exit
Drain the buffer at these boundaries:
// Before a serverless function returns (Lambda, Vercel, Cloud Functions):
await cnk.flush();
// On graceful shutdown of a long-running process — flushes, then refuses new signals:
await cnk.close();
// How many signals are still buffered:
cnk.pending;On Vercel & serverless
Serverless functions freeze on return — buffered signals never send unless you flush first.
Flush after the response so metering never adds latency to your handler:
// Next.js App Router route handler on Vercel:
import { after } from "next/server"; // or: import { waitUntil } from "@vercel/functions";
export async function POST(req: Request) {
const out = await callLLM(/* … */);
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input, output },
});
after(() => cnk.flush()); // runs after the response is sent, before the instance freezes
return Response.json(out);
}waitUntil(cnk.flush()) from @vercel/functions works the same way. Or skip
buffering entirely — use sync mode / { wait: true } and pay the latency.
Retries & idempotency
Reads, deletes, and metering signals retry automatically. Writes like
customers.create / purchases.create don't — a blip can't create a
duplicate.
- Retried: network errors, timeouts,
409,5xx— plus408/429, though the API doesn't send those yet. Exponential backoff with jitter; honoursRetry-After. - Never retried:
400/401/404/422.
Idempotency — two layers:
- SDK retries (automatic). One auto-generated key per call, reused across its own retries. Covers network blips between you and Clocknext only.
- Your retries (manual). A crash or redelivery re-runs your code from
scratch — a fresh call gets a fresh key, so layer 1 doesn't help. Pass
your own
idempotencyKeyto dedupe those.
Works over raw HTTP too — it's not SDK-exclusive. No key, no protection: a retry you do yourself will double-count.
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input: 1200, output: 480 },
idempotencyKey: `chat:${requestId}`, // survives restarts; the server dedups repeats
});Every signal takes idempotencyKey — credit, wallet, outcome, outcome
complete, unit, and composite complete.
Balance adjustments
The adjust endpoints (/customers/{id}/credits|outcomes|units|composites|wallet/adjust)
take the same two layers as signals. The SDK keys every adjustment for you
(one auto-generated key per call, reused across its retries), so a network blip
never adjusts twice. Over raw HTTP there is no automatic key — send one yourself.
Pass your own idempotencyKey (1–255 characters; your order or top-up id works
well) to also make your own retries safe:
- Same key, same request → nothing is adjusted again.
200withreplayed: true,adjustedAt(when the original happened) and the currentremaining. The message starts "Already processed". - Same key, different request (another customer, entitlement or amount) →
422, nothing adjusted. Use a new key for a new adjustment. - A failed adjustment never uses up its key — fix the request and resend it with the same key.
- Keys are unique per workspace and per adjust endpoint.
curl -X POST https://payments.clocknext.com/api/v1/customers/cus_abc123/credits/adjust \
-H "Authorization: Bearer $CLOCKNEXT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "agentKey": "pro_credit", "delta": 500, "idempotencyKey": "topup:ord_1042" }'const res = await cnk.customers.adjustCredit("cus_abc123", {
agentKey: "pro_credit",
delta: 500,
idempotencyKey: `topup:${orderId}`, // optional — the SDK generates one if you omit it
});
if (res.replayed) {
// Already applied at res.adjustedAt — nothing was added this time.
}Durability: the buffer is in-memory
A signal the server has already accepted is durable on the server. But signals still sitting in the async buffer when a process crashes (before send) are lost — there is no on-disk spool. For zero-loss-before-receipt:
- run in
syncmode (or pass{ wait: true }) so the send completes before your code continues, or await cnk.flush()/cnk.close()at your function/shutdown boundaries.
When the buffer is full (batch.maxQueueSize, default 10 000) new signals
are dropped rather than queued without bound — observe both drops and permanent
failures with the hooks:
new ClockNext({
apiKey: process.env.CLOCKNEXT_API_KEY!,
onError: (err, signal) => report(err, signal), // permanent failure (after retries / non-retryable)
onDrop: (signal, reason) => report(reason), // "queue_full" | "send_failed"
onFlush: (count) => metrics.add("flushed", count),
});Configuration
Every knob is optional; a bad value (negative / non-finite) falls back to its default rather than breaking the client.
Prop
Type