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 a storefront-level designation made when the storefront is created — today we set that up with you, so email help@zeroclick.ai. You can build and test the endpoint before that.
- A plan that is not pay-as-you-go — a
subscription,subscription_usage,credit, orfree_trialplan (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 all four SDKs:
@zeroclickai/sellers/stateful, zeroclick_sellers.stateful, the Go module’s stateful sub-package, and Ruby’s zeroclick/sellers/stateful. This guide’s examples are TypeScript; the other three mirror them name for name. On any other stack, implement the same two routes by hand — the reference has the full wire contract, the signature with worked verifiers, and a test vector to check yours against.The three ids, and which one to key on
Key your accounts onaccessId (zacc_…). It is the durable account handle ZeroClick mints for the customer at this seller: stable across renewals, top-ups, plan switches, and payment methods, present on every call, and never reused. One customer normally holds one per seller — a repeat purchase or plan switch updates the same account rather than opening a second one.
The other two ids describe who is calling, not the account:
agentId(agt_…) is the per-call caller identity — the same id your stateless endpoints see aszc-agent-id. A customer’s human can hold several agents, and any of them may act on the account, so different calls to one account may carry different agent ids. Verify it, log it, but never key on it.buyerId(byr_…) appears once the caller’s credential has been claimed by its human — the same value proxied traffic carries aszc-buyer-id. Not a key either, but worth an ordinary column if you also keep buyer-keyed customer records: it is what joins this account to them.
"rotating" replaces the previous key on every mint — the one-live-key shape most APIs want; "additive" mints another:
accessId; never try to infer a customer from a payment.
Tying the account to a user account
The ids above identify agents and buyers inside ZeroClick — none of them tells you which account on your side the customer is. We strongly recommend making that link: run your plans with therequested verified-email policy (or required, when your product cannot work without an email) and link on buyerEmail. It costs the buyer nothing under requested, and it means a human who later registers or signs in with the same email finds the entitlements their agents bought — able to see them, manage settings, and treat the purchase like any they made by hand. Without it, the resource stays reachable only through the agent that bought it.
Set the policy per plan (plans and pricing):
requested— the recommended default when you want the email. Purchases are never blocked. When the buying agent is already claimed by a human with a verified email, the account write carries it immediately; when the agent is still anonymous, the purchase goes through without it, and ZeroClick calls your endpoint again the moment the human claims — a fresh account write, onestateVersionhigher, identical except thatbuyerEmailis now set. Your existing write handler needs nothing new: apply the newest picture as always, and treat abuyerEmailappearing partway through an account’s life as your cue to link.required— ZeroClick refuses the purchase (403 verified_email_required) until the claim exists, so every write for the account carries the email from the first version. Use it only when you cannot deliver anything without an email; it costs you the fully autonomous purchase.
buyerEmail in the signed body, mirrored as the zc-buyer-email header (the SDKs hand it to your write handler as buyerEmail and reject a header/body disagreement for you). It is verified before it ever reaches you, so it is safe to key on. In your write handler:
- The email matches an existing user — attach the ZeroClick account (and the key you later mint) to that user, so the purchase shows up in the account they already have with you.
- The email is new — create a fresh user account for it, exactly as if they had signed up on your site, and provision the ZeroClick purchase into it.
- You would rather wait — store the email against the
accessIdand link the resource when the human registers or signs in with that address later; until then the agent still uses the minted key normally.
off never carry buyerEmail, so those purchases stay anonymous, agent-held resources keyed only on accessId.
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.
lifetimeCreditGrantedUsdis a running total, not the size of this top-up, 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: 5 } and ZeroClick sends the same write again until you say active. Hint the time you actually need: the hint only ever pushes the next attempt later, and the buyer waits out whatever you name.
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: 5 } 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.
Decide whether to serve
Once the key is out, every authenticated request to your API carries a serving decision, and it should be one derived check against the state you stored:isServiceable answers true only while status is active and the period, when its end is non-null, has not lapsed. Do not hand-combine the two conditions: checking status alone misses period-based revocation on interval plans, and checking 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, on every request.
The two statuses that turn the gate off ask for different follow-through:
suspendedis recoverable. Suspend service and delete nothing; a later write may return the account toactive.closedis terminal. ZeroClick sends it when the account is torn down on the ZeroClick side: revoke live API keys, stop serving, and keep your own records. No higherstateVersionwill ever follow.
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:POST to it verbatim, and the query string names the account — and your account key is exactly the id it needs.
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 lifetimeCreditGrantedUsd — which your existing onWrite already handles.
Card refunds and chargebacks arrive the same way: an account write with a larger lifetimeCreditReversedUsd (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. Both revocation signals fall inside isServiceable, so if you gate requests on it, applying the debit is the only reversal handling you need.
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, or top the account up. When that happens, the call carries the sameaccessId — ZeroClick resolves which account the caller may act on before anything is signed toward you — with the acting agent’s own id in agentId and, once claimed, the shared buyerId. So a second agent minting a key looks like a new agentId value against a familiar accessId: expected, not suspicious. You never enumerate the buyer’s 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.
- Gate every authenticated request on
isServiceable— period expiry has no push. - 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 stacks without an SDK.
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.