Skip to main content
Most ZeroClick integrations are stateless: a request arrives, you serve it, you report usage, and nothing is left behind. This page is for the other kind, where a purchase creates something that lasts — an account, a subscription, a credit balance, an API key. Say you sell a $20/month research API. A buyer purchases your plan through ZeroClick, and you want them to end up with an account in your database, a current billing period, and an API key they can put in their own code. That is what this integration does. ZeroClick handles buyer identity, payment, refunds, and letting a buyer recover their purchase later; your service stays the source of truth for the account.

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.
Two calls in, one key out.

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, or free_trial plan (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/access on 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 on accessId (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 as zc-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 as zc-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.
The remaining choice is your remint policy. "rotating" replaces the previous key on every mint — the one-live-key shape most APIs want; "additive" mints another:
Whether the customer paid by card or from a crypto wallet is invisible to you, and so is anything else about how they paid. Key everything on 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 the requested 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, one stateVersion higher, identical except that buyerEmail is now set. Your existing write handler needs nothing new: apply the newest picture as always, and treat a buyerEmail appearing 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.
Either way the email arrives as 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 accessId and link the resource when the human registers or signs in with that address later; until then the agent still uses the minted key normally.
Any of the three keeps one truth: the human who verified that email owns the resource, whichever of their agents bought it. Plans with the policy 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.
Give the SDK the raw request bytes and the original path and query, before any framework parses or rewrites them. The signature covers those exact bytes, so a body parsed to JSON and re-serialized will not verify — the same rule as verifying ordinary requests.
Do not guard this endpoint with guard, guardIdentity, or verifyRequest. Those verify the signature on ordinary paid traffic, which is a different signature. Account calls carry an extra purpose, so a signature captured from normal proxied traffic can never be replayed against your account routes — but only if you keep the two verifiers apart.

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:
  1. Ignore anything you have already applied. shouldApply compares the incoming stateVersion against the one you stored.
  2. Never grant the same credit twice. lifetimeCreditGrantedUsd is a running total, not the size of this top-up, so you add the difference. deriveCreditDelta computes it and tells you which of three cases you are in.
The row lock, the balance change, and the stored version must commit together — if they can commit separately, a retry landing between them grants credit twice. Never lower a total you have already recorded. Answer 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.
If the account is not ready yet, return { 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:
  • suspended is recoverable. Suspend service and delete nothing; a later write may return the account to active.
  • closed is 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 higher stateVersion will 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:
Putting that in your refusal is the difference between an agent retrying blindly and an agent topping up and carrying on. The URL works as handed out — an agent can 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 any 5xx, backing off for up to 24 hours. It does not retry other 4xx responses, so a bug that returns 400 permanently 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.
Exact backoff, attempt limits, and retry-hint handling are in the reference.

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 same accessId — 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 kid and keep the previous one through a rotation.
  • Never route these requests through guard, guardIdentity, or verifyRequest.
  • Apply stateVersion and 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 503 for 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.