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 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, or credit 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 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:
A customer might hold several of something — two or three live keys, or subscriptions to two of your plans. Then key on accessId, since it is what tells one purchase from another. "additive" adds a key per request; "rotating" replaces the last:
Either way ZeroClick keeps sending 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.
Whether the customer paid by card or from a crypto wallet is invisible to you, and so is anything else about how they paid. You see an 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.
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. creditGrantedUsd is the running total ever granted, 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: 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.
If the account is not ready yet, return { 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:
Putting that in your refusal is the difference between an agent retrying blindly and an agent topping up and carrying on. It needs the 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 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. 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 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.
  • 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 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.