Skip to main content
Every detail behind sell accounts and API keys. Read that guide first — this page is for looking things up once you are building.

Configuring the endpoint

ZeroClick derives the endpoint from the origin of your upstream base URL plus /zeroclick/access. Any path on the base URL is dropped: https://api.example.com/v1 gives https://api.example.com/zeroclick/access. To host it elsewhere, set an override:
GET the same path returns stateful (is this storefront designated to sell accounts) and the current accessEndpointUrl. The mint route is always the write route plus /{accessId}/keys.

Request headers

Both calls arrive as POST with:

The account write

POST /zeroclick/access
Every declared field is always present in the body — explicitly null when unset, never omitted. Every money field is a string with exactly six decimal places. parseMoneyUsd converts one to an integer count of millionths of a dollar (“micros”) so you never do floating-point math on money; formatMoneyUsd converts back.

Why the write is a complete picture

The write describes how the account should look, not what changed. Replay it, receive it twice, or receive an old one late: applying the newest version you have seen always leaves the account correct. That is why lifetimeCreditGrantedUsd and lifetimeCreditReversedUsd are running totals rather than deltas. deriveCreditDelta nets the two totals into one purse movement, and returns one of four outcomes:

When the period advances

A renewal arrives as an ordinary write whose state.period.start differs from the one you stored — periodAdvanced compares the two. That is your cue to reset any per-period counters of your own (a monthly quota, included usage you meter yourself). The credit totals are the asymmetry: they are lifetime running totals and never reset, which is exactly what lets deriveCreditDelta stay correct across renewals. Reset quotas on a new period; never reset the recorded grant and reversal totals.

Deciding whether to serve

isServiceable (is_serviceable in Python, IsServiceable in Go, serviceable? in Ruby) is the one request-time gate: it answers true only while the stored status is active and the period, when its end is non-null, has not lapsed. Gate every authenticated request on it rather than combining status and the period yourself — status alone misses period-based revocation on interval plans, and the period alone misses suspension and closure. Period expiry has no push: nothing arrives at period end, so the check must be local and time-based, against the state you stored.

The key mint

POST /zeroclick/access/zacc_8h2m4x0q9k1f/keys
A successful response returns the key once. maxKeys tells ZeroClick how many live keys this account may hold (the SDK sends 1 automatically when remintPolicy is "rotating"), and keyExpiresAt lets ZeroClick tell the buyer when to come back for a new one. Both are optional.
200
ZeroClick passes the key straight to the buyer and never stores it. If the buyer needs it again, they ask for a new one and you mint again under your remintPolicy.

What your handlers receive

WriteInput, passed to onWrite: MintInput, passed to onMint, is the same minus entitlement, with dedupeKey of mint:{requestId}. Its accessId comes from the URL path rather than a body.

What your handlers can return

onWrite: onMint: Before either handler runs, the SDK answers 401 on a missing, stale, or invalid signature, and 400 if the body is malformed, its agentId does not match the signed zc-agent-id, its buyerId does not match the zc-buyer-id header, or its buyerEmail does not match the zc-buyer-email header.

Retries, timeouts, and failures

Account writes are at-least-once. Expect duplicates, and expect a retry after a network timeout that happened after you committed. When a delivery permanently fails, the buyer is made whole automatically: a reserved payment is released, a captured one is refunded. You never owe a refund for an account you did not create. If the refund itself fails, ZeroClick flags the account internally and follows up — there is nothing for you to do. Because ZeroClick considers the write applied when you answer 200 with lifecycle: "active", do not start anything irreversible after that point.

Buyer-facing routes on your pay URL

Your customer’s agent uses these to manage a purchase, all on your pay URL (https://acme.pay.zeroclick.io) and all authenticated with the agent’s own bearer token. You do not implement them — ZeroClick does — but you may want to point agents at them.

Free plans

A free_trial plan creates an account with no payment. By default its verified email policy is required — that is what stops one person minting unlimited free accounts — so its write arrives with buyerEmail set, and each human can claim it once. A seller may relax the policy per plan, accepting that unclaimed agents can then open trial accounts (the email then arrives on a later write once the human claims, under requested). Once created, the free account is durable and recoverable across the buyer’s agents, just like a paid account.

Signature, for stacks without an SDK

The stateful helpers ship in the TypeScript (@zeroclickai/sellers/stateful), Python (zeroclick_sellers.stateful), Go (cdn.zeroclick.io/sdks/sellers-go/stateful), and Ruby (zeroclick/sellers/stateful) SDKs. On any other stack, implement the two routes and verify the signature yourself. It is HMAC-SHA256 over these seven lines joined by \n — the ordinary canonical string plus a trailing purpose:
Select the purpose from the route you matched, never from anything in the request. That is what stops a signature captured from ordinary proxied traffic from authorizing an account write. Compare against the v1= value in constant time, and reject a t= more than 5 minutes old or in the future. The header grammar, parsing rules, and raw-bytes rules are exactly those of the signature spec — only the trailing purpose line is new. Handler results map to the statuses in what your handlers can return; refuse a failed verification with a 401 and {"error":"missing_or_malformed_signature"}, {"error":"stale_timestamp"}, {"error":"unknown_kid"}, or {"error":"invalid_signature"}.

Worked verifiers

A compact verifier in the standard library — a porting reference for stacks without an SDK. (On Python and Go themselves, prefer the SDK’s verify_access_signature / stateful.Verify.) Pass the purpose from the route you matched:
The dedup, never-regress, and credit-delta arithmetic from the account write still apply on these stacks — implement shouldApply, isServiceable, and deriveCreditDelta from their descriptions there, and keep money in integer micros, never floats.

Worked test vector

The ids and secret below are test fixtures in the style of the signature spec’s vector; production values look the same. Signing secret:
Request:
Body (exactly these 430 bytes, one line, no trailing newline):
Its SHA-256 is 19c2447c4bcd377389b0c7f06660b54e33eacb06a39b6c8ffdfed39936b66e3c, and the matched route is the write route, so the canonical string is:
Expected result: with the verifier’s clock pinned to 1760000000 and tolerance 300, the HMAC-SHA256 of the canonical string with the secret equals the header’s v1. Verification succeeds. Your implementation must also refuse two signatures over the same request. Signed with no purpose line — what ZeroClick sends with ordinary proxied traffic — v1 is 70969f4352049b371bfd6fcff35eb4409c12cc08c734ad0889a79de1708e54a6; your access verifier must reject it. Signed for the other purpose (access.mint), v1 is 1c3fb09f2d2983a1f725d5459ea2743c61de2aaf79699e171bfd7d15ce0b196e; a mint signature must never authorize a write, nor the reverse.