How it works
You build one HTTPS endpoint. ZeroClick makes two kinds of call to it.1
ZeroClick tells you what the account should look like
After a purchase or a top-up,
POST /zeroclick/access arrives with a complete picture of the account: which customer, which plan, what period it runs for, how much credit has been granted in total. You create or update the account and answer 200.2
ZeroClick asks you for an API key
When the buyer requests a key after the account is ready and the payment has gone through,
POST /zeroclick/access/{accessId}/keys arrives. You generate a key, store its hash, and return the key once in plaintext.3
The buyer calls you directly
ZeroClick hands the key to the buyer. From then on they talk to your API with your key, on your terms. ZeroClick does not sit in the middle of those calls and never stores the key.
Before you start
Selling accounts is not self-serve yet. Email help@zeroclick.ai and we will turn it on for your seller. You can build and test the endpoint before that.
- A plan that is not pay-as-you-go — a
subscription,subscription_usage, orcreditplan (plans and pricing). Pay-as-you-go buyers pay per call and leave nothing behind, so there is no account to create. - A signing secret. ZeroClick signs every call with it and you verify that signature. Create one in the dashboard and keep its id (the
kid) and value in your secret manager (keys and secrets). - An HTTPS endpoint. By default it is
/zeroclick/accesson the origin of your upstream base URL; you can point it somewhere else. - Durable storage for accounts, the last version you applied, the credit you have granted in total, and your API key hashes.
The stateful helpers ship in the TypeScript SDK today. On any other stack, implement the same two routes by hand — the reference has the full wire contract and signature.
Decide how to key your accounts
Every call carries two ids:accessId identifies the purchase (zacc_…), and agent identifies the customer (agt_…, the same id your stateless endpoints already see as zc-agent-id). You tell the SDK which one your handlers should receive as accountId.
One account per customer, one live API key. The shape most SaaS products have, and the simpler database. Your handlers get the agent id, and you can ignore accessId entirely — it never has to appear in your schema. Asking for a key again replaces the old one, which is why this requires rotation:
accessId, since it is what tells one purchase from another. "additive" adds a key per request; "rotating" replaces the last:
accessId, because it is how a top-up, renewal, or retry is matched back to the right purchase. Ignoring it is a choice about your storage, not about the protocol.
Even on the
agent profile, consider keeping accessId as an ordinary column rather than discarding it. You do not need it to find the account, but it is what lets you hand a customer a direct top-up link when they run out of credit — see running out of credit.agent id and nothing more, and that id is stable across payment methods and renewals. Key everything on accountId; never try to infer a customer from a payment.
Build the endpoint
handleAccessRequest routes, verifies, and validates both calls, then hands off to your two functions. It returns null for anything that is not one of its routes, so you can mount it inside an existing handler.
Create the account
The write is a complete description of how the account should look, not a list of changes — which makes duplicates and late arrivals harmless, as long as you follow two rules inside one database transaction:- Ignore anything you have already applied.
shouldApplycompares the incomingstateVersionagainst the one you stored. - Never grant the same credit twice.
creditGrantedUsdis the running total ever granted, so you add the difference.deriveCreditDeltacomputes it and tells you which of three cases you are in.
active only when the account really works. If setup is slow — provisioning a tenant, warming an index — answer { lifecycle: "provisioning", retryAfterSeconds: 30 } and ZeroClick sends the same write again until you say active.
Every field in the request body is documented in the reference.
Mint the key
- Return the plaintext exactly once and store only a hash. ZeroClick passes the key to the buyer and keeps no copy, so nobody can look it up again.
- Hash it appropriately. For a random key with real entropy — 32 bytes from a CSPRNG or better — a plain SHA-256 is right: fast enough to check on every request, with nothing to brute-force. Reach for a slow KDF like Argon2 only if your keys are short or human-chosen.
- Never log it, and keep it out of error messages. The SDK already sets
cache-control: no-store. - Revoke and insert in one transaction when rotating, or a retry can leave the customer with two live keys or none.
{ notProvisioned: true, retryAfterSeconds: 30 } rather than an error. { unknown: true } is only for an account that genuinely does not exist, and { conflict: { code } } is for refusing with a reason of your own — a key cap reached, say. Every return value, and the status each becomes, is in the reference; the inputs your handlers receive are there too.
When a customer runs out of credit
Once you have minted a key, the customer calls your API directly. ZeroClick is not in that path, so it never sees the moment a subscription lapses or a credit balance hits zero — the refusal is yours, in your own error shape. ZeroClick does not define an envelope for it, and you should not invent one that changes what your directly-signed-up customers already see. Same status, same body, same as ever. What you can usefully add, only on the branch where you already know this account came from ZeroClick, is where to go to fix it. Every purchase has a top-up URL on your pay URL:accessId, which is the one good reason to store it even when you key accounts on the agent.
If you would rather add nothing, an agent can still find its own way: GET /purchases/receipt on your pay URL, with the agent’s own bearer token, lists every purchase it holds with you and includes both a topUpUrl and a redeemUrl for each. See buyer-facing routes.
Note that a top-up does not go through your error path at all. It arrives as another account write, with a higher stateVersion and a larger creditGrantedUsd — which your existing onWrite already handles.
Card refunds and chargebacks arrive the same way: an account write with a larger creditReversedUsd (and, for a fully reversed purchase, an ended period or status: "suspended"). deriveCreditDelta answers debit for the difference; subtract it and clamp the balance at zero.
Retries and failures
Account writes are at-least-once, so expect duplicates and expect a retry after a network timeout that happened after you committed. Three things to design around:- You have 7 seconds to respond. Commit, then answer. Anything slower belongs behind
lifecycle: "provisioning". - Return
503— or just throw — for anything temporary. ZeroClick retries network errors,429, and any5xx, backing off for up to 24 hours. It does not retry other 4xx responses, so a bug that returns400permanently fails the delivery. - A permanent failure refunds the buyer automatically. A reserved payment is released, a captured one refunded. You never owe a refund for an account you did not create.
Customers with more than one agent
The short version: you do nothing, and your account key never changes. A ZeroClick buyer can hold several agent identities — a replacement for a rotated credential, a second agent for another workload (agents belong to buyers). Any of them can come back later and ask for a key to a purchase the buyer already owns. When that happens, ZeroClick still signs the call with the original agent that made the purchase and still sends that agent in the body. The account you created on day one is the account that gets the new key. You never see the buyer’s other identities and do not need to model them.Before you go live
- Serve the endpoint over HTTPS, with no redirects.
- Verify signatures against the raw body bytes and the original path and query.
- Look up secrets by
kidand keep the previous one through a rotation. - Never route these requests through
guard,guardIdentity, orverifyRequest. - Apply
stateVersionand credit in one locked transaction, and never lower a recorded total. - Store key hashes only, return plaintext once, keep keys out of logs.
- Make rotation revoke-and-insert atomic.
- Return
503for anything temporary; remember other 4xx responses are permanent. - Alert on handler errors and on repeated retries for the same account.
- Test a duplicate write, an out-of-order write, a handler timeout, a repeated key request, and a signing-secret rotation.
Next steps
Account and key reference
Every field, status code, retry rule, and the signature for non-TypeScript stacks.
Keys and secrets
Create, scope, and rotate the signing secret this endpoint verifies against.
Agents and access
What an agent id is, and how one buyer can hold several.
The integration contract
The stateless side: verify, check, serve, settle.