ClocknextHelp
DocumentationAPI ReferenceMCP Tools
Dashboard

Get started

OverviewQuickstart

Core concepts

The response envelopeErrorsPaginationAsync, flushing & reliability

API reference

Record Signals POSTRecord usage logs POST
List customers GETCreate a customer POSTGet a customer GETUpdate a customer PATCHDelete a customer DELETEGet a customer's balances GETGet a customer's current plan GETList a customer's wallet transactions GETAdd a wallet transaction POSTAdjust a customer's credit balance POSTAdjust a customer's outcome balance POSTAdjust a customer's unit balance POST
List a customer's members GETAdd a member POSTUpdate a member PATCHRemove a member DELETE
List purchases GETCreate a purchase POSTGet a purchase GETUpdate a purchase's auto-payment setting PATCHCancel a purchase POST
List invoices GETGet an invoice GETMint a hosted pay link GET
List payments GETGet a payment GET
Mint a customer-portal access token POST

API Reference

Async, flushing & reliability

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 /api/v1/usage over raw HTTP, none of the buffering below applies — each call is its own request, and retries, batching, and idempotency are yours to implement.

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

In the default async mode, a signal call buffers and returns right away — it never blocks your model/response path:

// returns synchronously as { queued: true } — the send happens in the background
cnk.signals.credit({
  customerId: "cus_abc123",
  model: "gpt-4o",
  agentKey: "pro_credit",
  tokens: { input: 1200, output: 480 },
});

A background flusher drains the buffer when either trigger fires first:

  • the buffer reaches batch.maxSize signals (default 20), or
  • batch.maxIntervalMs elapses (default 2000 ms).

Each drain sends up to batch.maxConcurrency requests at once (default 5). "Batching" here is about timing, not a bulk endpoint — every signal is still its own POST /api/v1/usage.

In async mode, server-side rejections (e.g. insufficient allowance, 422) are not thrown from the call — they surface via the onError hook (or when you flush()). Use { wait: true } or sync mode below if you need to react inline.

Get the priced result back

To receive the computed usageLog (cost, credits drawn, post-signal balance) — or to gate on a real-time allowance rejection — await the send. 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

Because sends are deferred, drain the buffer at the right 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

A serverless (or edge) function is usually frozen the moment it returns, so signals still in the async buffer never send — and a per-request function rarely reaches the size/interval flush trigger, so without one of the patterns below you can silently lose most of your signals.

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) is the framework-agnostic equivalent. Prefer simplicity over throughput? Run the client in sync mode (or pass { wait: true }) and await each signal — the send finishes before your handler returns, at the cost of a little latency.

Retries & idempotency

Reads (GET/DELETE) and metering signals opt into automatic retries; other non-idempotent writes (customers.create, purchases.create, …) do not, so a transient blip can never create a duplicate.

  • Retried: transient transport failures — network errors, timeouts, and 408 / 409 / 429 / 5xx. Backoff is exponential (baseDelayMs · 2ⁿ, capped at maxDelayMs) with equal jitter, and a server Retry-After header is honoured.
  • Never retried: deterministic 400 / 401 / 404 / 422 (validation, auth, not-found, plan/allowance).
  • Idempotency: every metering signal carries an idempotency key — auto-generated (UUID v4) per signal and reused across all of that signal's retries, so a retried send is deduplicated server-side instead of double-counted. Pass your own idempotencyKey (a stable id for the logical event) to also dedup across process restarts or at-least-once redelivery.
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
});

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 sync mode (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

Pagination

Page through list endpoints with limit and cursor.

Record Signals POST

Records exactly one Signal — a single metered request against a wallet, credit or outcome meter.

On this page

Async by defaultGet the priced result backFlush before you exitOn Vercel & serverlessRetries & idempotencyDurability: the buffer is in-memoryConfiguration