ClocknextHelp
DocumentationAPI ReferenceMCP Tools
Dashboard

Get started

OverviewQuickstart

Good to know

Response formatHandling errorsPaginating resultsSending data reliablyYour own customer idsWebhooks

API reference

Record credit POSTRecord outcome POSTComplete outcome POSTRecord wallet POSTRecord unit POSTComplete composite POST
List customers GETCreate a customer POSTGet a customer GETUpdate a customer PATCHDelete a customer DELETEGet a customer's balances GETGet a customer's wallet ledger GETSet a customer's low-balance alert PUTGet a customer's current plan GETChange a recurring grant PUTList Customer Usage Log GETLook Up Usage Logs by ID GET
Manage a customer's credit balance POSTManage a customer's outcome balance POSTManage a customer's unit balance POSTManage a customer's composite balance POSTManage a customer's wallet balance POST
List a customer's members GETAdd a member POSTUpdate a member PATCHRemove a member DELETE
PlansCreditsOutcomesUnitsComposites
List taxes GETCreate a tax POSTGet a tax GETUpdate a tax PATCH
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
wallet.balance.update POSTwallet.balance.drop POST
Mint a customer-portal access token POST

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.maxSize signals queued (default 20)
  • batch.maxIntervalMs elapsed (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 — plus 408/429, though the API doesn't send those yet. Exponential backoff with jitter; honours Retry-After.
  • Never retried: 400 / 401 / 404 / 422.

Idempotency — two layers:

  1. SDK retries (automatic). One auto-generated key per call, reused across its own retries. Covers network blips between you and Clocknext only.
  2. 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 idempotencyKey to 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. 200 with replayed: true, adjustedAt (when the original happened) and the current remaining. 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 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

Paginating results

Page through list endpoints with limit and cursor.

Your own customer ids

Give a customer an externalId — the id your product already uses — and name it by that instead of ours.

On this page

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