# Analytics overview Source: https://docs.zeroclick.ai/api-reference/analytics/analytics-overview https://api.zeroclick.io/openapi.json get /v1/analytics/overview Aggregates agent transactions, revenue, wallet sources, and top services for the authenticated organization. # List agent transactions Source: https://docs.zeroclick.ai/api-reference/analytics/list-agent-transactions https://api.zeroclick.io/openapi.json get /v1/analytics/transactions Lists a page of agent payment records for the authenticated organization, newest first, filterable by status, wallet source, and seller. `total` counts the matching records across all pages. # Revenue analytics Source: https://docs.zeroclick.ai/api-reference/analytics/revenue-analytics https://api.zeroclick.io/openapi.json get /v1/analytics/revenue Aggregates settled revenue, average transaction value, and lifetime totals for the authenticated organization. # Seller analytics Source: https://docs.zeroclick.ai/api-reference/analytics/seller-analytics https://api.zeroclick.io/openapi.json get /v1/analytics/sellers/{sellerId} Aggregates calls, transactions, revenue, and new wallets for one seller owned by the authenticated organization. # Seller service analytics Source: https://docs.zeroclick.ai/api-reference/analytics/seller-service-analytics https://api.zeroclick.io/openapi.json get /v1/analytics/sellers/{sellerId}/services Aggregates all-time revenue, transactions, and calls per service and per meter for one seller owned by the authenticated organization. # Create an API key Source: https://docs.zeroclick.ai/api-reference/api-keys/create-an-api-key https://api.zeroclick.io/openapi.json post /v1/api-keys/ Creates an organization API key and returns the secret once. # List API keys Source: https://docs.zeroclick.ai/api-reference/api-keys/list-api-keys https://api.zeroclick.io/openapi.json get /v1/api-keys/ Lists active API keys for the authenticated organization. # Revoke an API key Source: https://docs.zeroclick.ai/api-reference/api-keys/revoke-an-api-key https://api.zeroclick.io/openapi.json delete /v1/api-keys/{apiKeyId} Revokes an active API key for the organization. # API reference Source: https://docs.zeroclick.ai/api-reference/introduction Base URL, authentication, scopes, conventions, and errors for the ZeroClick REST API. The ZeroClick REST API manages your catalog (sellers, services, meters, plans, and prices) and handles usage checks, usage reports, and analytics. The endpoint pages that follow are generated from the live OpenAPI document the API serves, so they always match production. The interactive playground on each page sends real requests; authenticate it with your API key. ## Base URL ```text theme={null} https://api.zeroclick.io ``` All endpoints live under `/v1/` and speak JSON over HTTPS. ## Authentication Authenticate with an API key as a bearer token. Keys start with `zc_` and are shown once. Create them in the [dashboard](https://dashboard.zeroclick.io) under Settings → API keys. See [keys and secrets](/integrate/keys-and-secrets). ```sh theme={null} curl https://api.zeroclick.io/v1/sellers \ -H "Authorization: Bearer $ZEROCLICK_API_KEY" ``` Every key is scoped to your organization and carries one or more scopes: | Scope | Grants | | ------------- | -------------------------------------------------------------------------------------------- | | `admin:read` | Read catalog, analytics, and sandbox endpoints. | | `admin:write` | Create, update, and delete catalog and sandbox resources; create the Stripe onboarding link. | | `usage:read` | `POST /v1/usage/check` (allowance checks). | | `usage:write` | `POST /v1/usage` (usage reports). | Auth requirements differ by route group: * **Usage** endpoints accept API keys only, with the split read and write scopes above. A dashboard session cannot call them. * **Sellers** (including signing secrets), **Services**, **Meters**, **Plans**, **Plan meter prices**, **Analytics**, and **Sandbox** accept an API key (`admin:read` for reads, `admin:write` for writes) or a dashboard session. * **API keys**, **Organizations**, **Users**, and **Stripe Connect** (except `POST /v1/stripe-connect/account-link`, which accepts `admin:write`) are dashboard-session-only. An API key gets `403 {"error":"auth_type_not_allowed"}`. This is why API keys cannot mint more API keys. A key missing a required scope gets `403 {"error":"insufficient_scope"}`. ## Conventions **Ids are prefixed strings**, returned in the `id` field and used in path and query parameters: | Prefix | Resource | | -------- | -------------------------------------------- | | `sel_` | Seller | | `svc_` | Service | | `mtr_` | Meter | | `pln_` | Plan | | `pmp_` | Plan meter price | | `ak_` | API key | | `hsec_` | Signing secret (the `kid` in `zc-signature`) | | `agt_` | Buyer agent | | `zcreq_` | Forwarded request (`zc-request-id`) | | `use_` | Usage event | **Lists are parent-scoped and unpaginated.** List endpoints filter by a required parent id (`GET /v1/services?sellerId=sel_…`, `GET /v1/meters?serviceId=svc_…`, `GET /v1/plan-meter-prices?planId=pln_…`) and return the full set in one response. There are no cursors or page parameters. **Timestamps** are ISO 8601 strings in UTC. **There are no webhooks**; poll the analytics endpoints instead. ## Errors Errors return a machine-readable envelope, with an optional human-readable `reason`: ```json theme={null} { "error": "insufficient_scope" } ``` | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------- | | `400` | Request validation failed. | | `401` | Missing, invalid, or revoked credentials. | | `402` | Usage denial: `access_inactive`, `plan_expired`, `usage_exhausted`. | | `403` | `organization_required`, `insufficient_scope`, `auth_type_not_allowed`, `organization_permission_required`. | | `404` | Resource missing, or owned by another organization; the two are deliberately indistinguishable. | | `409` | Conflict, such as `duplicate_slug` on create or `meter_not_priced` on a usage report. | | `422` | Semantic rejection, such as `payg_price_not_whole_cents`. | The [errors reference](/resources/errors) catalogs every code. # Create a meter Source: https://docs.zeroclick.ai/api-reference/meters/create-a-meter https://api.zeroclick.io/openapi.json post /v1/meters/ Creates one measurable usage dimension for a service. # Delete a meter Source: https://docs.zeroclick.ai/api-reference/meters/delete-a-meter https://api.zeroclick.io/openapi.json delete /v1/meters/{meterId} Soft-deletes a meter and active prices that reference it. # Get a meter Source: https://docs.zeroclick.ai/api-reference/meters/get-a-meter https://api.zeroclick.io/openapi.json get /v1/meters/{meterId} # List meters Source: https://docs.zeroclick.ai/api-reference/meters/list-meters https://api.zeroclick.io/openapi.json get /v1/meters/ Lists usage meters for a service. # Update a meter Source: https://docs.zeroclick.ai/api-reference/meters/update-a-meter https://api.zeroclick.io/openapi.json patch /v1/meters/{meterId} # Create an organization Source: https://docs.zeroclick.ai/api-reference/organizations/create-an-organization https://api.zeroclick.io/openapi.json post /v1/organizations/ Creates an organization, creates an active membership for the caller, and creates the local product organization row. # Get the current organization Source: https://docs.zeroclick.ai/api-reference/organizations/get-the-current-organization https://api.zeroclick.io/openapi.json get /v1/organizations/current Returns the caller's current organization. # Invite a member Source: https://docs.zeroclick.ai/api-reference/organizations/invite-a-member https://api.zeroclick.io/openapi.json post /v1/organization-members/invitations Sends an invitation email so the recipient can join an organization with the given role. # List members Source: https://docs.zeroclick.ai/api-reference/organizations/list-members https://api.zeroclick.io/openapi.json get /v1/organization-members/ Lists the active members of an organization. # List pending invitations Source: https://docs.zeroclick.ai/api-reference/organizations/list-pending-invitations https://api.zeroclick.io/openapi.json get /v1/organization-members/invitations Lists the pending invitations for an organization. # Remove a member Source: https://docs.zeroclick.ai/api-reference/organizations/remove-a-member https://api.zeroclick.io/openapi.json delete /v1/organization-members/{membershipId} Removes a member from an organization. Their access is revoked immediately. # Resend an invitation Source: https://docs.zeroclick.ai/api-reference/organizations/resend-an-invitation https://api.zeroclick.io/openapi.json post /v1/organization-members/invitations/{invitationId}/resend Resends the invitation email for a pending invitation and refreshes its expiry. # Revoke an invitation Source: https://docs.zeroclick.ai/api-reference/organizations/revoke-an-invitation https://api.zeroclick.io/openapi.json delete /v1/organization-members/invitations/{invitationId} Revokes a pending invitation so it can no longer be used. # Update a member's role Source: https://docs.zeroclick.ai/api-reference/organizations/update-a-members-role https://api.zeroclick.io/openapi.json patch /v1/organization-members/{membershipId} # Update the current organization Source: https://docs.zeroclick.ai/api-reference/organizations/update-the-current-organization https://api.zeroclick.io/openapi.json patch /v1/organizations/current Updates the name of the caller's current organization. # Create a plan meter price Source: https://docs.zeroclick.ai/api-reference/plan-meter-prices/create-a-plan-meter-price https://api.zeroclick.io/openapi.json post /v1/plan-meter-prices/ Adds usage pricing for a meter on a buyer-facing plan. # Delete a plan meter price Source: https://docs.zeroclick.ai/api-reference/plan-meter-prices/delete-a-plan-meter-price https://api.zeroclick.io/openapi.json delete /v1/plan-meter-prices/{planMeterPriceId} # Get a plan meter price Source: https://docs.zeroclick.ai/api-reference/plan-meter-prices/get-a-plan-meter-price https://api.zeroclick.io/openapi.json get /v1/plan-meter-prices/{planMeterPriceId} # List plan meter prices Source: https://docs.zeroclick.ai/api-reference/plan-meter-prices/list-plan-meter-prices https://api.zeroclick.io/openapi.json get /v1/plan-meter-prices/ Lists usage prices configured on a plan. # Update a plan meter price Source: https://docs.zeroclick.ai/api-reference/plan-meter-prices/update-a-plan-meter-price https://api.zeroclick.io/openapi.json patch /v1/plan-meter-prices/{planMeterPriceId} # Create a plan Source: https://docs.zeroclick.ai/api-reference/plans/create-a-plan https://api.zeroclick.io/openapi.json post /v1/plans/ Creates a buyer-facing billing plan for a seller. # Delete a plan Source: https://docs.zeroclick.ai/api-reference/plans/delete-a-plan https://api.zeroclick.io/openapi.json delete /v1/plans/{planId} Soft-deletes a plan and its active prices. # Get a plan Source: https://docs.zeroclick.ai/api-reference/plans/get-a-plan https://api.zeroclick.io/openapi.json get /v1/plans/{planId} # List plans Source: https://docs.zeroclick.ai/api-reference/plans/list-plans https://api.zeroclick.io/openapi.json get /v1/plans/ Lists buyer-facing plans for a seller. # Update a plan Source: https://docs.zeroclick.ai/api-reference/plans/update-a-plan https://api.zeroclick.io/openapi.json patch /v1/plans/{planId} # Fund the sandbox test wallet Source: https://docs.zeroclick.ai/api-reference/sandbox/fund-the-sandbox-test-wallet https://api.zeroclick.io/openapi.json post /v1/sandbox/sellers/{sellerId}/wallet/fund Requests a testnet funding drip for the organization's sandbox test wallet. # Get seller sandbox status Source: https://docs.zeroclick.ai/api-reference/sandbox/get-seller-sandbox-status https://api.zeroclick.io/openapi.json get /v1/sandbox/sellers/{sellerId}/status Returns the organization's sandbox test wallet, the seller's sandbox readiness checks, the sandbox spend limits, and the available buyer-agent profiles. # Get the seller's OpenAPI document Source: https://docs.zeroclick.ai/api-reference/sandbox/get-the-sellers-openapi-document https://api.zeroclick.io/openapi.json get /v1/sandbox/sellers/{sellerId}/openapi Fetches the seller's configured OpenAPI document so the sandbox can offer ready-made example requests. Returns the raw document text, or the reason it is unavailable - an unreachable document is not an error, the caller simply gets no examples. # Run a sandbox buyer-agent chat session Source: https://docs.zeroclick.ai/api-reference/sandbox/run-a-sandbox-buyer-agent-chat-session https://api.zeroclick.io/openapi.json post /v1/sandbox/sellers/{sellerId}/chat Runs the sandbox buyer agent against the seller's pay host and streams the conversation, protocol trace events, and wallet snapshots as a UI message stream over server-sent events. # Run one direct sandbox request against the seller's API Source: https://docs.zeroclick.ai/api-reference/sandbox/run-one-direct-sandbox-request-against-the-sellers-api https://api.zeroclick.io/openapi.json post /v1/sandbox/sellers/{sellerId}/direct Sends a single request to the seller's pay host from the sandbox test wallet, with no agent in the loop, and returns the full protocol trace as JSON. With autoPay (the default), 402 challenges along the way are paid automatically, subject to the sandbox spend caps. # Add a seller custom domain Source: https://docs.zeroclick.ai/api-reference/sellers/add-a-seller-custom-domain https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/domains Registers a custom domain for the seller and returns the DNS records the seller must add to verify ownership and serve TLS. # Clear seller body encryption keys Source: https://docs.zeroclick.ai/api-reference/sellers/clear-seller-body-encryption-keys https://api.zeroclick.io/openapi.json delete /v1/sellers/{sellerId}/body-encryption Removes the seller's body encryption configuration; buyers can no longer discover encryption keys for this seller. # Configure the seller's stateful access endpoint Source: https://docs.zeroclick.ai/api-reference/sellers/configure-the-sellers-stateful-access-endpoint https://api.zeroclick.io/openapi.json patch /v1/sellers/{sellerId}/stateful-access Sets the desired-state upsert base URL. Null uses /zeroclick/access under upstreamBaseUrl. Enabling sales remains a staff rollout action. # Create a seller Source: https://docs.zeroclick.ai/api-reference/sellers/create-a-seller https://api.zeroclick.io/openapi.json post /v1/sellers/ Creates a seller profile and upstream API base URL for payment proxying. # Create a seller signing secret Source: https://docs.zeroclick.ai/api-reference/sellers/create-a-seller-signing-secret https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/signing-secrets # Delete a seller Source: https://docs.zeroclick.ai/api-reference/sellers/delete-a-seller https://api.zeroclick.io/openapi.json delete /v1/sellers/{sellerId} Soft-deletes a seller and its active billing setup records. Sellers that have issued stateful accounts must remain available for servicing and cannot be deleted. # Ensure the implementation wizard key set Source: https://docs.zeroclick.ai/api-reference/sellers/ensure-the-implementation-wizard-key-set https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/onboarding/keys Mints or re-serves the credentials the wizard's setup prompts embed: the signing secret, scoped usage keys, and a setup-only admin key. Idempotent within the display window: repeated calls return identical values. A usage key whose value has aged out after real use is returned name-only. # Get a seller Source: https://docs.zeroclick.ai/api-reference/sellers/get-a-seller https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId} # Get seller implementation onboarding progress Source: https://docs.zeroclick.ai/api-reference/sellers/get-seller-implementation-onboarding-progress https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId}/onboarding Lists the implementation wizard's steps with each one's status and verification evidence. Derived steps (catalog, custom domain, payout readiness) are re-checked on every read, so a regressed integration is reflected immediately. # List seller custom domains Source: https://docs.zeroclick.ai/api-reference/sellers/list-seller-custom-domains https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId}/domains # List seller discovery listings Source: https://docs.zeroclick.ai/api-reference/sellers/list-seller-discovery-listings https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId}/discovery Returns where this seller is listed across discovery services. Read-only: registering and refreshing listings stays a staff operation. # List seller signing secrets Source: https://docs.zeroclick.ai/api-reference/sellers/list-seller-signing-secrets https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId}/signing-secrets # List sellers Source: https://docs.zeroclick.ai/api-reference/sellers/list-sellers https://api.zeroclick.io/openapi.json get /v1/sellers/ Lists the seller records owned by the authenticated organization. # Re-check a seller custom domain Source: https://docs.zeroclick.ai/api-reference/sellers/re-check-a-seller-custom-domain https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/domains/{domainId}/verify Refreshes the domain's verification and certificate status from the edge provider. # Read stateful access configuration Source: https://docs.zeroclick.ai/api-reference/sellers/read-stateful-access-configuration https://api.zeroclick.io/openapi.json get /v1/sellers/{sellerId}/stateful-access # Remove a seller custom domain Source: https://docs.zeroclick.ai/api-reference/sellers/remove-a-seller-custom-domain https://api.zeroclick.io/openapi.json delete /v1/sellers/{sellerId}/domains/{domainId} # Revoke a seller signing secret Source: https://docs.zeroclick.ai/api-reference/sellers/revoke-a-seller-signing-secret https://api.zeroclick.io/openapi.json delete /v1/sellers/{sellerId}/signing-secrets/{secretId} # Set seller body encryption keys Source: https://docs.zeroclick.ai/api-reference/sellers/set-seller-body-encryption-keys https://api.zeroclick.io/openapi.json put /v1/sellers/{sellerId}/body-encryption Stores the seller's public-only P-256 JSON Web Key Set and active key id used by buyers to encrypt request bodies end to end. Replaces any existing configuration. # Skip the custom domain step Source: https://docs.zeroclick.ai/api-reference/sellers/skip-the-custom-domain-step https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/onboarding/custom-domain/skip Marks the custom domain step completed without a domain and records the choice in the step ledger. Available only to sellers whose custom domain requirement is waived; everyone else must verify a domain. # Update a seller Source: https://docs.zeroclick.ai/api-reference/sellers/update-a-seller https://api.zeroclick.io/openapi.json patch /v1/sellers/{sellerId} # Verify the seller's API setup Source: https://docs.zeroclick.ai/api-reference/sellers/verify-the-sellers-api-setup https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/onboarding/verify/api-setup Probes the supplied endpoints unpaid against the seller's upstream. Each passes only when the billing guard answers with the payment-required contract. The collected set becomes the step's evidence; all passing marks the step completed, any failure demotes it to in progress. # Verify the seller's storefront pointers Source: https://docs.zeroclick.ai/api-reference/sellers/verify-the-sellers-storefront-pointers https://api.zeroclick.io/openapi.json post /v1/sellers/{sellerId}/onboarding/verify/agent-traffic Fetches the supplied pages and checks each references the seller's storefront origin and carries at least one agent pointer (the llms.txt link or the badge). All passing marks the step completed, any failure demotes it to in progress. # Create a service Source: https://docs.zeroclick.ai/api-reference/services/create-a-service https://api.zeroclick.io/openapi.json post /v1/services/ Creates one sellable API capability for a seller. # Delete a service Source: https://docs.zeroclick.ai/api-reference/services/delete-a-service https://api.zeroclick.io/openapi.json delete /v1/services/{serviceId} Soft-deletes a service and its active meter pricing records. # Get a service Source: https://docs.zeroclick.ai/api-reference/services/get-a-service https://api.zeroclick.io/openapi.json get /v1/services/{serviceId} # List services Source: https://docs.zeroclick.ai/api-reference/services/list-services https://api.zeroclick.io/openapi.json get /v1/services/ Lists services for a seller. # Update a service Source: https://docs.zeroclick.ai/api-reference/services/update-a-service https://api.zeroclick.io/openapi.json patch /v1/services/{serviceId} # Create a connected account login link Source: https://docs.zeroclick.ai/api-reference/stripe-connect/create-a-connected-account-login-link https://api.zeroclick.io/openapi.json post /v1/stripe-connect/login-link Returns a hosted dashboard URL for the authenticated organization's connected payout account. # Create an onboarding link Source: https://docs.zeroclick.ai/api-reference/stripe-connect/create-an-onboarding-link https://api.zeroclick.io/openapi.json post /v1/stripe-connect/account-link Creates or reuses the authenticated organization's hosted onboarding account and returns a single-use onboarding URL. # Designate the branding seller Source: https://docs.zeroclick.ai/api-reference/stripe-connect/designate-the-branding-seller https://api.zeroclick.io/openapi.json put /v1/stripe-connect/branding-seller Chooses which of the organization's sellers the connected payout account's public branding and billing statement descriptor are synced from. Null reverts to the organization's oldest seller. # Disconnect a payout account Source: https://docs.zeroclick.ai/api-reference/stripe-connect/disconnect-a-payout-account https://api.zeroclick.io/openapi.json delete /v1/stripe-connect/account Deletes the connected payout account before soft-deleting the local organization connection. # Get connected payout account status Source: https://docs.zeroclick.ai/api-reference/stripe-connect/get-connected-payout-account-status https://api.zeroclick.io/openapi.json get /v1/stripe-connect/account Returns the hosted onboarding and payout readiness state for the authenticated organization. # Check usage allowance Source: https://docs.zeroclick.ai/api-reference/usage/check-usage-allowance https://api.zeroclick.io/openapi.json post /v1/usage/check Checks whether an access grant can perform an action without recording usage. Items may declare maxQuantity instead of quantity to gate an unknown amount at its ceiling, or declare neither to gate against the meter's configured default cap. # Report usage Source: https://docs.zeroclick.ai/api-reference/usage/report-usage https://api.zeroclick.io/openapi.json post /v1/usage/ Records an idempotent usage report for an access grant. # Get user settings Source: https://docs.zeroclick.ai/api-reference/users/get-user-settings https://api.zeroclick.io/openapi.json get /v1/users/{userId} # Agents and access Source: https://docs.zeroclick.ai/concepts/agents-and-access How ZeroClick identifies buying agents, what zcAgentId and zcBuyerId mean to sellers, why anonymous probes exist, and how access grants work. An agent's identity on ZeroClick is a credential it registers for. Sellers see one opaque, stable id per buyer and handle nothing else: no signup, no buyer API keys, no payment details. ## Identity is a registered credential An agent registers once and exchanges an assertion for an access token, then presents that token as `Authorization: Bearer …`. Buying an entitlement requires one: a plan purchase or top-up binds to the credential, so the buyer keeps its plan however it chooses to pay. Pay-as-you-go callers need no credential and still get a `zcAgentId`: ZeroClick registers an anonymous agent for each payer. How stable that id is depends on the rail: | Anonymous payer | What it gets | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Crypto | A durable id. The same payer returning over any network or protocol keeps the same `zcAgentId`, so it works as a foreign key across sessions. | | Card | A fresh id per payment. Treat these as single-transaction identifiers. | ## What sellers see: `zcAgentId` Sellers only ever see a buyer as `zcAgentId`, an id shaped like `agt_x7f2kq93bh0d`. It is stable per buyer and appears consistently across the product: the `zc-agent-id` request header on forwarded requests, the `zcAgentId` field in 402 challenge bodies, [usage reports and usage events](/concepts/usage-and-allowances), and transaction analytics. Use it as your foreign key for per-buyer state: rate limits, tenancy, audit logs. ## Agents belong to buyers: `zcBuyerId` An agent is one credential. A **buyer** is the person or company behind it, and one buyer can hold several agents: a replacement for a rotated credential, a second agent for a different workload, a recovered identity from an earlier registration. Entitlements live on the buyer, so every agent a buyer holds can draw on every plan the buyer bought, no matter which agent made the purchase. When ZeroClick knows the owner, forwarded requests carry `zc-buyer-id` (`byr_3n8v1c6t5j2w`) alongside `zc-agent-id`: ```http Agent whose owner is known theme={null} zc-request-id: zcreq_8h2m4x0q9k1f zc-agent-id: agt_x7f2kq93bh0d zc-buyer-id: byr_3n8v1c6t5j2w zc-signature: t=1785196800,kid=hsec_k5nq0v7m3d8p,v1=… ``` `zc-agent-id` is always the agent that made **this** call. It never switches to the agent that originally bought the plan being drawn down, so a request from a buyer's second agent looks like exactly what it is. The buyer id is what stays stable across all of them. No `zc-buyer-id` means an anonymous agent: identified, billable, and fine to serve, but not yet attached to an owner you should key durable records to. Prefer `zc-buyer-id` for anything a human should still see after the agent that created it is gone, and `zc-agent-id` for per-caller concerns like rate limits. The [headers reference](/resources/headers) has the full matrix, including what the signature does and does not cover. ## Anonymous probes have no agent id One forwarded request legitimately arrives without a buyer: the signed **anonymous probe**. When an unpaid pay-as-you-go request needs pricing, ZeroClick forwards it upstream so your API can state its price as a `402 payment_required` refusal. ZeroClick re-prices that refusal into the buyer's challenge (see [how ZeroClick works](/concepts/how-zeroclick-works)). Nobody has paid yet, so there is no verified identity to attach: the probe carries no `zc-agent-id` header, and the signature's agent segment is the empty string. Identity binds at verify time, from the signed payment; only the paid retry carries the agent id: ```http Anonymous probe: valid signature, no agent theme={null} zc-request-id: zcreq_8h2m4x0q9k1f zc-signature: t=1785196800,kid=hsec_k5nq0v7m3d8p,v1=… ``` ```http Paid retry: same request id, agent bound theme={null} zc-request-id: zcreq_8h2m4x0q9k1f zc-agent-id: agt_x7f2kq93bh0d zc-signature: t=1785196819,kid=hsec_k5nq0v7m3d8p,v1=… ``` Both are valid, expected traffic. The [seller SDKs](/sdks/overview) and the [verification guide](/integrate/verify-requests) treat probes as a first-class case, so you rarely branch on them yourself. ## Access grants A purchase creates a buyer **access**: the grant that ties one buyer to one plan at one seller, and the thing every allowance check evaluates. Each buyer holds at most one active access per seller. Buying again or switching plans replaces the grant in place rather than stacking a second one. Any remaining credit carries forward onto the new purchase, so an unspent balance survives a plan switch. An access carries: * `status`: `active` or `inactive`. Checks against an inactive access are denied `access_inactive`. * `periodStartsAt` / `periodEndsAt`: the current period for subscription-style plans. A check after `periodEndsAt` is denied `plan_expired`. * `remainingCreditUsd`: the prepaid balance on credit plans, or the included usage credit on `subscription_usage` plans. Usage draws it down; an amount it cannot cover is denied `usage_exhausted`. Pay-as-you-go needs no access at all: first-contact buyers pay per call and go. The [plans and pricing](/concepts/plans-and-pricing) page covers what each billing mode means for these fields, and [usage and allowances](/concepts/usage-and-allowances) covers the checks that consume them. Identity stays verified per call. Even on a purchased plan, every request carries the buyer's credential, and ZeroClick resolves it to the same `agt_…` before it forwards, so your API needs no buyer authentication of its own. The `zc-signature` header is the trust boundary. # How ZeroClick works Source: https://docs.zeroclick.ai/concepts/how-zeroclick-works The life of one paid agent request: transparent proxying, the single priced 402 challenge, the signed forward, the allowance check, and settlement. ZeroClick is a transparent paid proxy in front of your API. An agent calls your pay URL, `https://acme.pay.zeroclick.io/`, with exactly the request it would send your API directly: the same method, the same path and query, the same body. ZeroClick prices the call, collects payment over [x402 or MPP](/concepts/payment-protocols), and forwards the request to your upstream base URL. It signs the forward so your backend can trust it. This page follows one pay-as-you-go request through that lifecycle at the protocol level; the [quickstart](/quickstart) covers the integration itself. The full flow is two round trips from the agent's side. The agent talks only to your pay URL, and your API talks only to ZeroClick: ```mermaid theme={null} sequenceDiagram autonumber participant A as Buying agent participant Z as ZeroClick
(acme.pay.zeroclick.io) participant S as Your API
(upstream base URL) rect rgba(25, 25, 255, 0.06) note over A,S: Round trip 1: price the request A->>Z: POST /v1/product-watch Z->>S: same request, signed
zc-request-id + zc-signature (no agent id) S-->>Z: 402 refusal with declared usage Z-->>A: one priced 402 challenge
payment-required and www-authenticate end rect rgba(25, 25, 255, 0.06) note over A,S: Round trip 2: pay and get served A->>Z: identical request + payment proof
x-payment or authorization Z->>S: same request, signed
zc-request-id + zc-agent-id + zc-signature S->>Z: POST /v1/usage/check Z-->>S: allowed S-->>Z: 200 + zc-usage header Z-->>A: 200 response + zc-billing header end ``` The two arrows in the middle of round trip 2 are your backend calling ZeroClick's REST API at `api.zeroclick.io`; every other arrow carries the agent's own request and response. ZeroClick strips the `zc-usage` header before the response reaches the agent. The agent sends its normal request to `https://acme.pay.zeroclick.io/v1/product-watch`: no signup, no API key, no ZeroClick-specific headers. The pay URL host names the seller; the method, path, query, and body are your API's own. An unpaid request needs a price before ZeroClick can charge for it. For pay-as-you-go, ZeroClick forwards it upstream as a signed **anonymous probe**: the forward carries `zc-request-id` and `zc-signature` but no `zc-agent-id`. Your API refuses the probe with the exact body `{"error":"payment_required","serviceSlug":"product-watch","usage":[{"meterSlug":"requests","quantity":1}]}`. ZeroClick prices that declared usage against your catalog and answers the agent with a single `402 payment_required` challenge. The challenge arrives in one response, several ways at once: the body carries the exact amount (`payment.amountUsd`) and a block per protocol. The full signable challenges ride in the `payment-required` (x402) and `www-authenticate` (MPP) response headers. The total is one priced 402, so a stock single-payment client completes the flow unmodified. The agent pays the challenge with its wallet and retries the identical request with the proof attached: the signed x402 payload in the `x-payment` header, or an MPP credential in `authorization: Bearer …`. The payment binds to the challenged request's body digest, so the retry must carry the same bytes. ZeroClick refuses a different body with `409 payment_request_mismatch`. ZeroClick verifies the payment, binds the paying wallet to a [buyer agent](/concepts/agents-and-access) (`agt_…`), and forwards the request to your `upstreamBaseUrl`. It strips inbound `zc-*` and payment headers, then sets its own three: `zc-request-id`, `zc-agent-id`, and `zc-signature`. The signature is an HMAC over the method, path, body digest, and both ids (see the [signature spec](/integrate/signature-spec)). Your backend [verifies the signature](/integrate/verify-requests) first, then asks `POST /v1/usage/check` whether the buyer's plan covers the declared usage. The check is a pure decision: it [records nothing and burns nothing](/concepts/usage-and-allowances). If the check allows, your API does the work. If it denies, your API returns the 402 refusal body. ZeroClick turns that refusal into the buyer's next challenge. Your API settles actual usage synchronously, in a `zc-usage` response header on the 2xx response (preferred), or reports it asynchronously via `POST /v1/usage`. ZeroClick records the usage and settles the payment, at actual usage when the challenge priced a [ceiling](/integrate/charge-up-to-a-maximum). It strips `zc-usage` so the agent never sees it, then returns your response to the agent with a `zc-billing` header. ## Who does what | Actor | Responsibility | | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Agent | Discovers the storefront, sends normal API requests, signs and pays 402 challenges, retries with the proof attached. | | ZeroClick | Prices the challenge, verifies the payment, resolves wallet identity, signs and forwards the request, records usage, settles the money. | | Your API | Verifies `zc-signature`, checks the allowance, does the work, returns the exact 402 refusal on denial, settles usage. | The seller side of this table is the whole integration: verify, check, serve, settle. The [integration overview](/integrate/overview) states that contract on one page, and the [seller SDKs](/sdks/overview) implement it. ## One id ties it together `zc-request-id` (`zcreq_…`) is the correlation spine. ZeroClick mints it when a request first arrives and reuses it end to end. The anonymous probe, the challenge body (as `zcRequestId`), the paid retry, your allowance check, and the recorded usage events all carry the same id. Log it on every guarded request, and you can trace any transaction across your systems and the dashboard. The agent's side closes the loop with `zc-billing`, a response header on every per-call paid response that reconciles the authorized amount against the charged amount: ```json theme={null} { "status": "settled", "authorizedUsd": "0.016384", "chargedUsd": "0.009046", "remainderUsd": "0.007338", "remainderHandling": "escrow_returned", "settleTransactionHash": "0x…" } ``` For a fixed-quantity request, the authorized and charged amounts match. For a ceiling-priced request, the authorized amount is the most the agent could have paid, and the remainder went back to its wallet. `"status": "settling"` means on-chain settlement has not finished, so the final amounts are not stamped yet. Agents on purchased plans follow the same lifecycle with one difference: a credit or subscription purchase happens once, up front, through the storefront. After that, each call carries an identity proof instead of a fresh payment, and covered calls forward without a new 402. See [plans and pricing](/concepts/plans-and-pricing). The [headers reference](/resources/headers) catalogs every header in this flow. # Payment protocols Source: https://docs.zeroclick.ai/concepts/payment-protocols How ZeroClick uses x402 and MPP: challenge and proof headers, minted deposit addresses, reserve rails for ceiling charges, and settlement. ZeroClick accepts two agent payment protocols: [x402](https://www.x402.org) and MPP, the [Machine Payments Protocol](https://mpp.dev). Both settle USDC. This page explains how ZeroClick uses them, not how they work internally; as a seller you never implement either. Your API sees only signed, already-paid requests, and the billing guard is identical whichever rail the buyer chose. ## The two rails Both protocols follow the same loop: ZeroClick answers an unpaid request with the [single priced 402](/concepts/how-zeroclick-works), the agent signs the challenge with its wallet, and the retry carries the proof in a standard header. An anonymous challenge offers both protocols and lets the buyer pick by paying; a buyer that already authenticated on one rail is challenged on that rail. | | x402 | MPP | | -------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- | | Challenge rides in | `payment-required` response header (and the standard x402 document in the 402 body) | `www-authenticate` response header | | Proof rides in | `x-payment` request header (signed payment payload) | `authorization: Bearer ` request header | | Settles | USDC on Base (`eip155:8453`) | USDC on Tempo | | Fixed-quantity charges | `exact` scheme | one-shot charge | | Ceiling charges | `upto` scheme (reserve, settle at actual) | session escrow channel | | Receipt header on paid responses | `payment-response` | `payment-receipt` | Inside the 402 body, each offered rail appears as a block under `protocols`, alongside ZeroClick's own `payment` block with the exact USD amount: ```json theme={null} { "error": "payment_required", "zcRequestId": "zcreq_8h2m4x0q9k1f", "payment": { "id": "apay_…", "amountUsd": "0.010000" }, "protocols": { "x402": { "x402Version": 2, "network": "eip155:8453", "scheme": "exact", "amount": "10000", "asset": "0x…" }, "mpp": { "challengeId": "…", "method": "tempo", "intent": "charge" } } } ``` The challenge an agent signs embeds ZeroClick's payment metadata (the `payment.id` and `zcRequestId` above), so the signed proof is all a retry needs. Identity-only challenges use the same machinery at amount zero: proving wallet control is always free, and nothing settles on-chain for them. ## ZeroClick mints the deposit address Every priced challenge pays into a deposit (`payTo`) address that ZeroClick minted for that specific payment. Buyers pay exactly the address the challenge specifies. Sellers never publish, rotate, or even see these addresses. ## Ceiling charges need a reserve rail A [ceiling-priced request](/integrate/charge-up-to-a-maximum), one whose usage item declares a `maxQuantity`, authorizes the most the call could cost, but must charge only actual usage. That is possible only on the reserve-and-pay-actual rails, so a ceiling challenge offers exactly these: * **x402 `upto`**: the wallet reserves the ceiling; ZeroClick settles the actual amount after delivery, and the reserved remainder never leaves the buyer's wallet. * **MPP session escrow**: the buyer funds a channel to the ceiling and signs one voucher; ZeroClick closes the channel at actual usage, and the escrow refunds the rest. If the close ever fails, the buyer can force-close after the on-chain grace period, so escrowed funds are never stranded. ZeroClick never offers one-shot prepay (`exact`, or the MPP charge) for a ceiling: that is what guarantees an unspent ceiling cannot get stuck as seller credit. If neither reserve rail is available for a capped price, ZeroClick refuses to mint an unpayable challenge and returns `503 capped_pricing_unavailable` instead. A failed delivery debits nothing on either rail, and every per-call paid response reconciles authorized versus charged in the `zc-billing` header. ## Amounts and settlement Two money rules shape what the rails carry: * Amounts that settle on-chain as a single charge are **whole cents**: plan purchases, top-ups, subscription base prices, and pay-as-you-go rates. Catalog prices on credit and subscription plans keep 6-decimal precision, because they only burn prepaid balance. [Plans and pricing](/concepts/plans-and-pricing) covers both rules. * Buyers pay USDC, but your revenue settles to your connected Stripe account (in fiat or USDC, your choice). You never operate a wallet to sell through ZeroClick. ## Sellers stay protocol-agnostic Nothing protocol-specific ever reaches your API. ZeroClick strips payment headers before forwarding, and your integration is the same four steps on every rail: verify the [signature](/integrate/verify-requests), [check the allowance](/integrate/check-allowances), serve, and [settle usage](/integrate/settle-usage). When ZeroClick adds a new payment protocol, sellers ship nothing. For the protocols themselves, see [x402.org](https://www.x402.org) and [mpp.dev](https://mpp.dev). # Plans and pricing Source: https://docs.zeroclick.ai/concepts/plans-and-pricing The four billing modes, plan meter prices with unitSize and includedUnits, and the whole-cent and six-decimal precision rules. A plan defines how agents pay a seller. Each plan has a billing mode, a few plan-level money fields, and a set of **plan meter prices** that put a price on each [meter](/concepts/stores-services-meters). Together they determine what a 402 challenge costs and what an [allowance check](/concepts/usage-and-allowances) draws against. ## The four billing modes | Mode | For the buyer | For allowance checks | | -------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payg` | No purchase. Each call returns one priced 402 with the exact cost; pay and retry. | Covered by the verified per-call payment, by included units in free mode (claimed agents with a verified email only), or by a prepaid balance the buyer still holds. | | `credit` | Prepay a balance of at least `minimumPurchaseUsd`, call until it runs out, top up any time. | Passes while `remainingCreditUsd` covers the declared usage at the plan's prices; otherwise denied `usage_exhausted`. | | `subscription` | Purchase once at `basePriceUsd` for the period (month or year). Calls are allowed while the period runs. | Passes while the access period is current; a lapsed period is denied `plan_expired`. No usage balance is burned. | | `subscription_usage` | A subscription whose purchase also grants `includedCreditUsd` of usage; usage beyond it is metered and charged per call. | Passes while the period is current and the included credit covers the declared usage; beyond it, per-call charges take over. | Plans carry an `interval` (`none`, `month`, or `year`) that sets the access period a purchase grants, and three money fields: * `basePriceUsd`: what a subscription purchase charges; a whole-cent amount. * `includedCreditUsd`: the usage credit a `subscription_usage` purchase grants; up to 6 decimals. * `minimumPurchaseUsd`: the smallest credit purchase a buyer may make; whole cents, default \$0.01. A purchase creates the buyer's [access grant](/concepts/agents-and-access), which later checks evaluate. ## Plan meter prices A plan meter price attaches a price to one meter on one plan, with one price per meter per plan: * `priceUsd`: the price, up to 6 decimal places. * `unitSize`: how many units `priceUsd` buys (default 1). `priceUsd: "0.002"` with `unitSize: 1000` reads "\$0.002 per 1,000 units". * `includedUnits`: free units per billing period before `priceUsd` applies (default 0). * `defaultMaxQuantity`: an optional per-request ceiling, applied when a request declares neither a `quantity` nor a `maxQuantity` for the meter. Without a default, such an item is denied `meter_not_priced`. Cost is quantity × `priceUsd` ÷ `unitSize`, rounded to six decimal places. ## A worked example Acme prices the `output_tokens` meter of its `product-watch` service on a credit plan at \$0.002 per 1,000 tokens: ```json theme={null} { "priceUsd": "0.002", "unitSize": 1000, "includedUnits": 0, "defaultMaxQuantity": 8192 } ``` A request that settles 4,523 output tokens costs 4,523 × $0.002 ÷ 1,000 = **$0.009046\*\*, deducted from the buyer's `remainingCreditUsd`. A request that declares no token count up front is gated and authorized at the 8,192-token default ceiling (at most \$0.016384). It settles at the actual count, and the remainder returns to the buyer. [Charge up to a maximum](/integrate/charge-up-to-a-maximum) covers ceiling billing from the seller's side. If the price also carried `includedUnits: 100000`, the buyer's first 100,000 tokens each period would be free, and `priceUsd` would apply only beyond them. ## Two precision rules 1. **Money that settles on-chain is whole cents.** Buyer-chosen amounts (credit purchases and top-ups) must be whole-cent values of at least `minimumPurchaseUsd`, and `basePriceUsd` is whole cents. Because ZeroClick charges a pay-as-you-go rate call by call, a payg plan's `priceUsd` must itself be a whole-cent amount. The API rejects anything else with `422 payg_price_not_whole_cents` (see [errors](/resources/errors)). 2. **Catalog prices carry 6 decimals.** Prices on credit and subscription plans only burn prepaid allowance, so they keep full 6-decimal precision: sub-cent rates like `0.000250` are normal there. `unitSize` is how pay-as-you-go expresses sub-cent rates within the whole-cent rule: the credit-plan rate above becomes `priceUsd: "2.00"` with `unitSize: 1000000` on payg. That is the same \$0.002 per 1,000 tokens, stated as a whole-cent price per million. ## Managing plans ZeroClick sets up plans and prices with you during onboarding; you manage them afterward in the [dashboard](https://dashboard.zeroclick.io) or with the [REST API](/api-reference/introduction). Agents always read live prices from the storefront's `manifest.json`, so a price change takes effect on their next call. # Sellers, services, and meters Source: https://docs.zeroclick.ai/concepts/stores-services-meters The ZeroClick catalog model: how sellers, services, and meters are identified, where each slug surfaces, and the uniqueness rules that apply. Your catalog on ZeroClick has three levels. A **seller** is one storefront. It contains **services**, the purchasable things, and each service contains **meters**, the billable measurements within it. Every identifier an agent sees on the storefront, and every identifier your guard sends to ZeroClick, resolves through these three levels. Catalog pricing is a separate concern; [plans and pricing](/concepts/plans-and-pricing) covers it. ## Sellers A seller is one agent-facing storefront; the dashboard presents it as your store. Its slug names the hosted pay URL: slug `acme` gives `https://acme.pay.zeroclick.io`, and a custom domain such as `agents.acme.com` can front the same storefront. Seller slugs are unique across all of ZeroClick. Two seller fields do the heavy lifting: * `upstreamBaseUrl`: the origin ZeroClick forwards paid requests to, such as `https://api.acme.internal`. Agents never see it: they call the pay URL, and ZeroClick maps the path onto your upstream on the signed forward. * Storefront metadata: name, description, tagline, tags, logo, docs and OpenAPI URLs, contact email. Agents read this metadata when they discover you, both on the storefront and in the machine-readable catalog it serves at `/manifest.json`. The pay URL is a transparent proxy over the upstream, so the mapping is mechanical. An agent sends the request it would have sent you directly: ```http theme={null} POST /v1/product-watch HTTP/1.1 Host: acme.pay.zeroclick.io { "url": "https://example.com/product/42" } ``` and, once paid, ZeroClick forwards the same method, path, query, and body to `https://api.acme.internal/v1/product-watch` with the `zc-request-id`, `zc-agent-id`, and `zc-signature` headers attached. The full lifecycle is in [how ZeroClick works](/concepts/how-zeroclick-works). ## Services A service is one purchasable thing: an API, a model, a workflow, or a feature. It has a name, an optional description, and a slug that is unique per seller: two sellers can both have a `product-watch`, but one seller cannot have two. The service slug is the identifier your integration speaks. Your guard passes it as `serviceSlug` in [allowance checks](/integrate/check-allowances) and [usage reports](/integrate/settle-usage). Your API names it in the `402 payment_required` refusal body that ZeroClick re-prices into a challenge. ## Meters A meter is one billable measurement within a service: `requests` with unit "request", or `output_tokens` with unit "token". A meter is only a key and a unit: prices attach to meters through plans, so the same meter can cost different amounts on different plans. Meter keys must match `^[a-z][a-z0-9_]*$` (up to 80 characters) and are unique per service. The key appears as `meterSlug` everywhere usage is named: allowance checks, usage reports, the `zc-usage` response header, and the `usage` items in 402 challenges. ## Where each identifier surfaces | Level | Example | Unique within | Surfaces as | | ------------ | --------------- | ---------------- | -------------------------------------------------------------- | | Seller slug | `acme` | all of ZeroClick | the pay URL host, `acme.pay.zeroclick.io` | | Service slug | `product-watch` | its seller | `serviceSlug` in guard calls, 402 refusals, and the catalog | | Meter key | `output_tokens` | its service | `meterSlug` in allowance checks, usage reports, and `zc-usage` | Seller and service slugs are lowercase kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`, up to 80 characters); meter keys use underscores instead of hyphens. ## What agents read The pay URL doubles as the seller's machine-readable surface. Alongside the storefront itself, it serves: * `/manifest.json`: the live catalog (services, meters, plans, and prices) as JSON. Agents read it at call time, so catalog changes reach buyers without a republish step. * `/llms.txt` and `/llms-full.txt`: an agent-facing index and full guide for calling your API through ZeroClick, generated from the same catalog. ZeroClick generates all three from your catalog; none of them is something you author or host. ## Managing the catalog ZeroClick sets up your catalog with you during onboarding. After that, you manage it in the [dashboard](https://dashboard.zeroclick.io), or with the [REST API](/api-reference/introduction) when you want to automate changes. Sellers, services, and meters are plain resources scoped to your organization. Changing a slug changes the identifier your guard must send, so coordinate slug changes with a deploy of your integration. # Usage and allowances Source: https://docs.zeroclick.ai/concepts/usage-and-allowances What an allowance check decides, the three usage-item forms, the seven denial reasons, free mode, and how sync and async settlement stay idempotent. An allowance is the answer to one question, asked before billable work runs: does this buyer's plan cover this usage? Your API asks it with `POST https://api.zeroclick.io/v1/usage/check` (API key scope `usage:read`). The body carries the request's `zcRequestId`, the `serviceSlug`, and 1 to 20 usage items: one per meter, no duplicates. The response is a pure decision: ```json Request theme={null} { "zcRequestId": "zcreq_8h2m4x0q9k1f", "serviceSlug": "product-watch", "usage": [{ "meterSlug": "output_tokens", "maxQuantity": 8192 }] } ``` ```json Response theme={null} { "allowed": true, "reason": null } ``` A check records nothing and burns nothing: no usage event, no credit deduction, no included units consumed. It exists so you can refuse before doing the work; billing happens only when you settle. The [check allowances](/integrate/check-allowances) guide covers calling it; this page covers what the answers mean. ## The three usage-item forms Each usage item names a meter and sizes the usage one of three ways. An item may not declare both a quantity and a ceiling. * `quantity`: the exact amount, known up front ("this call is 1 request"). * `maxQuantity`: a ceiling for work you cannot size in advance ("at most 8,192 output tokens"). The check gates at the ceiling: could the largest allowed call go through? * Neither: the item falls back to the meter's `defaultMaxQuantity` from the [plan meter price](/concepts/plans-and-pricing) and gates at that ceiling. If the meter has no default, there is nothing to gate against, and the check denies `meter_not_priced`. Quantities are integers from 1 to 2,147,483,647. ## The seven denial reasons `allowed: false` always comes with exactly one machine-readable reason: | Reason | Meaning | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `service_not_found` | The `serviceSlug` does not name a service in your catalog. | | `access_not_found` | Nothing stands behind the request: no active access, no verified payment, no free-mode coverage. This is the normal first-touch denial that becomes a priced 402. | | `access_inactive` | The buyer's access exists but is not active, or its period has not started. | | `plan_expired` | The access period has ended (`periodEndsAt` is in the past). | | `meter_not_found` | The `meterSlug` does not name a meter on this service. | | `meter_not_priced` | The buyer's plan has no price for this meter, or the item declared neither quantity nor ceiling and the meter has no `defaultMaxQuantity`. | | `usage_exhausted` | The plan prices the usage, but the remaining balance or allowance cannot cover the declared amount. | Your API turns a denial into the `402 payment_required` refusal body, and ZeroClick re-prices it into the buyer's next challenge. A denial is a payment prompt, not a dead end. The [errors reference](/resources/errors) lists the response shapes. ## Free mode Prices can carry `includedUnits`: free units per period before the price applies. On pay-as-you-go, consuming them is an explicit opt-in: the agent sends the `zc-mode: free` request header on each call. A missing or unrecognized header means paid; ZeroClick never rejects a call over it. Anonymous free-mode calls are never served free, and neither are unclaimed agents: the allowance is reserved for agent identities claimed by a human with a verified email, and it is scoped to that buyer - consumption aggregates across every agent the same human owns, so claiming more agents never grants more free usage. ZeroClick answers anonymous calls with a \$0 identity challenge first, and the identified retry gets the real coverage check, including the claimed-and-verified requirement. ZeroClick records the mode on the request, so your allowance check and the usage recorder honor the same decision ZeroClick made. See [free and identity endpoints](/integrate/free-and-identity-endpoints). ## Settling usage: sync and async A check gates; a settlement records. There are two ways to settle, and both write the same usage events: * **Sync (preferred):** put a `zc-usage` header on your 2xx response. ZeroClick records it while returning the response and strips the header before the agent sees it. A request can never settle the same meter twice. ```http theme={null} zc-usage: [{"serviceSlug":"product-watch","meterSlug":"output_tokens","quantity":4523}] ``` * **Async:** call `POST https://api.zeroclick.io/v1/usage` (scope `usage:write`) with `zcAgentId`, `serviceSlug`, `meterSlug`, `quantity`, an optional `occurredAt`, and an `idempotencyKey`. Async events are unique per `(service, idempotencyKey)`. Keys are yours and must be derived, never random (`zcreq_8h2m4x0q9k1f_output_tokens`), so a retry reproduces the same key. A replay answers `"recorded": true, "duplicate": true` with the originally stored event: a success, not an error. Every recorded usage event carries `source: "sync"` or `"async"`, along with the `zcAgentId`, `zcRequestId`, `meterSlug`, `quantity`, and the computed `totalCostUsd`. This is the same record you see in the dashboard and the [REST API](/api-reference/introduction). ZeroClick can also refuse a report after the fact: `402` for `access_inactive`, `plan_expired`, or `usage_exhausted`, `409` for `meter_not_priced`, and `404` otherwise. That is one more reason to check before you serve. The [settle usage](/integrate/settle-usage) guide covers both paths in code. # What is ZeroClick? Source: https://docs.zeroclick.ai/index ZeroClick turns any API or offering into an agent-purchasable service, with x402 and MPP support, a machine-readable storefront, and agent transaction analytics. A dotted globe with agent activity lights converging on ZeroClick storefronts A dotted globe with agent activity lights converging on ZeroClick storefronts ZeroClick is the storefront and transaction layer for selling to AI agents. Connect an existing API or product, and agents can discover it, pay for it with [x402](https://www.x402.org) or [MPP](https://mpp.dev), and use it. You keep the API and the product experience; ZeroClick operates the agent-facing payment, identity, and proxy layer in front of it. Revenue settles to your connected Stripe account, in fiat or USDC. ## How it works Every seller gets a hosted pay URL, such as `https://acme.pay.zeroclick.io`. The pay URL is a machine-readable storefront: agents read it to find your services, plans, and prices, then transact with your API through it. A shopping assistant, coding agent, or procurement agent discovers your storefront, picks a plan, and calls your service through the pay URL using x402 or MPP. ZeroClick resolves the agent's identity, handles the payment challenge, then signs the request and forwards it to your API. If an agent shows up without a wallet, ZeroClick creates one on the spot. Your API verifies the request signature, checks the agent's allowance, and returns the result. Funds settle to your Stripe account. ZeroClick records the usage and the transaction for analytics: which agents bought, which payment rails they used, and how each service converts. The [how ZeroClick works](/concepts/how-zeroclick-works) page walks the same lifecycle at the protocol level. ## What ZeroClick handles, and what you build ZeroClick operates the agent-facing side: the storefront, payment challenges, wallet and identity resolution, payment verification and settlement, request signing, usage recording, and transaction analytics. You add one thing to your API: a billing guard. On each forwarded request, your backend verifies ZeroClick's signature, checks the agent's allowance, does the work, and reports what was used. The [seller SDKs](/sdks/overview) for TypeScript, Python, and Go implement the guard; the [REST walkthrough](/integrate/rest-walkthrough) covers doing it without an SDK. ## Core concepts | Concept | Meaning | | --------- | ----------------------------------------------------------------------------------------------- | | Seller | Your storefront on ZeroClick. Each seller gets a pay URL that agents call and pay through. | | Service | One thing agents can buy: an API, a model, a workflow, or a feature. | | Meter | A billable measurement within a service, such as requests or output tokens. | | Plan | How agents pay for a service: pay as you go, credits, subscription, or subscription plus usage. | | Agent | The buyer. Every agent is identified and verified, and every call is logged. | | Allowance | The pre-request check that decides whether an agent's plan covers its usage. | ## Explore Serve your first paid agent request in about ten minutes. The integration contract: verify, check allowance, serve, settle usage. The website changes that point agents at your storefront. Seller SDKs for TypeScript, Python, and Go. Manage sellers, services, meters, plans, usage, and analytics with the REST API. Questions? Email [help@zeroclick.ai](mailto:help@zeroclick.ai). # Charge up to a maximum Source: https://docs.zeroclick.ai/integrate/charge-up-to-a-maximum Bill work you can't size up front (output tokens, pages, seconds of processing) by declaring a ceiling and settling at the actual amount used. You can't price some work until it is done: tokens generated, pages extracted, seconds of processing. Charging a flat worst-case rate overcharges everyone else; metering after the fact leaves the buyer unauthorized. ZeroClick's answer is a ceiling: declare the most the request could use, let the buyer authorize that maximum, and settle at what the request actually used. ## Charging up to a maximum Declare the ceiling with `maxQuantity` instead of `quantity`: ```ts TypeScript theme={null} const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [ { meterSlug: "requests", quantity: 1 }, { meterSlug: "output_tokens", maxQuantity: 100_000 }, ], }); ``` ```python Python theme={null} decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[ UsageItem(meter_slug="requests", quantity=1), UsageItem(meter_slug="output_tokens", max_quantity=100_000), ], ) ``` ```go Go theme={null} result, err := seller.Guard(r.Context(), sellers.FromHTTP(r, body), "product-watch", []sellers.UsageItem{ sellers.PerRequest("requests", 1), sellers.UpTo("output_tokens", 100_000), }, "") ``` The buyer authorizes up to the ceiling, and the payment settles at the actual usage you report: a ceiling never overcharges. The SDK rejects a usage item that declares both `quantity` and `maxQuantity` before any network call. You can also declare **neither**: the item then defers to the meter's configured **Max usage per request** (`defaultMaxQuantity` on the plan's meter price). That keeps the ceiling in your catalog instead of your code. If the meter has no configured maximum, the allowance check denies with `meter_not_priced`. On the buyer's side, a ceiling is payable only on the reserve payment rails: the agent authorizes the maximum and pays the actual amount you settle. An exact `quantity` lets ZeroClick offer the immediate-settle scheme more clients can pay today, so when you know the size, prefer fixed charges. See [payment protocols](/concepts/payment-protocols). ## Settle at the actual amount A ceiling has no settled quantity until you report one. Settle it like any other usage: on the response for amounts you know by the end of the handler, or [asynchronously](/integrate/settle-usage#report-usage) for work that finishes later. ```ts TypeScript theme={null} // 4,200 tokens actually produced, against the 100,000 ceiling. return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, { serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4200 }, ]); ``` ```python Python theme={null} return to_fastapi( zeroclick.with_usage( ZcResponse.json({"tokens": 4200}), [ SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1), SyncUsageItem(service_slug="product-watch", meter_slug="output_tokens", quantity=4200), ], ) ) ``` ```go Go theme={null} value, err := sellers.UsageHeader([]sellers.SyncUsageItem{ {ServiceSlug: "product-watch", MeterSlug: "requests", Quantity: 1}, {ServiceSlug: "product-watch", MeterSlug: "output_tokens", Quantity: 4200}, }) if err == nil { w.Header().Set("zc-usage", value) // before writing the body; 2xx only } ``` The settled quantity can be anywhere from zero up to the declared ceiling. An unreported ceiling on a paid, delivered response settles at zero (safe for the buyer, unbilled for you), so the report is what turns the work into revenue. See [settle usage](/integrate/settle-usage) for the header rules (2xx only, stripped by ZeroClick). ## Go: Meter takes fixed quantities only Go's `Meter` middleware settles exactly what it declares, so it accepts fixed quantities only. Handed an `UpTo` item, it **panics at wire-up**: a ceiling has no settled quantity, and the middleware would otherwise bill zero, silently, on a delivered 200. You have two working shapes: call `Guard` yourself and settle explicitly, or keep `Meter` for a fixed per-request charge and report the variable part after responding. ```go theme={null} mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) tokens := doTheWork() // discovered by doing the work w.Header().Set("content-type", "application/json") fmt.Fprintf(w, `{"tokens":%d}`, tokens) // Reported after the response, on BackgroundContext (r.Context() is // already cancelled) with a derived idempotency key. _, err := seller.ReportUsage(sellers.BackgroundContext(r), sellers.ReportUsageInput{ AgentID: zc.AgentID, ServiceSlug: "product-watch", MeterSlug: "output_tokens", Quantity: tokens, IdempotencyKey: zc.RequestID + "_output_tokens", }) if err != nil { log.Printf("output_tokens not reported: %v", err) } }))) ``` The fixed charge keeps the immediate-settle scheme available to buyers; the report settles the variable part. The same fixed-plus-reported split works in any language when you know the variable amount only after the response. See [settle usage](/integrate/settle-usage#report-usage) for the reporting rules. # Check allowances Source: https://docs.zeroclick.ai/integrate/check-allowances Guard before work: declare what a request will use, let ZeroClick decide whether the buyer's plan covers it, and handle denials and allowance-API outages. After a signature verifies, and before your API does any work, ask ZeroClick whether the buyer's plan covers what this request will use. That is the allowance check: `POST /v1/usage/check` with the request's `zcRequestId` and the declared usage. Checking records nothing and burns no credit; it is a pre-gate, not a charge. For the model behind it, see [usage and allowances](/concepts/usage-and-allowances). The SDK `guard` runs the check for you, immediately after verification: ```ts TypeScript theme={null} const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (decision.action === "deny") return decision.response; // decision.context.zcRequestId, decision.context.zcAgentId // decision.allowance.status === "allowed" | "unavailable" ``` ```python Python theme={null} decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) # decision.context.zc_request_id, decision.context.zc_agent_id # decision.allowance == "allowed" | "unavailable" ``` ```go Go theme={null} result, err := seller.Guard(r.Context(), sellers.FromHTTP(r, body), "product-watch", []sellers.UsageItem{{MeterSlug: "requests", Quantity: 1}}, "") if err != nil { // The usage list is malformed, or the outage policy is Throw. http.Error(w, `{"error":"internal_error"}`, http.StatusInternalServerError) return } if !result.OK { result.Response.WriteTo(w) return } // result.Context.RequestID, result.Context.AgentID // result.Outcome == sellers.OutcomeAllowed | sellers.OutcomeUnavailable ``` In Go, the [`Meter` middleware](/sdks/go/middleware) wraps `Guard` and the usage settlement in one. When you need the decision in your own handler, call `Guard` directly. ## Declare what the request will use Each usage item names a meter and takes one of three forms: | Form | Meaning | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `quantity` | An exact amount, known up front. | | `maxQuantity` | A ceiling for work you can't size yet; the payment settles at what you [report](/integrate/settle-usage). See [charge up to a maximum](/integrate/charge-up-to-a-maximum). | | Neither | Defer to the meter's configured **Max usage per request** (`defaultMaxQuantity`). If the meter has none, the check denies with `meter_not_priced`. | Before any network call, the SDK rejects an item that declares both `quantity` and `maxQuantity`. A check takes 1 to 20 items, one per meter. The SDK rejects duplicate meters too. ## Read the decision `guard` returns a decision, not an exception. A buyer who can't pay is an expected event: * **Deny** carries a ready-to-return response: the `401` for a failed signature, the exact `402 payment_required` body for a business denial, or the `503` under a fail-closed outage policy. Return it unchanged (`decision.response` in TypeScript and Python, `result.Response` in Go). Do no work. The `reason` field carries one of the seven denial reasons, listed at [errors](/resources/errors). * **Allow** carries the verified context (`zcRequestId`, `zcAgentId`) and an allowance status: | Status | Meaning | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowed` | The buyer's plan covers the declared usage. | | `unavailable` | The allowance API gave no answer and your policy chose to serve anyway: possibly unbilled work. | | `not_required` | Identity was proven and no allowance was needed. Only `guardIdentity` produces this. See [free and identity endpoints](/integrate/free-and-identity-endpoints). | ## When the allowance API gives no answer The check sits in front of every billable request, so its timeout is short: 1.5 seconds by default (`checkTimeoutMs` / `check_timeout_seconds` / `CheckTimeout`). When it expires, or the API is unreachable or failing, your configured policy decides: | Policy | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | `allow` (default) | Serve the request with allowance status `unavailable`. A ZeroClick outage should not take your API down with it. | | `deny` | Refuse with the `503`. When unbilled work costs more than a failed request, choose this policy. | | `throw` | Surface the typed error and decide in application code. | The policy applies only after a signature has verified: a fail-open allowance policy never becomes a fail-open signature policy. The Go SDK further restricts the policy to genuine no-answers: | Condition | Go SDK behavior | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Timeout, DNS failure, connection refused, 5xx, 429, 408 | The policy applies. | | Any other 4xx (revoked or wrong API key, unknown service) | Always an error, never fail-open: it is permanent and would give your product away indefinitely. | | `allowed: false` with a reason the SDK does not recognize | Always a `402`. The SDK honors the denial and passes the unfamiliar reason through for you to log. | | The caller's context was cancelled | Not an outage. The check runs anyway on its own timeout, and its real answer stands. | The TypeScript and Python SDKs route every allowance API error through the policy, including 4xx responses and unrecognized denial reasons. Under the default `allow` policy, monitor `onAllowanceUnavailable` for repeated incidents: a persistent 4xx such as a revoked key means you are serving every request unbilled. Each SDK's errors page documents its exact classification. Set `onAllowanceUnavailable` (and, in Go, `Logger`) to record every incident the policy absorbs. A fail-open that is also silent is invisible unbilled traffic: ```ts TypeScript theme={null} const zeroClick = createSeller({ signingSecrets: { [kid]: secret }, apiKey: process.env.ZEROCLICK_API_KEY!, allowanceUnavailable: "allow", onAllowanceUnavailable: (error) => logger.warn("allowance outage", error.context), }); ``` ```python Python theme={null} zeroclick = create_async_seller( signing_secrets={kid: secret}, api_key=os.environ["ZEROCLICK_API_KEY"], allowance_unavailable_policy="allow", on_allowance_unavailable=lambda error: logger.warning("allowance outage: %s", error), ) ``` ```go Go theme={null} seller, err := sellers.New(sellers.Config{ APIKey: os.Getenv("ZEROCLICK_API_KEY"), ServiceSlug: "product-watch", SigningSecrets: secrets, Policy: sellers.PolicyAllow, Logger: log.Default(), OnAllowanceUnavailable: func(err error) { alert("allowance outage", err) }, }) ``` ## Call the allowance API directly For flows where verification and the check live in different layers, call the check yourself with a `zcRequestId` from an already-verified context. The response is `{ "allowed": boolean, "reason": }`. Building the `402` from a denial is then on you (`paymentRequired` / `payment_required` / `PaymentRequired` construct the exact body). ```ts TypeScript theme={null} const decision = await zeroClick.checkAllowance({ zcRequestId: context.zcRequestId, serviceSlug: "product-watch", usage: [{ meterSlug: "output_tokens", maxQuantity: 100_000 }], }); ``` ```python Python theme={null} decision = await zeroclick.check_allowance( zc_request_id=context.zc_request_id, service_slug="product-watch", usage=[UsageItem(meter_slug="output_tokens", max_quantity=100_000)], ) ``` ```go Go theme={null} decision, err := seller.CheckAllowance(r.Context(), zc.RequestID, "product-watch", []sellers.UsageItem{{MeterSlug: "output_tokens", MaxQuantity: 100_000}}) ``` Unlike `guard`, the direct check surfaces outages as errors instead of applying the policy. It runs under your own cancellation (an optional `{ signal }` in TypeScript, the caller's context in Go). Once a request is allowed, serve it. Then [settle the usage](/integrate/settle-usage). # Free and identity endpoints Source: https://docs.zeroclick.ai/integrate/free-and-identity-endpoints Serve endpoints that cost nothing but must know which buyer is calling, and understand how included free units reach your API. Some endpoints cost nothing but still must know who is asking: polling a job the buyer created, reading account limits, any read or write served only to the buyer that owns the underlying records. Guarding them with `guard` would demand payment for free work; skipping the guard entirely would serve them to anyone. The identity guard is the middle path: it verifies the signature exactly like `guard`, requires a proven buyer, and makes no allowance call. ## Guard identity, not allowance ```ts TypeScript theme={null} export async function GET(request: Request) { const decision = await zeroClick.guardIdentity(request, { serviceSlug: "product-watch", }); if (decision.action === "deny") return decision.response; const jobId = new URL(request.url).pathname.split("/").pop(); const job = await findJob(jobId, { owner: decision.context.zcAgentId }); if (!job) return Response.json({ error: "not_found" }, { status: 404 }); return Response.json(job); } ``` ```python Python theme={null} @app.get("/v1/jobs/{job_id}") async def job_status(request: Request, job_id: str) -> Response: zc_request = zc_request_from_asgi_scope(request.scope, await request.body()) # Synchronous even on the async client: no network call, nothing to await. decision = zeroclick.guard_identity(zc_request, service_slug="product-watch") if decision.action == "deny": return to_fastapi(decision.response) return to_fastapi( ZcResponse.json({"jobId": job_id, "owner": decision.context.zc_agent_id}) ) ``` ```go Go theme={null} mux.Handle("/v1/limits", seller.Identify()(http.HandlerFunc(limits))) func limits(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) w.Header().Set("content-type", "application/json") json.NewEncoder(w).Encode(map[string]any{"zcAgentId": zc.AgentID}) } ``` The decision works like `guard`'s: * A failed signature is the usual `401` deny: unverified traffic never reaches free endpoints either. * A verified request **with** `zc-agent-id` allows, with allowance status `not_required` and the buyer in `context`. Scope your reads and writes to that id. * A verified request **without** a buyer, a [signed anonymous probe](/integrate/verify-requests#signed-anonymous-probes-are-valid), denies with reason `identity_required` and this exact body: ```json theme={null} { "error": "payment_required", "serviceSlug": "product-watch", "usage": [] } ``` The empty usage list is meaningful: it tells ZeroClick the call costs nothing and the missing piece is identity. ZeroClick answers the agent with `401 bearer_required` and a pointer to its registration recipe, and the retry arrives with the buyer's `zc-agent-id` attached. No payment is involved. ## Identity and access are separate A buyer that has registered holds an identity; billing needs an access grant on top of it. A purchase creates access, and allowance checks and usage reports draw against it (see [agents and access](/concepts/agents-and-access)). Reporting usage against an identity-only buyer fails with `access_not_found`. Keep the division clean: `guardIdentity` for free identity-scoped calls, `guard` for anything billable. The same division applies to settlement: no `zc-usage` belongs on a free response. There is nothing to settle. Free means free. ## Included free units There is a second kind of free: a plan's meter price can grant `includedUnits`, free units per period, configured in your catalog (see [plans and pricing](/concepts/plans-and-pricing)). These are billable meters whose first units cost nothing, not identity-scoped endpoints, and they reach your API through the normal guard path: * **The buyer opts in per call** by sending `zc-mode: free` to the pay URL. ZeroClick operates paid-by-default: a missing or unrecognized mode means paid. The proxy consumes the header and never forwards it to you. * **The free allowance needs no funding, but it does need a claimed agent.** Free units are served only to an agent identity that a human has claimed with a verified email. An unclaimed agent, or one whose claim carries no verified email, gets no free units - its free-mode calls are refused like any unpaid call. A claim with a verified email, rather than a payment, is what stands between a new agent and its free units. * **Free calls require an identified caller.** Included-units coverage is per buyer - aggregated across every agent that human has claimed - so ZeroClick answers an unidentified free-mode call with `401 bearer_required` and its registration-and-claim recipe. The identified retry re-runs the real coverage check, including the claimed-and-verified requirement. * **Your integration does not change.** Guard the route and settle usage exactly as for paid traffic. For a covered free-mode call, the allowance check answers `allowed`, and the usage you settle draws down the buyer's included allowance instead of charging. That symmetry is the point of the guard: paid, ceiling, included-free, and identity-only traffic all arrive as signed requests. The same four obligations (verify, check, serve, settle) handle every one. Back to the [integration overview](/integrate/overview) for the full contract, or on to [settle usage](/integrate/settle-usage) if you came here mid-integration. # Keys and secrets Source: https://docs.zeroclick.ai/integrate/keys-and-secrets The credentials a ZeroClick integration runs on: the signing secret and its kid, scoped usage keys, admin keys, rotation, and vault-backed resolution. Your integration runs on three runtime credentials, all minted in the [dashboard](https://dashboard.zeroclick.io) under your store's **Implementation** tab (or **Settings** for individual keys): | Credential | Format | Used for | | --------------- | -------------------------------------------------- | -------------------------------------------------------- | | Signing secret | Value `zcsec_` + 48 chars, key id (`kid`) `hsec_…` | Verifying the `zc-signature` on every forwarded request. | | Usage read key | `zc_` + 48 chars, scope `usage:read` | Allowance checks (`POST /v1/usage/check`). | | Usage write key | `zc_` + 48 chars, scope `usage:write` | Usage reports (`POST /v1/usage`). | A single API key carrying both usage scopes works in place of the two scoped keys. Every SDK accepts either form and falls back to the combined key when a scoped key is unset. ZeroClick shows every secret once, at creation, and cannot show it again. The dashboard displays the first characters of each API key for recognition. Put them in your secret manager immediately: ```sh theme={null} ZEROCLICK_SIGNING_SECRET_KID=hsec_k5nq0v7m3d8p ZEROCLICK_SIGNING_SECRET=zcsec_… ZEROCLICK_API_KEY=zc_… ``` ## The kid must match exactly The dashboard shows the signing secret's key id, the `kid` (format `hsec_…`), next to the secret. The kid arrives in every `zc-signature` header (`t=…,kid=hsec_k5nq0v7m3d8p,v1=…`). The SDKs use the kid to select which secret verifies each request. The key in your secrets map must match the dashboard's key id character for character. A mismatched kid fails every request with `unknown_kid` and the `401` refusal. ## Scopes Send API keys as `Authorization: Bearer zc_…` to `https://api.zeroclick.io`. Each key carries one or more of four scopes: | Scope | Grants | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `usage:read` | Allowance checks: `POST /v1/usage/check`. | | `usage:write` | Usage reports: `POST /v1/usage`. | | `admin:read` | Reading your catalog: sellers, services, meters, plans. | | `admin:write` | Managing the catalog over the [REST API](/api-reference/introduction), including minting signing secrets. That is enough for an agent to automate seller setup end to end. | Manage keys in the dashboard under **Settings → API keys**, in a signed-in session only: API keys cannot create or revoke API keys. Requests with a missing scope get `403 {"error":"insufficient_scope"}`. See [errors](/resources/errors). ## Split read and write keys The process that guards requests only needs to read allowances; the process that reports usage only needs to write. Splitting the keys follows least privilege and matches a common deployment shape. The read key lives in the service that serves requests; the write key lives in the worker that reports usage asynchronously. ```ts TypeScript theme={null} const zeroClick = createSeller({ signingSecrets: { [process.env.ZEROCLICK_SIGNING_SECRET_KID!]: process.env.ZEROCLICK_SIGNING_SECRET!, }, usageReadKey: process.env.ZEROCLICK_USAGE_READ_KEY!, usageWriteKey: process.env.ZEROCLICK_USAGE_WRITE_KEY!, }); ``` ```python Python theme={null} zeroclick = create_async_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, usage_read_key=os.environ["ZEROCLICK_USAGE_READ_KEY"], usage_write_key=os.environ["ZEROCLICK_USAGE_WRITE_KEY"], ) ``` ```go Go theme={null} seller, err := sellers.New(sellers.Config{ UsageReadKey: os.Getenv("ZEROCLICK_USAGE_READ_KEY"), UsageWriteKey: os.Getenv("ZEROCLICK_USAGE_WRITE_KEY"), ServiceSlug: "product-watch", SigningSecrets: secrets, }) ``` The read key covers `guard` and the direct allowance check; the write key covers usage reporting. When you pass both scoped keys, omit the combined `apiKey`. ## Rotate a signing secret ZeroClick signs each forwarded request with your newest active secret and names it by kid, so rotation is a three-step overlap, not a cutover: ZeroClick starts signing new requests with it. The signature's `kid` changes to the new secret's id. Requests signed with either secret verify while both are configured: ```ts TypeScript theme={null} const zeroClick = createSeller({ signingSecrets: { hsec_k5nq0v7m3d8p: process.env.ZEROCLICK_SIGNING_SECRET_PREVIOUS!, hsec_2b9fj6wt5xr0: process.env.ZEROCLICK_SIGNING_SECRET_CURRENT!, }, apiKey: process.env.ZEROCLICK_API_KEY!, }); ``` ```python Python theme={null} zeroclick = create_async_seller( signing_secrets={ "hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET_PREVIOUS"], "hsec_2b9fj6wt5xr0": os.environ["ZEROCLICK_SIGNING_SECRET_CURRENT"], }, api_key=os.environ["ZEROCLICK_API_KEY"], ) ``` ```go Go theme={null} // ZEROCLICK_SIGNING_SECRETS="hsec_k5nq0v7m3d8p:,hsec_2b9fj6wt5xr0:" secrets, err := sellers.SecretsFromEnv() if err != nil { log.Fatal(err) } ``` Once requests signed with the old secret can no longer be in flight, revoke it in the dashboard. Verification rejects signatures older than the 300-second tolerance anyway, so a few minutes of overlap is enough. Then remove the old kid from your configuration. Never log or return a signing secret, and never send it anywhere. It exists only to verify signatures inside your backend. ## Resolve secrets from a vault If your secrets live in a secret manager rather than the environment, resolve them by kid instead of passing a static map: ```ts TypeScript theme={null} const zeroClick = createSeller({ resolveSigningSecret: async ({ kid }) => secretStore.get(kid), apiKey: process.env.ZEROCLICK_API_KEY!, }); ``` ```python Python theme={null} zeroclick = create_async_seller( resolve_signing_secret=lambda kid: secret_store.get(kid), api_key=os.environ["ZEROCLICK_API_KEY"], ) ``` ```go Go theme={null} seller, err := sellers.New(sellers.Config{ APIKey: os.Getenv("ZEROCLICK_API_KEY"), ServiceSlug: "product-watch", Resolve: func(kid string) (string, bool, error) { return secretStore.Get(kid) }, }) ``` The SDK calls the resolver with each request's kid. Signal "unknown kid" by returning `null` (TypeScript), `None` (Python), or `ok=false` (Go). That request gets the `401` refusal. An unreachable vault is a different case: raise or return an error so it surfaces loudly instead of refusing valid traffic. Rotation works the same way as the static map: keep the vault serving both kids until the old one ages out. With keys in place, the first obligation is [verifying requests](/integrate/verify-requests). # Integration overview Source: https://docs.zeroclick.ai/integrate/overview The contract between ZeroClick and your API: what arrives on every forwarded request, your backend's four obligations, and the three refusals it must produce. Agents buy and pay at your pay URL (`https://acme.pay.zeroclick.io`). ZeroClick verifies the payment, signs the request, and forwards it to your API at your configured upstream base URL. This page is the whole contract between ZeroClick and your backend. The [seller SDKs](/sdks/overview) implement it; the [REST walkthrough](/integrate/rest-walkthrough) shows how to implement it without one. ## What arrives on every forwarded request ZeroClick sets three headers on each request it forwards: | Header | Value | Purpose | | --------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `zc-request-id` | `zcreq_8h2m4x0q9k1f` | Correlation id. The same id flows through the probe, the challenge, the paid retry, the allowance check, and the usage record. | | `zc-agent-id` | `agt_x7f2kq93bh0d` | The buyer. Absent or empty on signed anonymous probes, which are valid. See [verify requests](/integrate/verify-requests). | | `zc-signature` | `t=,kid=hsec_…,v1=<64 hex>` | HMAC-SHA256 proof that the request came through ZeroClick unmodified. Sandbox traffic appends `,sb=1` and adds a `zc-sandbox: 1` header. | Before forwarding, ZeroClick strips all inbound `zc-*`, `x-payment`, and `payment*` headers and sets its own, so a caller can never inject them. Other request headers pass through untouched. Your API's own authentication remains whatever you configured between yourself and ZeroClick. One more header matters to the contract without ever reaching you: `zc-mode`. A buyer sends `zc-mode: free` to the pay URL to opt a call into included-units coverage. ZeroClick consumes it there, and a missing or unrecognized value means paid. See [free and identity endpoints](/integrate/free-and-identity-endpoints). The full header reference is at [headers](/resources/headers). ## The four obligations On every forwarded request, your backend must: 1. **Verify** the `zc-signature` against your signing secret, before anything else. Only ZeroClick-signed traffic reaches your handlers. See [verify requests](/integrate/verify-requests). 2. **Check the allowance** with `POST /v1/usage/check`: does the buyer's plan cover what this request will use? See [check allowances](/integrate/check-allowances). 3. **Serve** the request as your API normally would. 4. **Settle** what the request used: a `zc-usage` header on the successful response, or an asynchronous usage report. See [settle usage](/integrate/settle-usage). With an SDK, the first two obligations are one `guard` call and the fourth is one helper: ```ts TypeScript theme={null} const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (decision.action === "deny") return decision.response; const response = Response.json({ watching: true }); return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, ]); ``` ```python Python theme={null} zc_request = zc_request_from_asgi_scope(request.scope, await request.body()) decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) return to_fastapi( zeroclick.with_usage( ZcResponse.json({"watching": True}), [SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1)], ) ) ``` ```go Go theme={null} // Meter verifies the signature, checks the allowance, refuses before the // handler runs, and settles the zc-usage header on a delivered 2xx. mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(productWatch))) ``` ## The three refusals When your API cannot serve, it must answer ZeroClick with one of exactly three responses. The SDKs build all of them. A deny decision carries the response ready to return. | Status | When | Exact body | | ------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `401` | The signature is missing, malformed, stale, or invalid. | `{"error":"invalid_zeroclick_signature"}` | | `402` | The allowance check denies, or an unpaid request needs payment. | `{"error":"payment_required","serviceSlug":"product-watch","usage":[{"meterSlug":"requests","quantity":1}]}` | | `503` | The allowance API is unreachable and your outage policy is fail-closed. | `{"error":"allowance_unavailable"}` | The `402` body is not an error page. It is data. ZeroClick reads the usage list, prices it from your catalog, and issues the buyer a signable payment challenge. The buyer never sees your body. A `402` with `usage: []` means "free, but identity-scoped", which ZeroClick answers with a \$0 identity challenge. The [errors](/resources/errors) page lists error codes across the platform. ## What never to do * **Never serve unverified traffic.** Your upstream URL is reachable; the signature is the gate. A request with no ZeroClick headers must get the `401`, not your handler. * **Never bill on a non-2xx.** `zc-usage` belongs on 2xx responses only: a 4xx is the buyer's bad input and a 5xx is your failure. Neither delivered anything worth charging for. * **Never price or cache challenges yourself.** Your `402` declares usage quantities, not prices. ZeroClick prices each refusal from your catalog per request and binds each challenge to that exact request. A cached price or replayed challenge cannot settle. ## SDK or REST If your backend is TypeScript, Python, or Go, use the SDK. It implements the contract, including the denial bodies and the signature edge cases: * [TypeScript quickstart](/sdks/typescript/quickstart): `@zeroclickai/sellers` on npm * [Python quickstart](/sdks/python/quickstart): `zeroclick-sellers` on PyPI * [Go quickstart](/sdks/go/quickstart): `cdn.zeroclick.io/sdks/sellers-go` Otherwise the contract is small enough to implement over REST: two API calls, one signature check, three fixed response bodies. Start at the [REST walkthrough](/integrate/rest-walkthrough) and the byte-level [signature spec](/integrate/signature-spec). ## Work through the contract The three runtime keys, scopes, split read/write keys, and rotation. What verification proves, anonymous probes, and the raw-path rule. Guard semantics, usage items, denials, and the outage policy. The `zc-usage` header and asynchronous usage reports. Use ceilings to bill work you can't size up front. Endpoints that cost nothing but must know the buyer. # REST walkthrough Source: https://docs.zeroclick.ai/integrate/rest-walkthrough Implement the ZeroClick seller contract with plain HTTP in any language: verify the signature, check the allowance, serve, and settle usage. The [seller SDKs](/sdks/overview) implement the billing guard for TypeScript, Python, and Go. If your backend runs on anything else, or you want zero dependencies, the contract is small enough to implement directly: verify one request header, call one HTTPS endpoint before the work, and settle usage after it. This page walks the whole contract with your language's standard library and `curl`. You need your seller's signing secret (`zcsec_…`) with its key id (`hsec_…`), a usage read key (`zc_…`, scope `usage:read`), and a usage write key (`zc_…`, scope `usage:write`). One API key with both usage scopes also works. See [keys and secrets](/integrate/keys-and-secrets). The examples use the seller Acme with service `product-watch` and meters `requests` and `output_tokens`. Every request ZeroClick forwards to your API carries three headers: | Header | Example | Meaning | | --------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `zc-request-id` | `zcreq_8h2m4x0q9k1f` | Correlation id for this request. Echo it in the allowance check. | | `zc-agent-id` | `agt_x7f2kq93bh0d` | The buying agent. Absent on signed anonymous probes, which ZeroClick uses to price pay-as-you-go challenges. This state is expected and valid. | | `zc-signature` | `t=1785258000,kid=hsec_k5nq0v7m3d8p,v1=…` | Proves the request came from ZeroClick and was not altered. | ZeroClick strips any `zc-*` headers the agent sent before forwarding and sets its own. Once the signature verifies, these values are ZeroClick's. Capture the raw body bytes and the raw request target (path plus query, still percent-encoded) before your framework decodes them; both feed verification. The [headers reference](/resources/headers) lists every header on the wire. `zc-signature` is an HMAC-SHA256 over six newline-joined fields, keyed by the signing secret whose id matches the header's `kid`. The fields are the timestamp, the uppercased method, the raw path and query, the SHA-256 of the body bytes, the request id, and the agent id (or empty string). Verify the timestamp is within 300 seconds of your clock. Compare the digest in constant time. The [zc-signature specification](/integrate/signature-spec) defines every rule byte by byte, with test vectors. A compact verifier in standard-library Python: ```python theme={null} import hashlib, hmac, os, re, time SIGNING_SECRETS = {"hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET"]} TOLERANCE_SECONDS = 300 def verify(method, raw_path_and_query, raw_body, headers): """Returns the verified context, or None -> respond 401.""" header = headers.get("zc-signature") if header is None: return None members = {} for entry in header.split(","): key, sep, value = entry.partition("=") key = key.strip() if not sep or not key or key in members: # malformed or duplicate member return None members[key] = value.strip() # unknown members are ignored t, kid, v1 = members.get("t"), members.get("kid"), members.get("v1") if not (t and kid and v1): return None if not re.fullmatch(r"\d{1,20}", t) or not re.fullmatch(r"[0-9a-f]{64}", v1): return None if abs(int(time.time()) - int(t)) > TOLERANCE_SECONDS: return None request_id = headers.get("zc-request-id") if not request_id: return None agent_id = headers.get("zc-agent-id") or "" secret = SIGNING_SECRETS.get(kid) if secret is None: return None canonical = "\n".join([ t, method.upper(), raw_path_and_query, # as sent, never URL-decoded hashlib.sha256(raw_body).hexdigest(), request_id, agent_id, ]) expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, v1): return None return {"zc_request_id": request_id, "zc_agent_id": agent_id or None} ``` Sign over the request target exactly as it arrived. Most frameworks hand you a decoded path (`%2F` becomes `/`), and the decoded form hashes to a different string. Read the raw request line. Hash the exact body bytes, not re-serialized JSON. Before you do the work, ask ZeroClick whether this agent's plan covers it. The check is bound to that request, so send the `zc-request-id` you received: ```sh theme={null} curl -sS https://api.zeroclick.io/v1/usage/check \ -H "Authorization: Bearer $ZEROCLICK_USAGE_READ_KEY" \ -H "Content-Type: application/json" \ -d '{ "zcRequestId": "zcreq_8h2m4x0q9k1f", "serviceSlug": "product-watch", "usage": [{ "meterSlug": "requests", "quantity": 1 }] }' ``` `usage` takes 1 to 20 items, one per meter, no duplicates. Each item declares `quantity` when the amount is known, or `maxQuantity` as a ceiling when it is not (see [charge up to a maximum](/integrate/charge-up-to-a-maximum)). An item that declares neither gates against the meter's configured default ceiling. An item may not declare both. The response is one of two shapes: ```json theme={null} { "allowed": true, "reason": null } ``` ```json theme={null} { "allowed": false, "reason": "usage_exhausted" } ``` `reason` is one of `service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, or `usage_exhausted`. See [check allowances](/integrate/check-allowances). Checking records nothing and burns no credit, so repeating a check is harmless. Two failure modes need different handling. No answer at all (a timeout, a connection failure, a 5xx) means you choose a policy: fail open and serve, or fail closed with the `503` below. The SDKs default to fail open, with a 1.5 second timeout. A 4xx from the API (a revoked key, a key missing `usage:read`) is a configuration error on your side. Fix it. Never fail open on it. Your API returns three refusals, each with an exact body ZeroClick recognizes. A missing, malformed, stale, or invalid signature gets a `401`. The body is the same for every signature failure. Keep the specific reason in your logs: ```json theme={null} { "error": "invalid_zeroclick_signature" } ``` `"allowed": false` gets a `402` naming what the request would cost. ZeroClick prices it and answers the agent with a payment challenge, so return the refusal before you do the work: ```json theme={null} { "error": "payment_required", "serviceSlug": "product-watch", "usage": [{ "meterSlug": "requests", "quantity": 1 }] } ``` An empty `"usage": []` marks a free, buyer-scoped endpoint that needs to know its caller. See [free and identity endpoints](/integrate/free-and-identity-endpoints). An unreachable allowance API under a fail-closed policy gets a `503`: ```json theme={null} { "error": "allowance_unavailable" } ``` On success, do the work. Settle usage synchronously by setting the `zc-usage` response header: a JSON array of what the response actually consumed. ```text theme={null} zc-usage: [{"serviceSlug":"product-watch","meterSlug":"requests","quantity":1}] ``` Set it on 2xx responses only. A refusal must never carry usage. ZeroClick reads the header, records the usage, settles payment, and strips the header before the agent sees the response. This is the preferred settlement path; see [settle usage](/integrate/settle-usage). When you know the amount only after the response has gone out (a background job, a stream's final token count), report it to the usage API with the write key: ```sh theme={null} curl -sS https://api.zeroclick.io/v1/usage \ -H "Authorization: Bearer $ZEROCLICK_USAGE_WRITE_KEY" \ -H "Content-Type: application/json" \ -d '{ "zcAgentId": "agt_x7f2kq93bh0d", "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens", "serviceSlug": "product-watch", "meterSlug": "output_tokens", "quantity": 1834 }' ``` `quantity` is an integer from 1 to 2,147,483,647. `occurredAt` (ISO 8601) is optional; when omitted, the server sets it to now. ```json theme={null} { "recorded": true, "duplicate": false, "usageEvent": { "id": "use_2q9d5k7x0mfb", "zcAgentId": "agt_x7f2kq93bh0d", "zcRequestId": null, "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens", "meterSlug": "output_tokens", "quantity": 1834, "totalCostUsd": "0.001834", "source": "async", "occurredAt": "2026-07-28T17:03:41.512Z" } } ``` Reports are idempotent per service on `idempotencyKey`. Derive the key from what it bills (`_`, as above). Never generate it randomly: a random key turns a retried report into a second charge, while a derived key makes the retry a replay. A replay returns `"duplicate": true` with the stored event; that is a success, not an error. Denials use the [error envelope](/resources/errors): `402` for `access_inactive`, `plan_expired`, and `usage_exhausted`; `409` for `meter_not_priced`; `404` for `service_not_found`, `access_not_found`, and `meter_not_found`. ## Ordering rules Three rules hold the contract together: 1. **Verify before everything.** Nothing runs on an unverified request: not the allowance check, not your handler. The signature is the only thing that makes `zc-request-id` and `zc-agent-id` trustworthy. 2. **Never bill a non-2xx.** `zc-usage` belongs on successful responses only. The `402` refusal comes before the work, not after it. 3. **Echo the received `zc-request-id` into the check.** The id ties the challenge, the payment, and the usage record to one request; an invented id fails the check. The byte-level spec: grammar, canonical string, and test vectors. Every error code the platform returns, with statuses. # Settle usage Source: https://docs.zeroclick.ai/integrate/settle-usage Tell ZeroClick what a served request actually used: the zc-usage header for synchronous settlement, and asynchronous reports for work that finishes later. An allowed request still has to settle: the payment finalizes against what the request actually used. Your revenue, credit burn, and analytics are computed from usage. The primary path is synchronous: stamp a `zc-usage` header on the successful response. ZeroClick records the usage as it returns the response to the agent. Reserve the [asynchronous report](#report-usage) for work that finishes after the response is gone. ## Settle on the response `zc-usage` is a response header carrying a JSON array of what the request used: ```json theme={null} [{ "serviceSlug": "product-watch", "meterSlug": "requests", "quantity": 1 }] ``` The SDK helpers build and validate it: ```ts TypeScript theme={null} const response = Response.json({ watching: true }); return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, ]); ``` ```python Python theme={null} return to_fastapi( zeroclick.with_usage( ZcResponse.json({"watching": True}), [SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1)], ) ) ``` ```go Go theme={null} // The Meter middleware attaches zc-usage for the quantities it declared, // and only on a 2xx; nothing to write in the handler. mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(productWatch))) ``` Three rules govern the header: * **2xx only.** A 4xx is the buyer's bad input and a 5xx is your failure; neither delivered anything, so neither bills. The Go middleware enforces this. In TypeScript and Python, call `withUsage` only on the success path. * **ZeroClick strips it.** The agent never sees `zc-usage`: the proxy consumes it and removes it from the response. * **Settle the actual quantity.** For a fixed `quantity`, that is what you declared. For a `maxQuantity` ceiling, it is anywhere from zero up to the ceiling. See [charge up to a maximum](/integrate/charge-up-to-a-maximum). The platform accepts `0` (a delivered response that produced nothing settles the ceiling at \$0). The TypeScript and Python helpers validate quantities as at least 1, so settle a zero-output ceiling by omitting that item. A paid, delivered response always settles, even if your header is missing or malformed: fixed items settle at the authorized quantity, ceilings settle at zero. That guarantee protects the buyer's payment from a garbage header. It does not excuse the report: an unreported ceiling settles as if nothing was produced. ## Report usage Some work outlives the response: a job that keeps extracting after you return `202`, tokens streamed over a connection that has already closed, a worker that settles from a queue. Report that usage asynchronously with `POST /v1/usage`, through `reportUsage`, `report_usage`, or `ReportUsage`, using the `usage:write` key ([split keys](/integrate/keys-and-secrets#split-read-and-write-keys) put it in the worker): ```ts TypeScript theme={null} const result = await zeroClick.reportUsage({ zcAgentId: "agt_x7f2kq93bh0d", idempotencyKey: "zcreq_8h2m4x0q9k1f_output_tokens", // derived, never random serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4200, }); console.log(result.recorded, result.duplicate); ``` ```python Python theme={null} result = await zeroclick.report_usage( zc_agent_id="agt_x7f2kq93bh0d", idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens", # derived, never random service_slug="product-watch", meter_slug="output_tokens", quantity=4200, ) ``` ```go Go theme={null} _, err := seller.ReportUsage(sellers.BackgroundContext(r), sellers.ReportUsageInput{ AgentID: zc.AgentID, ServiceSlug: "product-watch", MeterSlug: "output_tokens", Quantity: 4200, IdempotencyKey: zc.RequestID + "_output_tokens", // derived, never random }) if err != nil { // Delivered but unbilled. Log it; do not fail a request the buyer already has. log.Printf("usage not reported: %v", err) } ``` In Go, report with `sellers.BackgroundContext(r)`, not `r.Context()`. Go cancels the request context the moment your handler returns, so a report on it is silently dropped. The work goes unbilled, with nothing in the logs. **Derive the idempotency key.** Reports are idempotent per service on `idempotencyKey`, and the key is yours to construct: the SDKs never generate one. Derive it from stable facts, such as the request id plus the meter (`zcreq_8h2m4x0q9k1f_output_tokens`). A random key turns every retry into a double bill. `duplicate: true` means the key already landed and the stored event was replayed: a success, not an error. There are no automatic retries. Retry yourself with the same key. On the wire, the call and its response: ```http Request theme={null} POST /v1/usage HTTP/1.1 Host: api.zeroclick.io Authorization: Bearer zc_… Content-Type: application/json { "zcAgentId": "agt_x7f2kq93bh0d", "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens", "serviceSlug": "product-watch", "meterSlug": "output_tokens", "quantity": 4200, "occurredAt": "2026-07-28T17:04:05.000Z" } ``` ```json Response theme={null} { "recorded": true, "duplicate": false, "usageEvent": { "id": "use_2j8fq0w7xk4m", "zcAgentId": "agt_x7f2kq93bh0d", "zcRequestId": null, "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens", "meterSlug": "output_tokens", "quantity": 4200, "totalCostUsd": "0.042000", "source": "async", "occurredAt": "2026-07-28T17:04:05.000Z" } } ``` `quantity` is an integer of at least 1 (maximum 2,147,483,647); `occurredAt` is an optional ISO timestamp for when the usage happened. A report can fail with `402` (`access_inactive`, `plan_expired`, `usage_exhausted`), `409` (`meter_not_priced`), or otherwise `404` for a missing resource. The SDKs surface these as typed errors carrying the status and reason. See [errors](/resources/errors). A failed report after delivery means work went unbilled. Log it with the reason rather than failing a request the buyer already received. ## Choose a path * **Settle on the response** when you know the quantity before you respond: most requests, including ceilings whose actual you know by the end of the handler. * **Report asynchronously** when the work finishes after the response, when settlement happens in a separate worker, or when there is no HTTP response to stamp (a connection upgraded away from HTTP, a queue consumer). * **Combine them** for work you can't size up front: a fixed part settled on the response, the variable part reported when you know it. That is the pattern in [charge up to a maximum](/integrate/charge-up-to-a-maximum). # zc-signature specification Source: https://docs.zeroclick.ai/integrate/signature-spec The byte-level specification of the zc-signature request header: grammar, canonical string, verification algorithm, failure taxonomy, and a worked test vector. Every request ZeroClick forwards to a seller's API carries a `zc-signature` header: an HMAC-SHA256 over the request, keyed by the seller's signing secret. This page is the normative specification for implementers writing their own verifier. If you use a [seller SDK](/sdks/overview), it already implements every rule here. See [verify requests](/integrate/verify-requests). For the surrounding flow, see the [REST walkthrough](/integrate/rest-walkthrough). ## Header grammar The value is a comma-separated list of `key=value` members: ```text theme={null} zc-signature: t=,kid=,v1=<64 lowercase hex> ``` Sandbox traffic appends `sb=1`: ```text theme={null} zc-signature: t=1760000000,kid=hsec_k5nq0v7m3d8p,v1=25e2005f3229a60c8accc0d92576fc89f590533de7cd0ba880e820777ee6faa7,sb=1 ``` | Member | Value | Validation | | ------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `t` | Signing timestamp in unix seconds. | 1 to 20 ASCII digits. | | `kid` | Id of the signing secret that keyed the HMAC (`hsec_…`). Selects the right secret during [rotation](/integrate/keys-and-secrets). | Non-empty, no surrounding whitespace. | | `v1` | The hex-encoded HMAC-SHA256 signature. | Exactly 64 lowercase hex characters. | | `sb` | `1` on sandbox traffic. | Informational; the canonical string is byte-identical either way. | Parsing rules: * Split the value on `,`. Each member is `key=value`. Trim whitespace around a key or value. * Reject a member with no `=` or an empty key as malformed. * **Reject duplicate member keys as malformed.** Do not skip this rule. * **Ignore unrecognized member keys.** ZeroClick extends this header additively (`sb=1` is one such member), so a parser that demands exactly three members fails all sandbox traffic. * After parsing, `t`, `kid`, and `v1` must all be present and pass their validations; anything else is malformed. ## Canonical string The signed bytes are six fields joined by `\n` (LF, `0x0A`), with no trailing newline: ```text theme={null} \n\n\n\n\n ``` 1. **Timestamp**: the header's `t` value byte-for-byte as received. Never re-parse and re-format it. 2. **Method**: the HTTP method, uppercased. 3. **Request target**: the raw percent-encoded path plus query exactly as sent, with no scheme or host. `/v1/items/a%2Fb%20c` stays `/v1/items/a%2Fb%20c`. Never the decoded path: frameworks commonly hand you one, and it hashes to a different string. Preserve query order and repeated keys verbatim. 4. **Body digest**: lowercase hex SHA-256 of the exact body bytes as received. An empty body hashes to `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. Re-serialized JSON will not match: the digest covers bytes, not meaning. Hash an encrypted body as the ciphertext it arrived as. Verify before you decrypt. 5. **Request id**: the `zc-request-id` header value. 6. **Agent id**: the `zc-agent-id` header value, or the empty string when the header is absent. An absent header and a present-but-empty one sign identically; both mean a signed anonymous probe, which is valid. The signature is `HMAC-SHA256(secret, canonical string)`, lowercase hex, with the secret and canonical string both UTF-8 encoded. The secret is the one whose id equals `kid`. ## Verification algorithm 1. Read `zc-signature`. Absent → `missing_signature`. 2. Parse it per the grammar above. Any violation → `malformed_signature`. 3. Parse `t` as an integer. If `|now − t|` exceeds the tolerance (300 seconds by default, applied in both directions) → `stale_timestamp`. 4. Read `zc-request-id`. Absent or empty → `missing_request_id`. 5. Read `zc-agent-id`; treat absent as the empty string. 6. Resolve the secret for `kid`. No secret with that id → `unknown_kid`. Run steps 1 to 5 first so an unauthenticated request never reaches your key store. A lookup that *fails* (a vault being unreachable) is an operational error, not a refusal. 7. Compute the expected HMAC over the canonical string. Compare it to `v1` **in constant time** (`hmac.compare_digest`, `crypto.timingSafeEqual`, `hmac.Equal`). Mismatch → `invalid_signature`. 8. Otherwise the request is verified: trust `zc-request-id` and `zc-agent-id`. ## Failure taxonomy | Reason | Trigger | | --------------------- | -------------------------------------------------------------------------------------------------------------- | | `missing_signature` | No `zc-signature` header. | | `malformed_signature` | Grammar violation: missing or duplicate member, non-numeric or over-long timestamp, `v1` not 64 lowercase hex. | | `stale_timestamp` | `t` outside the clock tolerance, past or future. | | `missing_request_id` | No `zc-request-id` header. | | `unknown_kid` | No configured signing secret with the header's `kid`. | | `invalid_signature` | The HMAC comparison failed: wrong secret, tampered body, altered target or headers. | Every reason maps to the same seller response, one `401`: ```json theme={null} { "error": "invalid_zeroclick_signature" } ``` The body never says which check failed. Keep the reason in your own logs. ## Worked test vector This vector comes from the cross-SDK vector suite (the `identified_buyer` case), which every seller SDK executes. The full suite ships with the SDKs as `signing-vectors.json`. The ids and secret below are test fixtures. Production values look like `hsec_…`, `zcreq_…`, and `agt_…`. Signing secret: ```text theme={null} kid: zcsec_vector_1 secret: zcsec_vector_secret_do_not_use_in_production ``` Request: ```text theme={null} POST /v1/product-watch zc-request-id: zcreq_vector zc-agent-id: zcagent_vector zc-signature: t=1760000000,kid=zcsec_vector_1,v1=25e2005f3229a60c8accc0d92576fc89f590533de7cd0ba880e820777ee6faa7 ``` Body (exactly these 36 bytes, no trailing newline): ```json theme={null} {"productId":"sku_42","window":"7d"} ``` Its SHA-256 is `b07172b06310bf8dc76e9427aa33af6c61894b2a2abc1524a980c2b6bb7bd35e`, giving the canonical string: ```text theme={null} 1760000000 POST /v1/product-watch b07172b06310bf8dc76e9427aa33af6c61894b2a2abc1524a980c2b6bb7bd35e zcreq_vector zcagent_vector ``` 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 with request id `zcreq_vector` and agent id `zcagent_vector`. Your implementation must also pass two checks. With `zc-agent-id` absent, the same request verifies only against `v1=326b303be6b1ca89598b3b572961bce3b999c00cbf8333a71cc1c7f9f3d6566c` (the empty-string sixth field). Your verifier must reject a header that carries `v1` twice as malformed, even when both copies are correct. # Sell accounts and API keys Source: https://docs.zeroclick.ai/integrate/stateful-sellers Turn a ZeroClick purchase into a real account on your side, with a subscription, a credit balance, and an API key the buyer then uses with you directly. 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. 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`. 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. 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](mailto: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](/concepts/plans-and-pricing#the-four-billing-modes)). 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](/integrate/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](/resources/stateful-access-reference#configuring-the-endpoint). * **Durable storage** for accounts, the last version you applied, the credit you have granted in total, and your API key hashes. ```sh theme={null} pnpm add @zeroclickai/sellers ``` The stateful helpers ship in the TypeScript SDK today. On any other stack, implement the same two routes by hand — the [reference](/resources/stateful-access-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: ```ts theme={null} { accountKey: "agent", remintPolicy: "rotating" } ``` **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: ```ts theme={null} { accountKey: "accessId", remintPolicy: "additive" } ``` 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](#when-a-customer-runs-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](/integrate/verify-requests#verify-the-raw-body-bytes). ```ts theme={null} import { handleAccessRequest } from "@zeroclickai/sellers/stateful"; const options = { basePath: "/zeroclick/access", accountKey: "agent" as const, remintPolicy: "rotating" as const, secrets: { [process.env.ZEROCLICK_SIGNING_SECRET_KID!]: process.env.ZEROCLICK_SIGNING_SECRET!, }, onHandlerError: (error: unknown, operation: "write" | "mint") => { logger.error({ error, operation }, "ZeroClick access handler failed"); }, }; export async function handle(request: Request): Promise { const rawBody = new Uint8Array(await request.clone().arrayBuffer()); const url = new URL(request.url); const result = await handleAccessRequest( { method: request.method, pathAndQuery: `${url.pathname}${url.search}`, rawBody, headers: Object.fromEntries(request.headers), }, { onWrite, onMint }, options, ); // Not one of ZeroClick's two routes: let your own router handle it. if (result === null) return new Response("Not found", { status: 404 }); return new Response( result.body === undefined ? null : JSON.stringify(result.body), { status: result.status, headers: { ...(result.body === undefined ? {} : { "content-type": "application/json" }), ...result.headers, }, }, ); } ``` 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](/resources/stateful-access-reference#why-the-write-is-a-complete-picture) you are in. ```ts theme={null} import { deriveCreditDelta, shouldApply } from "@zeroclickai/sellers/stateful"; import type { WriteInput } from "@zeroclickai/sellers/stateful"; async function onWrite({ accountId, entitlement }: WriteInput) { await database.transaction(async (tx) => { const account = await tx.lockAccount(accountId); if (!shouldApply(account?.stateVersion, entitlement.stateVersion)) return; const delta = deriveCreditDelta( account ? { stateVersion: account.stateVersion, creditGrantedUsdMicros: account.creditGrantedUsdMicros, } : null, entitlement, ); if (delta.outcome === "credit") { await tx.addCredit(accountId, delta.deltaUsdMicros); await tx.storeGrant(accountId, delta.next); } else if (delta.outcome === "no_credit_dimension") { await tx.storeGrant(accountId, delta.next); } await tx.applyPlanAndPeriod(accountId, entitlement); }); return { lifecycle: "active" as const }; } ``` 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](/resources/stateful-access-reference#the-account-write). ## Mint the key ```ts theme={null} import type { MintInput } from "@zeroclickai/sellers/stateful"; async function onMint({ accountId }: MintInput) { const plaintext = generateApiKey(); const digest = hashApiKey(plaintext); const created = await database.transaction(async (tx) => { const account = await tx.lockActiveAccount(accountId); if (!account) return false; // Required when remintPolicy is "rotating"; skip it for additive keys. await tx.revokeLiveKeys(account.id); await tx.insertKeyHash(account.id, digest); return true; }); return created ? { apiKey: plaintext } : { unknown: true as const }; } ``` * **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](/resources/stateful-access-reference#what-your-handlers-can-return); the [inputs your handlers receive](/resources/stateful-access-reference#what-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: ``` https://acme.pay.zeroclick.io/extend?accessId=zacc_8h2m4x0q9k1f ``` 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](/resources/stateful-access-reference#buyer-facing-routes-on-your-pay-url). 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. ## 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](/resources/stateful-access-reference#retries-timeouts-and-failures). ## 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](/concepts/agents-and-access#agents-belong-to-buyers-zcbuyerid)). 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 Every field, status code, retry rule, and the signature for non-TypeScript stacks. Create, scope, and rotate the signing secret this endpoint verifies against. What an agent id is, and how one buyer can hold several. The stateless side: verify, check, serve, settle. # Verify requests Source: https://docs.zeroclick.ai/integrate/verify-requests Prove each forwarded request came from ZeroClick: what the SDKs check, why anonymous probes are valid, and the raw-path rule that trips up reverse proxies. Your upstream URL is reachable from the internet; the `zc-signature` header is the gate. Verification proves a request came through ZeroClick (payment handled, buyer resolved) and that nothing changed in transit. Every SDK guard verifies before it does anything else. A request that fails must get `401 {"error":"invalid_zeroclick_signature"}`, never your handler. ## What the SDKs check Verification runs entirely inside your process, with no network call: 1. Parse `zc-signature` (`t=,kid=,v1=<64 lowercase hex>`). Ignore unknown fields: ZeroClick appends fields additively, such as `,sb=1` on sandbox traffic. 2. Reject timestamps outside the clock tolerance: 300 seconds by default, in either direction. 3. Look up the signing secret by the signature's `kid`. An unknown kid is a refusal, so [rotation](/integrate/keys-and-secrets#rotate-a-signing-secret) means holding both kids. 4. Recompute the HMAC-SHA256 over the canonical string and compare in constant time. The canonical string is the timestamp, the uppercased method, the raw path and query, the SHA-256 of the raw body bytes, `zc-request-id`, and `zc-agent-id` (empty string when absent). The [signature spec](/integrate/signature-spec) specifies the exact bytes. Failures are decisions, not exceptions: the guard returns a deny carrying the `401`, with a machine-readable reason (`missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, or `invalid_signature`) for your logs. The `401` body itself says nothing about why the request failed. ## Signed anonymous probes are valid A verified request with no `zc-agent-id` (or an empty one) is a **signed anonymous probe**, and it is how pay-as-you-go pricing works. For an unpaid call, ZeroClick forwards a probe to your API. Your guard denies it with the `402 payment_required` usage body. ZeroClick re-prices that refusal into the one priced challenge the agent pays. Probes are expected traffic, not an attack, and only the paid retry carries the agent id. ```ts TypeScript theme={null} if (!decision.context.zcAgentId) { // Anonymous probe: zcAgentId is null. } ``` ```python Python theme={null} # Test with truthiness, not `is None`: an absent zc-agent-id header gives # None, but a present-but-empty one gives ""; both mean anonymous. if not decision.context.zc_agent_id: ... ``` ```go Go theme={null} if zc.AgentID == "" { // Anonymous probe. Absent and empty headers sign identically. } ``` ## The signature covers the raw path The canonical string contains the **raw, percent-encoded** path and query exactly as ZeroClick sent it. Every web framework hands you a decoded path, and a decoded path hashes differently. For `GET /v1/items/a%2Fb%20c`: | Source | Value | Verifies | | ----------------------------------------------------------- | --------------------- | -------- | | ASGI `scope["path"]`, WSGI `PATH_INFO`, Go `r.URL.Path` | `/v1/items/a/b c` | No | | ASGI `scope["raw_path"]`, WSGI `RAW_URI`, Go `r.RequestURI` | `/v1/items/a%2Fb%20c` | Yes | The SDKs handle this at the boundary. The TypeScript SDK reads the web-standard `Request` URL, which preserves encoding. The Python adapters (`zc_request_from_asgi_scope`, `zc_request_from_wsgi_environ`) read the raw target and handle the difference: ASGI's `raw_path` excludes the query string, while WSGI's `RAW_URI` includes it. Go's `FromHTTP` reads `r.RequestURI`, which Go leaves untouched. A reverse proxy, ingress controller, or managed load balancer in front of your server may normalize the path (`%2F` becoming `/`) before your server sees it. If that happens, every request with an encoded path segment fails verification. If you route through nginx, an ingress, or a load balancer, confirm it passes the request target through unmodified. On WSGI, if the server sets neither `RAW_URI` nor `REQUEST_URI`, an encoded separator cannot be recovered: the server decodes `%2F` before the SDK runs. gunicorn, werkzeug, uWSGI, and nginx all set one of them. ## Verify the raw body bytes The body digest covers the bytes exactly as they arrived, before any parsing or transformation. Adapt framework requests without re-encoding the body. If your service opts into encrypted bodies, verify first: the signature covers the ciphertext, so decryption comes after the guard. In Go, the `Meter` and `Identify` middleware read the body for verification and then restore `r.Body`, so your handler reads it normally. ## Lower-level verification `guard` is the right entry point for a billable route. When verification and allowance checking live in separate middleware layers (verify once at the edge, check allowances per route), use the verify-only entry point. In TypeScript, it reads a clone of the `Request`, so the body stays available to your handler. In Python and Go, you hand it the raw bytes you already hold. ```ts TypeScript theme={null} const result = await zeroClick.verifyRequest(request); if (!result.ok) return result.response; // the 401; result.reason for logs // result.context: { zcRequestId, zcAgentId, timestamp, kid } console.log(result.context.zcRequestId); ``` ```python Python theme={null} result = zeroclick.verify_request(zc_request) if not result.ok: return to_fastapi(result.response) # the 401; result.reason for logs context = result.context # zc_request_id, zc_agent_id, timestamp, kid ``` ```go Go theme={null} result, err := seller.Verify(sellers.FromHTTP(r, body)) if err != nil { // Secret resolution failed: a vault outage, not a bad request. http.Error(w, `{"error":"internal_error"}`, http.StatusInternalServerError) return } if !result.OK { result.Response.WriteTo(w) // the 401; result.Reason for logs return } // result.Context: RequestID, AgentID, Timestamp, KID ``` The verified context carries `zcRequestId` and `zcAgentId`: everything the [allowance check](/integrate/check-allowances) and [usage settlement](/integrate/settle-usage) need. ## Who called, and who they belong to `zcAgentId` is always the agent that made this call. When ZeroClick knows the owner behind that agent, the request also carries `zc-buyer-id` (`byr_…`), exposed by the TypeScript and Python SDKs as `zcBuyerId` / `zc_buyer_id`. One buyer can hold several agents, and they all share the buyer's entitlements, so the buyer id is what stays stable when a customer rotates or replaces an agent. Key durable per-customer records on the buyer id when it is present and per-caller state on the agent id. An absent buyer id means an anonymous agent: still identified and billable, just not yet attached to an owner. Note that the signature covers `zc-agent-id` and not `zc-buyer-id`; the [headers reference](/resources/headers) has the full matrix. # Quickstart Source: https://docs.zeroclick.ai/quickstart Add the ZeroClick billing guard to an existing API and serve your first paid agent request. This guide adds the ZeroClick billing guard to one route of an existing API. When you finish, an unpaid agent request to that route gets a priced `402` challenge, a paid request gets served, and the usage and transaction appear in your dashboard. You need a store that is set up in the [dashboard](https://dashboard.zeroclick.io). ZeroClick configures your store, services, and plans with you during onboarding. You also need an API that ZeroClick can reach at your store's upstream base URL. This guide uses a service `product-watch` with a meter `requests`; substitute your own slugs. Your integration runs on three keys, minted in the dashboard under your store's **Implementation** tab (or **Settings** for individual keys): * A **signing secret** (`zcsec_…`) and its key id (`hsec_…`), used to verify that requests really came from ZeroClick. The key id, or `kid`, appears alongside the secret in the dashboard. * A **usage read key** (`zc_…`, scope `usage:read`) for allowance checks. * A **usage write key** (`zc_…`, scope `usage:write`) for usage reporting. A single API key carrying both usage scopes works in place of the two scoped keys. Each secret is shown once when it is created, so store it in your secret manager right away: ```sh theme={null} ZEROCLICK_SIGNING_SECRET_KID=hsec_k5nq0v7m3d8p ZEROCLICK_SIGNING_SECRET=zcsec_… ZEROCLICK_API_KEY=zc_… ``` See [keys and secrets](/integrate/keys-and-secrets) for scopes, rotation, and split read/write keys. ```sh TypeScript theme={null} pnpm add @zeroclickai/sellers ``` ```sh Python theme={null} pip install zeroclick-sellers ``` ```sh Go theme={null} go get cdn.zeroclick.io/sdks/sellers-go ``` The TypeScript SDK is ESM and runs anywhere the web-standard `Request`, `Response`, and `fetch` exist, including current Node.js and edge runtimes. The Python SDK supports Python 3.10 and later, with sync and async clients. The Go SDK requires Go 1.24 and imports only the standard library. Configure the client once, then guard the route: verify the signature, check the allowance, do the work, and settle usage on the successful response. ```ts TypeScript theme={null} import { createSeller } from "@zeroclickai/sellers"; const zeroClick = createSeller({ signingSecrets: { [process.env.ZEROCLICK_SIGNING_SECRET_KID!]: process.env.ZEROCLICK_SIGNING_SECRET!, }, apiKey: process.env.ZEROCLICK_API_KEY!, }); export async function handle(request: Request): Promise { const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (decision.action === "deny") return decision.response; const response = Response.json({ message: "Hello from the seller", zcRequestId: decision.context.zcRequestId, }); return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, ]); } ``` ```python Python theme={null} import os from fastapi import FastAPI, Request from fastapi.responses import Response from zeroclick_sellers import ( SyncUsageItem, UsageItem, ZcResponse, create_async_seller, ) from zeroclick_sellers.adapters import zc_request_from_asgi_scope zeroclick = create_async_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) app = FastAPI() def to_fastapi(response: ZcResponse) -> Response: return Response( content=response.body, status_code=response.status, headers=dict(response.headers), ) @app.post("/v1/product-watch") async def product_watch(request: Request) -> Response: zc_request = zc_request_from_asgi_scope(request.scope, await request.body()) decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) result = do_the_work(owner=decision.context.zc_agent_id) return to_fastapi( zeroclick.with_usage( ZcResponse.json(result), [ SyncUsageItem( service_slug="product-watch", meter_slug="requests", quantity=1, ) ], ) ) ``` ```go Go theme={null} package main import ( "encoding/json" "log" "net/http" "os" sellers "cdn.zeroclick.io/sdks/sellers-go" ) func main() { secrets, err := sellers.SecretsFromEnv() // ZEROCLICK_SIGNING_SECRETS if err != nil { log.Fatal(err) } seller, err := sellers.New(sellers.Config{ APIKey: os.Getenv("ZEROCLICK_API_KEY"), ServiceSlug: "product-watch", SigningSecrets: secrets, Logger: log.Default(), }) if err != nil { log.Fatal(err) } mux := http.NewServeMux() mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(productWatch))) log.Fatal(http.ListenAndServe(":8080", mux)) } func productWatch(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) // zc.AgentID, zc.RequestID w.Header().Set("content-type", "application/json") json.NewEncoder(w).Encode(map[string]any{"ok": true, "agent": zc.AgentID}) } ``` The guard verifies the signature before it calls the allowance API. Its result is a decision, not an exception. A denial carries the exact response to return: `401` for a bad signature, the `402 payment_required` refusal for a business denial, or `503` under a fail-closed outage policy. In Go, the `Meter` middleware returns the denial and sets the `zc-usage` header for you. In TypeScript and Python, you return `decision.response` and settle usage with `withUsage`. Deploy the guarded route where ZeroClick can reach it, then open your store's **Implementation** tab in the dashboard and run the API setup verification. ZeroClick sends an unpaid probe to your endpoint and confirms it answers with the priced `402` refusal and reports usage correctly. You can also test the guard locally: a plain request with no ZeroClick headers must get `401 {"error":"invalid_zeroclick_signature"}`. Only signed requests from ZeroClick reach your handler. That's the whole integration. Agents now transact with the guarded route through your pay URL: an unpaid call gets one priced `402` challenge, the agent pays with x402 or MPP and retries, and ZeroClick forwards the signed request to your API. Each paid request appears under **Transactions** in the dashboard, with the agent, service, meter quantities, and settled amount. ## Next steps Everything your API must verify, check, and return, on one page. The full request lifecycle: challenge, payment, signed forward, settlement. Bill work you can't size up front, like output tokens. Configuration and full API surface for each SDK. # Errors Source: https://docs.zeroclick.ai/resources/errors Every machine error code ZeroClick returns, by surface: the REST API, the usage endpoints, the responses your API must return, and the agent-facing pay URL. ZeroClick returns errors on four surfaces: the REST API at `https://api.zeroclick.io`, the usage endpoints under `/v1/usage`, your own API (which returns three fixed bodies to ZeroClick), and the agent-facing pay URL (`https://acme.pay.zeroclick.io`). This page lists every machine code per surface. ## The error envelope Every error body is JSON with a machine code in `error`: ```json theme={null} { "error": "usage_exhausted" } ``` Some errors carry additional fields: pay URL errors may add a `reason` (and `settlementReason`), and `amount_below_minimum` adds `minimumPurchaseUsd`. Two cases fall outside the machine-code convention: * Request validation failures return `400` with a human-readable message in `error` describing the invalid field. * Server errors return `500 { "error": "Internal server error" }`. The body never carries internals. ## REST API errors Management endpoints (sellers, services, meters, plans, prices, signing secrets, API keys, analytics) authenticate with `Authorization: Bearer zc_…` and share these codes: | Code | Status | Meaning | | ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `missing_bearer_token` | 401 | The request has no `Authorization: Bearer` header. | | `invalid_token` | 401 | The API key or session token did not verify. | | `auth_type_not_allowed` | 403 | The endpoint does not accept this credential type. For example, API keys cannot manage API keys (session-only), and the usage endpoints accept API keys only. | | `insufficient_scope` | 403 | The API key lacks a scope the endpoint requires. See [keys and secrets](/integrate/keys-and-secrets). | | `organization_required` | 403 | The caller has no current organization. | | `organization_permission_required` | 403 | The caller may not act on the selected organization. | | `seller_not_found` | 404 | No such seller in your organization. | | `service_not_found` | 404 | No such service. | | `meter_not_found` | 404 | No such meter. | | `plan_not_found` | 404 | No such plan. | | `plan_meter_price_not_found` | 404 | No such plan meter price. | | `signing_secret_not_found` | 404 | No such signing secret. | | `duplicate_slug` | 409 | A seller, service, or plan with this slug already exists in that scope. | | `duplicate_key` | 409 | A meter with this key already exists on the service. | | `duplicate_plan_meter_price` | 409 | The plan already prices this meter. | | `payg_price_not_whole_cents` | 422 | Pay-as-you-go rates are charged per call, so `priceUsd` must be a whole-cent amount. Rates on credit and subscription plans keep full 6-decimal precision. | 404 is the default for anything that does not resolve. A resource that exists but belongs to another organization is also a 404, never a 403. ## Usage endpoint errors Both usage endpoints live at `https://api.zeroclick.io` and require an API key: `POST /v1/usage/check` needs scope `usage:read`, `POST /v1/usage` needs `usage:write`. A wrong scope is `403 insufficient_scope`; a non-API-key credential is `403 auth_type_not_allowed`. ### Allowance denials (`POST /v1/usage/check`) A denial is not an HTTP error. The check returns `200` with the decision, and checking records nothing and burns no credit: ```json theme={null} { "allowed": false, "reason": "usage_exhausted" } ``` `reason` is `null` when `allowed` is `true`, and exactly one of these seven codes otherwise: | Reason | Meaning | Fix | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `service_not_found` | No service with this slug under the organization your API key belongs to. | Send the service slug as configured in the dashboard, with a key from the owning organization. | | `access_not_found` | The `zcRequestId` does not match a live ZeroClick request for this service, or the request has no purchase or payment behind it. | Pass the `zc-request-id` header value through unchanged. For unpaid traffic, this is the expected denial: return the `402 payment_required` body, and ZeroClick prices the challenge. | | `access_inactive` | The buyer's access is not active: canceled, or its period has not started. | Deny with the `402` body; the agent must purchase again. | | `plan_expired` | The access period has ended. | Deny with the `402` body; the agent re-purchases. | | `meter_not_found` | No meter with this slug exists on the service. | Use the meter key exactly as configured on the service. | | `meter_not_priced` | The buyer's plan does not price this meter. Also returned when an item declares neither `quantity` nor `maxQuantity` and the price has no `defaultMaxQuantity` to gate against. | Price the meter on the plan, or set the price's `defaultMaxQuantity` (or declare an explicit `maxQuantity` per request). | | `usage_exhausted` | The plan cannot cover this usage: credit or included allowance has run out. | Deny with the `402` body; ZeroClick challenges the agent to pay or top up. | See [check allowances](/integrate/check-allowances) for the request shape and [usage and allowances](/concepts/usage-and-allowances) for how ZeroClick decides coverage. ### Report errors (`POST /v1/usage`) A replayed `idempotencyKey` is a success (`200` with `duplicate: true`), not an error. Failures map to: | Status | Codes | Meaning | | ------ | ---------------------------------------------------------- | ---------------------------------------------------------------------- | | 402 | `access_inactive`, `plan_expired`, `usage_exhausted` | The access exists but can no longer be billed. | | 409 | `meter_not_priced` | The buyer's plan does not price this meter. | | 404 | `service_not_found`, `access_not_found`, `meter_not_found` | The slug or agent does not resolve (the default for anything missing). | ## Errors your API returns to ZeroClick Your API answers ZeroClick with exactly three non-2xx bodies. The [seller SDKs](/sdks/overview) build all of them; if you integrate over [REST](/integrate/rest-walkthrough), return them byte-for-byte. An invalid, missing, or stale `zc-signature`, before any other work: ```json 401 theme={null} { "error": "invalid_zeroclick_signature" } ``` A business denial (no allowance, unpaid probe). ZeroClick reads the `usage` list, prices it, and issues the buyer a payment challenge. The buyer never sees this body. `planSlug` is optional; `usage: []` means an identity challenge for a [free identity-scoped endpoint](/integrate/free-and-identity-endpoints): ```json 402 theme={null} { "error": "payment_required", "serviceSlug": "product-watch", "usage": [{ "meterSlug": "requests", "quantity": 1 }] } ``` The allowance API unreachable under a fail-closed outage policy: ```json 503 theme={null} { "error": "allowance_unavailable" } ``` ## Errors agents see at your pay URL Buyers transact at your pay URL (`https://acme.pay.zeroclick.io`). Once ZeroClick forwards a paid request, agents receive your API's own status codes and bodies unchanged. The codes below are ZeroClick's own, issued before or instead of the forward: | Code | Status | Meaning | | ----------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request` | 400 | Malformed purchase or top-up body. | | `amount_required` | 400 | A credit plan purchase without `amountUsd`. | | `payg_not_purchasable` | 400 | Purchase attempt on a pay-as-you-go plan: payg is called, never purchased. | | `amount_not_cent_increment` | 400 | The `amountUsd` is not a whole-cent value; buyer-chosen amounts settle on-chain in whole cents. | | `amount_below_minimum` | 400 | The `amountUsd` is under the plan's minimum purchase; the body carries `minimumPurchaseUsd`. | | `encryption_not_configured` | 400 | An `application/jose` body was sent but the seller publishes no encryption keys. | | `invalid_compact_jwe` | 400 | The body is not a five-segment Compact JWE. | | `unsupported_jwe_suite` | 400 | The JWE does not use the seller's fixed `alg`/`enc` suite. | | `jwe_kid_required` | 400 | The JWE protected header names no `kid`. | | `unknown_jwe_kid` | 400 | The `kid` is not in the seller's published JWKS. | | `private_reply_jwk` | 400 | The reply JWK carries private key material. | | `invalid_reply_jwk` | 400 | The reply JWK is not a public P-256 key. | | `bearer_required` | 401 | The call needs a registered agent credential: buying or extending a plan, or a free-mode call covered by included units (the free case also requires the credential to be claimed by a human with a verified email). The body carries an `auth` block with the registration recipe. Pay-as-you-go per-call payments need no credential. | | `payment_required` | 402 | The payment challenge: the one 402 an agent pays. A `reason` field distinguishes the cases below. | | `seller_not_found` | 404 | The host does not map to a seller. | | `plan_not_found` | 404 | Unknown plan id; agents use ids from `GET /manifest.json`. | | `signing_secret_required` | 409 | The seller has no active signing secret: setup is incomplete. | | `payment_request_mismatch` | 409 | The paid retry differs from the challenged request: method, path, or body bytes changed. The payment is bound to the request's body digest, so the retry must be byte-identical. | | `payment_request_already_completed` | 409 | A retry of a request that already delivered. Each payment forwards its request once. | | `payment_request_in_flight` | 409 | A concurrent retry while another attempt for the same request is still running. | | `payment_amount_mismatch` | 409 | A purchase or top-up retry carried a different `amountUsd` than the challenge that was paid. | | `usage_not_priced` | 409 | The upstream's refusal names usage no pay-as-you-go plan prices, so ZeroClick cannot mint a per-call challenge. | | `entitlements_not_available` | 501 | This storefront does not sell plans or top-ups. Pay-as-you-go per call still works. | | `capped_pricing_unavailable` | 503 | The request needs ceiling pricing but no reserve-and-pay-actual rail is currently available. | The seven `400` JWE codes apply only to requests sent with `Content-Type: application/jose` (end-to-end encrypted bodies). ZeroClick returns them before it asks for any payment. A billing failure detected after your API already delivered a 2xx does not become an error status. ZeroClick returns the delivered response and surfaces the failure in the `zc-usage-error` response header. See [headers](/resources/headers). ### `402 payment_required` reasons | Reason | Meaning | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *absent* | The normal per-call charge: the body carries a `payment` block with the exact `amountUsd` and a signable challenge per protocol. The agent pays and retries. | | `identity_invalid` | The identity proof did not verify. The agent signs a fresh challenge. | | `access_not_found` | The agent has no active plan to draw from or extend. It purchases one first. | | `usage_exhausted` | The plan's credit ran out. The agent tops up or purchases again. | | `settlement_failed` | The signed payment did not settle; `settlementReason` says why (for example `insufficient_funds`). The agent fixes its wallet and restarts for a fresh challenge. | A priced challenge looks like this (the same challenge also rides in the `payment-required` and `www-authenticate` response headers): ```json 402 theme={null} { "error": "payment_required", "zcAgentId": "agt_x7f2kq93bh0d", "zcRequestId": "zcreq_8h2m4x0q9k1f", "serviceSlug": "product-watch", "usage": [{ "meterSlug": "requests", "quantity": 1 }], "plan": { "id": "pln_...", "slug": "metered", "billingMode": "payg" }, "payment": { "id": "apay_...", "amountUsd": "0.010000", "rail": "base_usdc", "network": "base" }, "protocols": { "x402": { "x402Version": 2, "network": "eip155:8453", "scheme": "exact", "amount": "10000", "asset": "0x..." }, "mpp": { "challengeId": "...", "method": "tempo", "intent": "charge" } } } ``` The body also carries the standard x402 document fields (`x402Version`, `resource`, `accepts`) at the top level for body-reading clients. ZeroClick omits `rail` and `network` on anonymous challenges, where the agent picks a protocol by paying. The full lifecycle around this challenge is on [how ZeroClick works](/concepts/how-zeroclick-works); the protocols themselves are on [payment protocols](/concepts/payment-protocols). # Headers Source: https://docs.zeroclick.ai/resources/headers The zc-* and payment header reference for every direction: ZeroClick to your API, your API to ZeroClick, ZeroClick to the agent, and the agent to ZeroClick. Four parties exchange headers on every paid request: the agent, ZeroClick's proxy, your API, and the response path back. This page lists every header per direction, with formats and realistic values. The `zc-request-id` value (`zcreq_…`) is the correlation spine. The same id flows through the challenge, the paid retry, the allowance check, and the usage record. See [how ZeroClick works](/concepts/how-zeroclick-works) for the lifecycle. ## ZeroClick to your API (request) Headers ZeroClick sets on every request it forwards to your `upstreamBaseUrl`: | Header | Example | Notes | | ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `zc-request-id` | `zcreq_8h2m4x0q9k1f` | Correlation id for this request. Echo it in `POST /v1/usage/check` as `zcRequestId`. | | `zc-agent-id` | `agt_x7f2kq93bh0d` | The agent that made this call. Absent on signed anonymous probes: a valid, expected state used to price pay-as-you-go 402s. | | `zc-anonymous-id` | `agt_x7f2kq93bh0d` | The same value as `zc-agent-id`, under the name that will eventually replace it. Present and absent in exactly the same cases. | | `zc-buyer-id` | `byr_3n8v1c6t5j2w` | The person or company the agent belongs to. Sent only once that owner is known; absent means an anonymous agent. | | `zc-signature` | see below | HMAC proof the request came from ZeroClick. Verify it before anything else. | | `zc-sandbox` | `1` | Sent on sandbox test traffic only; branch to your own test mode on it. | ### `zc-agent-id` and `zc-buyer-id` These answer two different questions, and a request can carry either one or both: * `zc-agent-id` is **who called**. It is always the agent that executed this request, never some other agent that happens to have paid for the plan being drawn down. * `zc-buyer-id` is **who they belong to**. One buyer can hold many agents; every one of them is entitled to everything the buyer owns. | What you see | What it means | | ------------- | ----------------------------------------------------------------------------------- | | Neither | A signed anonymous probe. No identity has been established yet. | | Agent id only | An anonymous agent: identified and billable, but not yet attached to a known owner. | | Both | The agent belongs to that buyer and inherits the buyer's entitlements. | Key per-caller state (rate limits, per-run scratch data) on `zc-agent-id`. Key per-customer state (history, tenancy, saved records, anything a human should still see after their agent is replaced) on `zc-buyer-id` when it is present, since a buyer can retire one agent and call you with the next. Two requests carrying the same `zc-buyer-id` under different agent ids are the same customer. The signature covers `zc-agent-id`, not `zc-buyer-id`. Treat the buyer id as a fact ZeroClick asserts over the authenticated channel rather than as an independently proven one, and never let it alone unlock records you would not release to the agent id it arrived with. Before forwarding, ZeroClick strips every inbound `zc-*` and `x-zc-*` header plus `x-payment`, `payment`, and `payment-signature`, then sets its own. An agent can never spoof these values. Any upstream auth you configured between ZeroClick and your API passes through unchanged. ### `zc-signature` ```text theme={null} zc-signature: t=1785254400,kid=hsec_k5nq0v7m3d8p,v1=9f2c7d41a8e05b6c3f1d92e47ab08c5d6e1f3a29b47c80d5e2f16a3b4c5d6e7f ``` The grammar is `t=,kid=,v1=<64 lowercase hex>`; sandbox traffic appends `,sb=1`. `t` is the signing time (verify within a tolerance, 300 seconds by default). `kid` names the signing secret so you can select the right one during rotation. `v1` is the HMAC-SHA256 (constant-time compare) over a six-field canonical string: `t`, the uppercased method, the raw path and query, the SHA-256 hex of the raw body bytes, the `zc-request-id` value, and the `zc-agent-id` value or empty string. The signed canonical is identical for sandbox and live traffic; verifiers that parse only `t`/`kid`/`v1` ignore the trailing `sb=1`. Full byte-level rules: [signature spec](/integrate/signature-spec). ## Your API to ZeroClick (response) | Header | Example | Notes | | ---------- | --------- | -------------------------------------------------------------- | | `zc-usage` | see below | Settles usage synchronously on a delivered response. 2xx only. | ### `zc-usage` ```text theme={null} zc-usage: [{"serviceSlug":"product-watch","meterSlug":"requests","quantity":1},{"serviceSlug":"product-watch","meterSlug":"output_tokens","quantity":842}] ``` A JSON array of `{ "serviceSlug", "meterSlug", "quantity" }` items; `quantity` is an integer from 0 to 2,147,483,647 (0 settles a capped meter at zero). Set it only on a 2xx response that delivered. A 4xx must not carry it. ZeroClick strips the header before the agent sees the response. This is the preferred way to settle usage; [settle usage](/integrate/settle-usage) covers it against the async `POST /v1/usage` alternative. If billing fails after your API already delivered (a malformed header, a denial, or a settlement exception), ZeroClick does not drop the response. It returns the body to the agent and names the failure in the `zc-usage-error` response header. The payment stays observable rather than silently lost. ## ZeroClick to the agent (response) | Header | When | Carries | | ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `payment-required` | 402 challenges | The full signable x402 challenge (the complete PaymentRequired document, header-encoded). | | `www-authenticate` | 402 challenges | The full signable MPP challenge. | | `zc-billing` | Responses to requests paid with a per-request payment | The settlement summary: authorized vs. charged vs. remainder. | | `payment-response` | Paid x402 responses | The x402 settle receipt, base64-encoded: loggable proof of what settled. | | `payment-receipt` | Paid MPP responses | The serialized MPP receipt. | | `zc-usage-error` | Delivered responses whose billing failed | A machine code: an allowance denial reason, `invalid_usage_header`, or `settlement_failed`. | | `zc-usage-adjusted` | Settled quantities differ from reported | JSON array of `{ "meterSlug", "reportedQuantity", "settledQuantity" }`. Reports beyond the authorization settle at the cap or at zero. | Identity-only responses carry no receipt header. Free-mode responses covered by included units carry no `zc-billing`: there was no per-request payment to settle. ### `zc-billing` ```text theme={null} zc-billing: {"status":"settled","authorizedUsd":"1.250000","chargedUsd":"0.421000","remainderUsd":"0.829000","remainderHandling":"escrow_returned","settleTransactionHash":"0x9c2e..."} ``` | Field | Meaning | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `settled` once the settlement committed; `settling` while it is still finishing (the amounts below are withheld until final). | | `authorizedUsd` | What the agent's payment authorized. On [ceiling-priced requests](/integrate/charge-up-to-a-maximum) this is the ceiling, not the charge. | | `chargedUsd` | What the request actually cost, read back from the settlement ledger. | | `remainderUsd` | `authorizedUsd` minus `chargedUsd`. | | `remainderHandling` | Where the remainder went: `credited` (added to the buyer's balance with this seller), `escrow_returned` (it never left the wallet, or was refunded from escrow), or `escrow_expired` (the hold lapsed unsettled; the buyer kept the full ceiling). | | `settleTransactionHash` | The on-chain settle transaction, when one exists. | ## The agent to ZeroClick (request) | Header | Example | Notes | | --------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `x-payment` | `x-payment: ` | The signed x402 proof answering a `payment-required` challenge. Also accepted under the `payment-signature` name. | | `authorization` | `authorization: Bearer ` | The signed MPP credential answering a `www-authenticate` challenge. | | `zc-mode` | `zc-mode: free` | Opt-in to free included-units coverage on pay-as-you-go. Any other value, or no header, is paid mode. | The proof carries everything: the challenge an agent signs embeds ZeroClick's payment metadata, so the paid retry needs no other headers. The retry must be byte-identical to the challenged request, because the payment is bound to the request's body digest. Protocol details: [payment protocols](/concepts/payment-protocols). ### `zc-mode` `free` is the only recognized value; ZeroClick treats anything else as paid rather than rejecting it. The mode applies to the whole request, so your allowance pre-gate (`POST /v1/usage/check`) honors the same decision the proxy made. Agents send it on every call they want covered. ZeroClick tracks the allowance per buyer - every agent claimed by the same human draws from one shared pool - and serves it only to claimed agents with a verified email, so an unidentified free-mode caller is answered with `401 bearer_required` and registers - and gets claimed - before its first covered call. See [free and identity endpoints](/integrate/free-and-identity-endpoints). # Account and key reference Source: https://docs.zeroclick.ai/resources/stateful-access-reference The complete wire contract for stateful sellers: request and response bodies, field meanings, handler return values, retry and timeout behavior, and the signature for non-TypeScript stacks. Every detail behind [sell accounts and API keys](/integrate/stateful-sellers). 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: ```sh theme={null} curl -X PUT https://api.zeroclick.io/v1/sellers/{sellerId}/stateful-access \ -H "authorization: Bearer $ZEROCLICK_API_KEY" \ -H "content-type: application/json" \ -d '{"accessEndpointUrl":"https://accounts.example.com/hooks/zeroclick"}' ``` `GET` the same path returns the current `accessEndpointUrl`, plus `salesEnabled` (can buyers make new purchases) and `servicingEnabled` (can existing customers still recover and rotate keys). Changing those two is not self-serve — email [help@zeroclick.ai](mailto:help@zeroclick.ai). The mint route is always the write route plus `/{accessId}/keys`. ## Request headers Both calls arrive as `POST` with: | Header | Meaning | | --------------- | --------------------------------------------------------------------------------------------- | | `zc-signature` | `t=…,kid=…,v1=…`. A `t` more than 5 minutes old or in the future is rejected. | | `zc-agent-id` | The customer this account belongs to. Sent again as `zc-anonymous-id` with the same value. | | `zc-request-id` | Stable across retries of the same call. For account writes it is `{accessId}:{stateVersion}`. | ## The account write ```json POST /zeroclick/access theme={null} { "accessId": "zacc_8h2m4x0q9k1f", "agent": "agt_x7f2kq93bh0d", "idempotencyKey": "zacc_8h2m4x0q9k1f:3", "stateVersion": 3, "plan": { "slug": "research-pro", "name": "Research Pro", "billingMode": "subscription_usage", "interval": "month", "basePriceUsd": "20.000000" }, "state": { "period": { "start": "2026-08-01T00:00:00Z", "end": "2026-09-01T00:00:00Z" }, "creditGrantedUsd": "45.000000", "status": "active" } } ``` | Field | What it means | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `accessId` | The durable ZeroClick account handle, `zacc_…`. Your account key if you chose `accountKey: "accessId"`. | | `agent` | The customer, `agt_…`. Your account key if you chose `accountKey: "agent"`. Always equal to the signed `zc-agent-id`; the SDK rejects the request with `400` if they disagree. | | `stateVersion` | A counter that goes up every time ZeroClick sends a newer picture of this account. **Apply a write only if this is higher than the last version you stored.** Equal or lower is a retry you have already handled. | | `idempotencyKey` | `{accessId}:{stateVersion}`, the same value as `zc-request-id`. Keep it for your audit log; `stateVersion` is what you deduplicate on. | | `plan.billingMode` | `subscription`, `subscription_usage`, or `credit` — see [plans and pricing](/concepts/plans-and-pricing#the-four-billing-modes). | | `plan.interval` | `none`, `month`, or `year`. | | `plan.basePriceUsd` | What the plan charges per period. | | `state.period` | The window this plan is paid for. `end` is `null` for plans with no expiry. | | `state.creditGrantedUsd` | **The total ever granted for this account** — not the remaining balance, and not the size of this top-up. `null` if the plan has no credit component. | | `state.status` | `active` or `suspended`. Suspend service on `suspended`; do not delete anything. | 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 `creditGrantedUsd` is a running total rather than a delta. `deriveCreditDelta` turns that total into the amount to add, and returns one of three outcomes: | Outcome | What to do | | --------------------- | --------------------------------------------------------------------------------- | | `credit` | Add `deltaUsdMicros` to the balance, then store `next` as the new recorded total. | | `no_credit_dimension` | This plan has no credit component. Store `next`; leave the balance alone. | | `replay` | You have already applied this version or a newer one. Do nothing. | ## The key mint ```json POST /zeroclick/access/zacc_8h2m4x0q9k1f/keys theme={null} { "agent": "agt_x7f2kq93bh0d" } ``` 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. ```json 200 theme={null} { "apiKey": "sk_live_…", "maxKeys": 1, "keyExpiresAt": "2027-08-01T00:00:00Z" } ``` 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`: | Field | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `accountId` | The id selected by your `accountKey` — the `agent` or the `accessId`. Key your account table on this. | | `accessId` | Always the durable ZeroClick account handle, whichever `accountKey` you chose. | | `agentId` | Always the customer id, whichever `accountKey` you chose. Already checked against the signed `zc-agent-id`. | | `entitlement` | The parsed request body, plus `basePriceUsdMicros` and `creditGrantedUsdMicros` as integers, and `period`, `status`, and `creditGrantedUsd` lifted to the top level for convenience. | | `requestId` | The `zc-request-id` header. | | `dedupeKey` | `write:{requestId}`. A ready-made unique key if you keep a table of processed callbacks; deduplicating on `stateVersion` is still the rule that protects the account state. | | `kid` | Which signing secret verified this request. Useful for logging a rotation. | `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`: | Return | Status | What ZeroClick does | | -------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------- | | `{ lifecycle: "active" }` | `200` | Acknowledges the applied state. ZeroClick finalizes the payment; the buyer may then request a key. | | `{ lifecycle: "provisioning", retryAfterSeconds }` | `200` | Sends the same write again after the hint, capped at 30 minutes. | | *throws* | `503` | Treated as temporary and retried. Your `onHandlerError` callback fires first. | `onMint`: | Return | Status | What ZeroClick does | | --------------------------------------------- | ------ | ------------------------------------------------------------------------------------------ | | `{ apiKey, keyExpiresAt? }` | `200` | Delivers the key to the buyer, with `cache-control: no-store`. | | `{ notProvisioned: true, retryAfterSeconds }` | `409` | The account exists but is not ready; the buyer is told to retry, default 30 seconds. | | `{ unknown: true }` | `404` | No such account. The mint fails — use this only when the account genuinely does not exist. | | `{ conflict: { code } }` | `409` | Your own refusal, with your code in the body. | | *throws* | `503` | The mint fails and `onHandlerError` fires. | Before either handler runs, the SDK answers `401` on a missing, stale, or invalid signature, and `400` if the body is malformed or its `agent` does not match the signed `zc-agent-id`. ## 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. | | Behavior | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Response budget | **7 seconds** for both calls. Anything slower belongs behind `lifecycle: "provisioning"`. | | Retried | Network failure, `429`, any `5xx`, and a `200` whose body cannot be parsed. | | **Not** retried | Any other non-2xx, including `400`, `403`, and `404`. A bug that returns `400` permanently fails the delivery — return `503` when unsure. | | Backoff | Starts at 30 seconds and doubles with jitter, to a 30-minute ceiling. | | Retry hints | A `Retry-After` header on a `429`/`5xx`, or `retryAfterSeconds` in a `200` body. Both are capped at 30 minutes. | | Giving up | After 20 attempts or 24 hours, whichever comes first. | | Redirects | Not followed. Serve the account routes directly. | | Signing-secret rotation | If your endpoint answers `401`, ZeroClick retries once with your newest active secret. See [rotating a signing secret](/integrate/keys-and-secrets#rotate-a-signing-secret). | 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. | Route | What it does | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /purchases/receipt` | Lists every purchase this buyer holds with you: `accessId`, `provisioningState`, plan, access period, delivery status, plus a `redeemUrl` and a `topUpUrl` for each. | | `POST /keys/redeem` | Asks for an API key, which triggers your mint handler. `accessId` may be omitted when the buyer holds exactly one account with you. | | `POST /extend` | Tops up or renews. The `topUpUrl` above is this route with `?accessId=…` already filled in. | ## Free plans A `$0` `subscription` or `subscription_usage` plan can create an account, but only for a buyer who has verified their email address — that is what stops one person minting unlimited free accounts. A buyer without a verified email cannot claim a live free plan. Once created, the free account is durable and recoverable across that verified buyer's agents, just like a paid account. ## Signature, for stacks without an SDK The stateful helpers ship in the TypeScript SDK today. 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](/integrate/signature-spec#canonical-string) plus a trailing purpose: ``` {timestamp} {METHOD} {path and query, unnormalized} {lowercase hex SHA-256 of the raw body} {zc-request-id} {zc-agent-id} {access.write | access.mint} ``` | Route | Purpose | | --------------------------------- | -------------- | | `POST {endpoint}` | `access.write` | | `POST {endpoint}/{accessId}/keys` | `access.mint` | 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. # Go SDK API reference Source: https://docs.zeroclick.ai/sdks/go/api Every public identifier in the Go seller SDK: Client methods, package helpers, response builders, result types, and the jwe subpackage. Everything in `cdn.zeroclick.io/sdks/sellers-go`, importable as: ```go theme={null} import sellers "cdn.zeroclick.io/sdks/sellers-go" ``` The `Client` is safe for concurrent use: build it once with [`New`](/sdks/go/configuration) and share it. Every decision flows through the framework-neutral [`Request`](#request) and [`Response`](#response) types, so the SDK works outside `net/http` too. `FromHTTP` is only the `net/http` adapter, and the [`Meter` and `Identify` middleware](/sdks/go/middleware) are packaged versions of the same calls. Guards return decisions, not errors: an unsigned or unpayable request is an expected event on a public endpoint, so it comes back as a result carrying the exact `Response` to return. The SDK reserves errors for your own misconfiguration and for ZeroClick API failures. See [Go SDK errors](/sdks/go/errors). ## Client methods ### Guard ```go theme={null} func (c *Client) Guard(ctx context.Context, request *Request, serviceSlug string, usage []UsageItem, planSlug string) (GuardResult, error) ``` The one call a billable endpoint needs: it verifies the signature, then checks that the buyer's allowance covers `usage`, before any work happens. Verification always runs first: a request that fails it never reaches the allowance API. `planSlug` may be empty. ```go theme={null} result, err := seller.Guard(r.Context(), sellers.FromHTTP(r, body), "product-watch", []sellers.UsageItem{{MeterSlug: "requests", Quantity: 1}}, "") if err != nil { // The usage list was malformed, or Policy is PolicyThrow. http.Error(w, `{"error":"internal_error"}`, http.StatusInternalServerError) return } if !result.OK { result.Response.WriteTo(w) return } // Serve the request. result.Context.AgentID is the buyer, // result.Context.RequestID correlates with ZeroClick's logs. ``` `body` must be the raw request bytes, read in full before the call. The middleware bounds that read for you. When you call `Guard` directly, bound the read yourself (for example with `http.MaxBytesReader`). Declaring an exact `Quantity` lets ZeroClick offer the `exact` x402 payment scheme. A `MaxQuantity` forces `upto`, which fewer clients can pay. Prefer a fixed per-request charge, and report variable work afterwards with [`ReportUsage`](#reportusage). See [charge up to a maximum](/integrate/charge-up-to-a-maximum). `Guard` deliberately severs the caller's cancellation for the allowance check: it wraps `ctx` with `context.WithoutCancel` and applies its own `CheckTimeout`. Inheriting cancellation meant a client disconnect, or any proxy-side timeout, short-circuited the check before it was even sent. Under the `allow` policy the request was served anyway: a buyer who would have been denied got served, and the API was never asked. The check's own timeout still bounds it. [`CheckAllowance`](#checkallowance) keeps the caller's context, because it surfaces the error instead of applying a policy to it. The SDK still honors a denial with a reason it does not recognize: `allowed` is legible on its own, and the unfamiliar reason passes through in `GuardResult.Reason` for you to log. Rejecting the whole response on an unfamiliar reason would turn a denial into an allow the day ZeroClick adds a new one. An additive server change must stay additive. ### GuardIdentity ```go theme={null} func (c *Client) GuardIdentity(request *Request, serviceSlug string) (GuardResult, error) ``` Guards a free endpoint that must still know which buyer is calling, such as a limits or account route. It makes no network call. A signed request with no `zc-agent-id` is an anonymous probe: valid, but it has not established a buyer. `GuardIdentity` answers it with a `402` carrying an empty usage list (`Reason` is `identity_required`). That makes ZeroClick issue a \$0 identity challenge and retry the request with the identity attached. A signed request that does carry a buyer passes with `Outcome` set to `OutcomeNotRequired`. See [free and identity endpoints](/integrate/free-and-identity-endpoints). ### Verify ```go theme={null} func (c *Client) Verify(request *Request) (VerifyResult, error) ``` Verifies the request's signature using the client's secrets, tolerance, and clock; it does nothing else. Use it directly only for endpoints that bill nothing. Anything billable should go through [`Guard`](#guard), which also checks that the buyer can pay. The package-level [`Verify`](#verify-package-function) does the same without a `Client`. ### CheckAllowance ```go theme={null} func (c *Client) CheckAllowance(ctx context.Context, requestID, serviceSlug string, usage []UsageItem) (AllowanceDecision, error) ``` Asks the allowance API directly, without verifying a request and without applying the outage policy. Errors surface for you to handle, classified with [`IsAllowanceUnavailable`](#isallowanceunavailable). It keeps the caller's context, bounded by `CheckTimeout`. Checking records nothing and burns no credit. [`Guard`](#guard) is what an endpoint should use; this is for sellers driving the check themselves. ```go theme={null} decision, err := seller.CheckAllowance(ctx, "zcreq_8h2m4x0q9k1f", "product-watch", []sellers.UsageItem{{MeterSlug: "output_tokens", MaxQuantity: 4096}}) if err != nil { // No usable answer; classify with sellers.IsAllowanceUnavailable(err). } if !decision.Allowed { // decision.Reason, e.g. "usage_exhausted" } ``` ### ReportUsage ```go theme={null} func (c *Client) ReportUsage(ctx context.Context, input ReportUsageInput) (ReportUsageResult, error) ``` Records usage out of band, after the response has gone out. This is how a seller bills for work whose amount is only known once it is done (pages extracted, tokens produced). The seller still declares a fixed per-request charge to the guard, so the `exact` scheme stays available. `CheckTimeout` bounds the call, and the write-scoped key authenticates it. ```go theme={null} _, err := seller.ReportUsage(sellers.BackgroundContext(r), sellers.ReportUsageInput{ AgentID: zc.AgentID, ServiceSlug: "product-watch", MeterSlug: "output_tokens", Quantity: tokens, IdempotencyKey: zc.RequestID + "_output_tokens", // derived, never random }) ``` Reports are idempotent on `IdempotencyKey`: a retried report replays the stored event with `Duplicate: true`, which is a success, not a failure. Derive the key from the request id and the meter: a random key double-bills on retry. A failure here means work was delivered and not billed. Log it with the reason; do not fail the request the buyer already received. ### Meter ```go theme={null} func (c *Client) Meter(usage ...UsageItem) func(http.Handler) http.Handler ``` Middleware that guards a billable endpoint: it verifies the signature, checks that the buyer can pay, and, if the handler responds 2xx, settles the declared usage via the `zc-usage` response header. It takes fixed quantities only. If handed an [`UpTo`](#perrequest-and-upto) item or a non-positive quantity, it panics at wire-up. Covered in depth in [Go SDK middleware](/sdks/go/middleware). ### Identify ```go theme={null} func (c *Client) Identify() func(http.Handler) http.Handler ``` Middleware form of [`GuardIdentity`](#guardidentity), for free identity-scoped endpoints. Also covered in [Go SDK middleware](/sdks/go/middleware). ## Request helpers ### FromHTTP ```go theme={null} func FromHTTP(r *http.Request, body []byte) *Request ``` Builds a [`Request`](#request) from a server-side `*http.Request` and an already-read body. It reads `r.RequestURI`, which is the raw percent-encoded request target exactly as it arrived. `r.URL.Path` is decoded and would break verification for any target containing an encoded character. On a hand-built request (where `RequestURI` is unset), it falls back to the escaped path plus raw query, which is exact unless the path held an encoded slash. ### FromContext ```go theme={null} func FromContext(ctx context.Context) (Context, bool) ``` Returns what a guarded request proved about its caller; `ok` is false on an unguarded route. ```go theme={null} zc, ok := sellers.FromContext(r.Context()) // zc.AgentID: the buyer (agt_…) // zc.RequestID: correlates with ZeroClick's logs; use it for idempotency keys ``` ### BackgroundContext ```go theme={null} func BackgroundContext(r *http.Request) context.Context ``` Returns a context for work that outlives the response, such as reporting usage after the buyer has their result. It is `context.WithoutCancel(r.Context())`: context values survive, cancellation does not. Deriving from `r.Context()` would not work: the server cancels that context the moment the handler returns. The report would be dropped, and the work would go unbilled with nothing in the logs to say so. ## Usage constructors ### PerRequest and UpTo ```go theme={null} func PerRequest(meterSlug string, quantity int) UsageItem func UpTo(meterSlug string, maxQuantity int) UsageItem ``` `PerRequest` declares a fixed charge, which is what lets ZeroClick offer the `exact` x402 scheme. `UpTo` declares a ceiling for work whose cost is unknown until it is done. It settles at the amount actually reported, so a ceiling never overcharges. But it forces the `upto` scheme, which fewer clients can pay, and [`Meter`](#meter) rejects it because a ceiling has no settled quantity. Prefer `PerRequest` plus [`ReportUsage`](#reportusage) for the variable part. ## Signing secret helpers ### SecretsFromEnv and ParseSigningSecrets ```go theme={null} func SecretsFromEnv() (map[string]string, error) func ParseSigningSecrets(raw string) (map[string]string, error) ``` `SecretsFromEnv` parses the `ZEROCLICK_SIGNING_SECRETS` environment variable (the constant `SigningSecretsEnv`), which holds `:` pairs, comma-separated. Listing more than one is how a rotation survives. `ParseSigningSecrets` parses the same format from any source, such as a secret manager. Entries split on the first colon only, so secrets containing colons are safe, and parse errors never echo the value. See [configuration](/sdks/go/configuration) for the format and rotation examples. ### SigningSecrets ```go theme={null} func SigningSecrets(secrets map[string]string) ResolveSigningSecret ``` Adapts a static map to the `ResolveSigningSecret` resolver signature, for use with the package-level [`Verify`](#verify-package-function). `Config.SigningSecrets` applies this adapter for you. ```go theme={null} type ResolveSigningSecret func(kid string) (secret string, ok bool, err error) ``` `ok == false` means the key id is unknown (a refusal); a non-nil error means the lookup itself failed, such as a vault being unreachable (a fault). ## Response builders `Guard` and the middleware build these responses for you; use them directly when you drive the flow yourself. ### PaymentRequired ```go theme={null} func PaymentRequired(serviceSlug string, usage []UsageItem, planSlug string) (*Response, error) ``` The `402` that tells ZeroClick what this request would cost: `{"error":"payment_required","serviceSlug":…,"usage":[…]}`, with `planSlug` included when non-empty. ZeroClick reads the usage list, prices it, and issues the buyer an x402 or MPP challenge; the buyer never sees this body. Return it **before** doing the work: a `402` after the fact means the work is done and unpaid. An empty usage list is meaningful, not a mistake: it is the free identity-scoped refusal, answered with a \$0 identity challenge. ```go theme={null} response, err := sellers.PaymentRequired("product-watch", []sellers.UsageItem{{MeterSlug: "requests", Quantity: 1}}, "") ``` ### MustPaymentRequired ```go theme={null} func MustPaymentRequired(serviceSlug string, usage []UsageItem, planSlug string) *Response ``` [`PaymentRequired`](#paymentrequired) for a usage list built from constants, where a validation failure is a bug rather than a runtime condition. It panics instead of returning an error. ### InvalidSignature ```go theme={null} func InvalidSignature() *Response ``` The `401 {"error":"invalid_zeroclick_signature"}` for a request that did not prove it came from ZeroClick. The body says nothing about why, deliberately. Your logs carry the [`FailureReason`](/sdks/go/errors). ### AllowanceUnavailable ```go theme={null} func AllowanceUnavailable() *Response ``` The `503 {"error":"allowance_unavailable"}` for "we could not reach the allowance API", as distinct from "the allowance API said no". The SDK returns it only under `PolicyDeny`. ### WithUsage and UsageHeader ```go theme={null} func WithUsage(response *Response, usage []SyncUsageItem) (*Response, error) func UsageHeader(usage []SyncUsageItem) (string, error) ``` `UsageHeader` renders the `zc-usage` header value: what this response actually consumed, settled against whatever was authorized. `WithUsage` attaches it to a response. Set it only on a response that delivered: a `4xx` for input you rejected must not carry it, because that bills the buyer for a refusal. The [`Meter`](#meter) middleware does this for you on 2xx responses. ```go theme={null} response, err := sellers.WithUsage(sellers.JSON(200, result), []sellers.SyncUsageItem{ {ServiceSlug: "product-watch", MeterSlug: "requests", Quantity: 1}, }) ``` ### JSON ```go theme={null} func JSON(status int, payload any) *Response ``` Builds a JSON [`Response`](#response) with the `content-type` header set. It panics only on a payload that cannot be marshalled, which is a programming error rather than a runtime condition. ## Verification primitives ### Verify (package function) ```go theme={null} func Verify(req *Request, opts VerifyOptions) (VerifyResult, error) type VerifyOptions struct { Resolve ResolveSigningSecret // required ToleranceSeconds int // 0 means 300 Now func() time.Time // nil means time.Now } ``` Verifies a ZeroClick signature over an already-read body, without a `Client`; no API key is needed. The body must be the raw bytes as received: re-serialized JSON will not match, because the digest covers bytes, not meaning. The verifier parses the signature header additively: it ignores unknown members (ZeroClick appends fields such as the sandbox marker over time) and rejects duplicate members. The returned error is non-nil only for a resolver fault (wrapped in `ErrSecretResolution`) or a missing resolver; every authentication failure is a `VerifyResult` decision instead. ### CanonicalString ```go theme={null} func CanonicalString(timestamp, method, pathAndQuery string, body []byte, requestID, agentID string) string ``` Builds the exact bytes that get signed, as six newline-joined fields: the timestamp, the uppercased method, the raw percent-encoded path and query, the lowercase hex SHA-256 of the body bytes, the `zc-request-id` value, and the `zc-agent-id` value or empty string. Useful for debugging a verification mismatch or building your own verifier; the full wire format is in the [signature spec](/integrate/signature-spec). ## Error helpers ### IsAllowanceUnavailable ```go theme={null} func IsAllowanceUnavailable(err error) bool ``` Reports whether an error means "the allowance API did not give us an answer", as opposed to "the API said no" or "we are misconfigured". Only that condition is subject to the configured outage policy. The full classification is on the [errors page](/sdks/go/errors). ## Types ### Request ```go theme={null} type Request struct { Method string PathAndQuery string // raw, percent-encoded, exactly as it arrived Headers map[string][]string // an http.Header or any map Body []byte // raw bytes as received, before any parsing } func (r *Request) Header(name string) string ``` The framework-neutral view of an inbound request. `PathAndQuery` must be the raw percent-encoded target: the signature covers those bytes, and the decoded path most frameworks hand you hashes differently. [`FromHTTP`](#fromhttp) takes care of this for `net/http`. `Header` looks up the first value for a name, case-insensitively, whether `Headers` is an `http.Header` or a plain map. ### Response ```go theme={null} type Response struct { Status int Body []byte Headers map[string]string } func (r *Response) WriteTo(w http.ResponseWriter) error ``` The framework-neutral response you return to ZeroClick. `WriteTo` writes it to a `net/http` response writer; outside `net/http`, map the three fields onto your framework's response type. ### Context ```go theme={null} type Context struct { RequestID string // zcreq_…, the correlation id across ZeroClick and your logs AgentID string // agt_…; empty for a signed anonymous probe AnonymousID string // the same value as AgentID, under the name that will replace it BuyerID string // byr_…; the owner behind the agent, empty for an anonymous agent Timestamp int64 // the signature's unix-seconds timestamp KID string // which signing secret verified this request } ``` `AnonymousID` repeats `AgentID` under the name that will eventually replace `zc-agent-id`. `BuyerID` is the buyer that agent belongs to; unlike the fields above it is not covered by the signature. See [agents and access](/concepts/agents-and-access). What a verified request proved. `AgentID` is empty for a signed anonymous probe, which is valid: ZeroClick uses probes to price pay-as-you-go `402`s. Test it with `== ""`, never against a sentinel: an absent header and a present-but-empty one both mean "no buyer identity" and both sign identically. ### GuardResult and GuardOutcome ```go theme={null} type GuardResult struct { OK bool Context Context Outcome GuardOutcome Reason string Response *Response // never nil when OK is false } const ( OutcomeAllowed GuardOutcome = "allowed" // the buyer's allowance covers this request OutcomeUnavailable GuardOutcome = "unavailable" // no answer; the policy chose to serve, possibly unbilled OutcomeNotRequired GuardOutcome = "not_required" // identity proven; no allowance was needed ) ``` The guard's decision. Read `OK` first; when it is false, return `Response` verbatim and do no work. `Reason` is the denial code: a verification [`FailureReason`](/sdks/go/errors), an allowance denial such as `usage_exhausted`, `identity_required` from [`GuardIdentity`](#guardidentity), or `allowance_unavailable` under `PolicyDeny`. Watch for `Outcome == OutcomeUnavailable` in logs and metrics: it means work is being done that may not be billed. ### VerifyResult ```go theme={null} type VerifyResult struct { OK bool Context Context Reason FailureReason // why the request was refused, for your logs Response *Response // the 401 to return, ready-built } ``` A decision, not an error. The errors page lists the [`FailureReason`](/sdks/go/errors) values and explains why they are values rather than errors. ### UsageItem ```go theme={null} type UsageItem struct { MeterSlug string Quantity int MaxQuantity int } ``` What a request will be charged for. Set exactly one of `Quantity` or `MaxQuantity`, or neither to defer to the meter's configured per-request ceiling. A usage list must name at least one meter and may not repeat one. See [usage and allowances](/concepts/usage-and-allowances) for how the three forms are priced. ### SyncUsageItem ```go theme={null} type SyncUsageItem struct { ServiceSlug string MeterSlug string Quantity int } ``` Actual usage reported alongside a successful response, in the `zc-usage` header. ### AllowanceDecision ```go theme={null} type AllowanceDecision struct { Allowed bool Reason string // a denial code, or "" when allowed } ``` ZeroClick's answer to an allowance check. The SDK exports the denial codes as constants: ```go theme={null} const ( DenialServiceNotFound = "service_not_found" DenialAccessNotFound = "access_not_found" DenialAccessInactive = "access_inactive" DenialPlanExpired = "plan_expired" DenialMeterNotFound = "meter_not_found" DenialMeterNotPriced = "meter_not_priced" DenialUsageExhausted = "usage_exhausted" ) ``` A reason outside this set passes through unchanged, and the decision still stands. ### ReportUsageInput and ReportUsageResult ```go theme={null} type ReportUsageInput struct { AgentID string // the buyer, from Context.AgentID ServiceSlug string MeterSlug string Quantity int // must be positive IdempotencyKey string // derive from the request id + meter, never random OccurredAt string // RFC 3339; empty means "now", decided server-side } type ReportUsageResult struct { Recorded bool Duplicate bool // the idempotency key had already landed: a success UsageEvent map[string]any // the recorded event, as returned by the API } ``` Input and outcome of [`ReportUsage`](#reportusage). ## Encrypted request bodies ZeroClick can encrypt a buyer's request body to your public key. Support lives in a separate subpackage, so a seller who does not use it never pulls in a JOSE library. The core package imports only the standard library, and Go's module graph pruning keeps `go-jose` out of your `go.sum` and your binary unless you import this: ```go theme={null} import "cdn.zeroclick.io/sdks/sellers-go/jwe" ``` ```go theme={null} envelope, err := jwe.DecryptRequest(body, jwe.PrivateKeys(keys)) // envelope.Plaintext is the original request body; envelope.ReplyJWK is // non-nil when the buyer asked for an encrypted reply. reply, err := jwe.EncryptResponse(response, envelope) // Returns the response unchanged when no reply key was sent. ``` Verify the signature **before** decrypting: the signature covers the ciphertext as it arrived on the wire, not the plaintext. The cipher suite is pinned (ECDH-ES+A256KW key management, A256GCM content encryption), so a sender cannot negotiate something weaker. The [errors page](/sdks/go/errors) lists the subpackage's error codes. Known limitation: `jwe.DecryptRequest` returns `decryption_failed` for an encrypted request whose plaintext is **empty**; every non-empty body works. This is a limitation of Go's JOSE libraries, not of the platform: ZeroClick permits empty-plaintext encrypted requests, and the TypeScript and Python SDKs decrypt them. If your endpoint accepts encrypted requests and a zero-length body is meaningful to it, handle that case before decrypting. # Go SDK configuration Source: https://docs.zeroclick.ai/sdks/go/configuration Every field of the Go seller SDK's Config: usage keys, signing secrets and rotation, body limits, the allowance outage policy, and timeouts. `sellers.New(sellers.Config{…})` builds the client. The config requires a usage key (or a both-scopes `APIKey`) and exactly one of `SigningSecrets` or `Resolve`. Everything else has a working default. `New` returns an error rather than defaulting past a bad config. A seller booting with no signing secret would accept nothing, and failing at startup is far cheaper than discovering that in traffic. `New` refuses a config that leaves either usage direction uncovered, sets both or neither of `SigningSecrets` and `Resolve`, contains an empty secret, or names an unknown `Policy`. ```go theme={null} seller, err := sellers.New(sellers.Config{ APIKey: os.Getenv("ZEROCLICK_API_KEY"), ServiceSlug: "product-watch", SigningSecrets: map[string]string{ "hsec_k5nq0v7m3d8p": os.Getenv("ZEROCLICK_SIGNING_SECRET"), }, Logger: log.Default(), }) if err != nil { log.Fatal(err) } ``` The client is safe for concurrent use; build it once at startup. ## Usage keys A single API key (`zc_…`) carrying both the `usage:read` and `usage:write` scopes. It backfills whichever scoped key below is not set. A key with the `usage:read` scope, used for allowance checks (`Guard`, `CheckAllowance`). Falls back to `APIKey` when empty. A key with the `usage:write` scope, used for usage reporting (`ReportUsage`), which often runs in a separate process such as a worker. Falls back to `APIKey` when empty. Provide `APIKey` alone, or provide the scoped keys and omit it. If either direction ends up with no key, `New` returns an error naming the missing side. ```go theme={null} // Least privilege: split keys, no both-scopes credential. seller, err := sellers.New(sellers.Config{ UsageReadKey: os.Getenv("ZEROCLICK_USAGE_READ_KEY"), UsageWriteKey: os.Getenv("ZEROCLICK_USAGE_WRITE_KEY"), ServiceSlug: "product-watch", SigningSecrets: map[string]string{ "hsec_k5nq0v7m3d8p": os.Getenv("ZEROCLICK_SIGNING_SECRET"), }, }) ``` See [keys and secrets](/integrate/keys-and-secrets) for how the scopes map to the ZeroClick API. ## Service and plan The service these endpoints sell, as configured in ZeroClick. Required by the `Meter` and `Identify` middleware, which read it from the client. `Guard` takes a service slug per call instead, so a multi-service backend can share one client. Optional. When set, the SDK offers this plan on the `402 payment_required` response, steering the buyer toward it. ## Signing secrets Provide exactly one of `SigningSecrets` or `Resolve`. A static map from key id (`kid`, format `hsec_…`) to secret value (`zcsec_…`). The kid appears in every request's `zc-signature` header, and the verifier uses it to select the secret. The map keys must match the key ids shown in your dashboard exactly. The client copies the map at construction; a later mutation of your map does not change which requests the client accepts. A per-kid lookup for secrets that live in a vault or database rather than a static map: ```go theme={null} type ResolveSigningSecret func(kid string) (secret string, ok bool, err error) ``` For a kid you do not recognize, return `ok == false`; the SDK refuses the request with a `401`. Return an error only when the lookup itself failed (the vault was unreachable). It surfaces as an error to your code, not as a refusal, so an infrastructure fault is never silently read as a forged request. Because the client consults `Resolve` on each request, rotated secrets take effect without a restart. ### Loading from the environment `SecretsFromEnv()` builds the map from the `ZEROCLICK_SIGNING_SECRETS` environment variable, which holds `:` pairs, comma-separated. The format carries the key id, so there is nothing to hardcode: ```sh theme={null} ZEROCLICK_SIGNING_SECRETS="hsec_k5nq0v7m3d8p:zcsec_…" ``` ```go theme={null} secrets, err := sellers.SecretsFromEnv() if err != nil { log.Fatal(err) } ``` `ParseSigningSecrets(raw string)` parses the same format from any source, for sellers reading the value from a secret manager instead of the environment. Each entry splits on the first colon only, because secrets may contain colons. Error messages never echo the value, so a config mistake does not put a secret in your logs. ### Rotation Hold both secrets while requests signed with either are still in flight, then drop the old one: ```go theme={null} SigningSecrets: map[string]string{ "hsec_k5nq0v7m3d8p": os.Getenv("ZEROCLICK_SIGNING_SECRET_CURRENT"), "hsec_w2j9r4t8b6mx": os.Getenv("ZEROCLICK_SIGNING_SECRET_PREVIOUS"), }, ``` Or, in the one-variable form: ```sh theme={null} ZEROCLICK_SIGNING_SECRETS="hsec_k5nq0v7m3d8p:zcsec_…,hsec_w2j9r4t8b6mx:zcsec_…" ``` Each request names its kid, so the verifier always picks the right secret. Rotation requires no downtime. ## Request body limit Caps what a guarded endpoint will buffer. Zero means `DefaultMaxBodyBytes` (10 MiB). The signature covers the whole body, so the SDK must hold the whole body in memory to verify it. An attacker does not need a valid signature to make you buffer. This is a memory bound against unauthenticated callers, not a politeness limit. The SDK refuses a request over the cap with `413 {"error":"request_body_too_large"}` before verification. ## Allowance outage policy If the allowance API gives **no answer** (a timeout, a transport failure, a 5xx), `Policy` decides what happens. This is separate from the API answering *no*, which is always a `402`, and from your own misconfiguration, which is always an error. See [Go SDK errors](/sdks/go/errors) for the exact classification. | Policy | Behavior | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | | `PolicyAllow` (default) | Serve the request. A ZeroClick outage should not take your API down with it. | | `PolicyDeny` | Refuse with `503 {"error":"allowance_unavailable"}`. Choose this when unbilled work costs more than a failed request. | | `PolicyThrow` | Surface the error and decide yourself. In the middleware, this becomes a `500`. | The guard consults the policy only after a signature verifies, so a fail-open allowance policy never becomes a fail-open signature policy. But every legitimate buyer request is signed, so `PolicyAllow` bounds free work to real traffic rather than to a small subset. Set `Logger` or `OnAllowanceUnavailable`, because a fail-open that is also silent is invisible unbilled traffic. Bounds each allowance check (and each `ReportUsage` call). It sits in front of every billable request, so it is deliberately short: under the outage policy, a slow answer is worse than no answer. Called with the underlying error before the policy applies, so you can alarm on an outage you are choosing to absorb. ## Logging Receives conditions the SDK absorbs rather than surfaces: an allowance outage served under the allow policy, a guard failure the middleware turns into a `500`, a usage report that failed after the response was delivered. `*log.Logger` satisfies it, so `Logger: log.Default()` works. Nil discards these conditions, which makes unbilled traffic invisible; set it. ## Signature verification How far a signature's timestamp may drift from your clock, in either direction, before the SDK refuses the request as stale. See the [signature spec](/integrate/signature-spec) for what the timestamp protects against. ## Plumbing Base URL for the allowance and usage endpoints. Leave it unset in production. The client used for ZeroClick API calls. Defaults to a plain `&http.Client{}`; per-call timeouts come from `CheckTimeout`. Clock override so tests can pin time. Defaults to `time.Now`. ## Defaults at a glance | Constant | Value | | ------------------------- | --------------------------- | | `DefaultAPIBaseURL` | `https://api.zeroclick.io` | | `DefaultToleranceSeconds` | `300` | | `DefaultCheckTimeout` | `1500 * time.Millisecond` | | `DefaultMaxBodyBytes` | `10 << 20` (10 MiB) | | `SigningSecretsEnv` | `ZEROCLICK_SIGNING_SECRETS` | | Default `Policy` | `PolicyAllow` | # Go SDK errors Source: https://docs.zeroclick.ai/sdks/go/errors How the Go seller SDK separates decisions from errors: the Error type, error codes, verification failure reasons, and outage classification. The Go SDK splits outcomes into decisions and errors, and the split is load-bearing. * **Decisions** are values. An unsigned request, a bad signature, a buyer who cannot pay: these are expected events on a public endpoint, not faults in your program. They come back as results (`VerifyResult`, `GuardResult`, `AllowanceDecision`) carrying the ready-to-return `Response`. Nothing to catch, nothing to recover. * **Errors** are reserved for what you got wrong (malformed input, bad configuration) and what the environment did (the ZeroClick API unreachable or answering nonsense). They are `*sellers.Error` values with a stable machine-readable code. This page covers the error side; the decision flow is on the [API reference](/sdks/go/api). ## The Error type ```go theme={null} type Error struct { Code ErrorCode // stable classification; switch on this, not the message Operation string // the SDK call that failed: "new", "guard", "guard_identity", // "check_allowance", "report_usage" Status int // HTTP status from the ZeroClick API, or 0 if no response arrived Reason string // the API's own denial code, when it gave one Cause error // the underlying transport or decoding failure, if any } func (e *Error) Error() string func (e *Error) Unwrap() error ``` The message renders every populated field, for example: ```text theme={null} zeroclick: api_status_error during report_usage (status 402) (reason usage_exhausted) ``` `Unwrap` returns `Cause`, so `errors.Is` and `errors.As` see through to the underlying failure. ## Error codes The core package defines four codes: | Code | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `malformed_input` | You handed the SDK something invalid: a config `New` refuses, a usage list with a duplicate meter or with both `Quantity` and `MaxQuantity` set, a missing service slug or idempotency key. `Cause` explains what. | | `api_transport_error` | The call to the ZeroClick API never completed: timeout, DNS failure, connection refused, cancelled context. `Cause` holds the transport error. | | `api_status_error` | The API answered with a 4xx or 5xx. `Status` holds the code. For usage reports refused with `402`, `404`, or `409`, `Reason` carries the API's machine code: `402` for `access_inactive`, `plan_expired`, or `usage_exhausted`; `409` for `meter_not_priced`; otherwise `404`. | | `api_response_invalid` | The API answered, but with a body the SDK could not read as the documented shape: unparseable JSON, a missing field, an unrecognized error code on a typed status. | See [errors](/resources/errors) for the control-plane error envelope these map from. ### jwe error codes The `jwe` subpackage (encrypted request bodies) adds its own codes, on the same `*sellers.Error` type with `Operation` set to `decrypt_request` or `encrypt_response`: | Code | Meaning | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_compact_jwe` | The body is not a well-formed five-segment Compact JWE: wrong segment count, an empty segment, an undecodable protected header, or a parse failure. | | `unsupported_jwe_suite` | The header names an algorithm outside the pinned suite (ECDH-ES+A256KW key management, A256GCM content encryption). | | `jwe_kid_required` | The protected header carries no `kid`, so no private key can be selected. | | `invalid_reply_jwk` | The buyer's reply key is malformed: not an EC P-256 public key, unexpected members, or bad coordinates. | | `private_reply_jwk` | The reply key carries private key material. A protocol violation, refused outright rather than quietly used. | | `private_key_not_found` | Your resolver does not know the header's `kid`. `Reason` carries the kid. | | `private_key_resolution_failed` | The private-key lookup itself failed: a vault error, not a missing key. `Reason` carries the kid. | | `decryption_failed` | The suite and key were right, but decryption failed. `Reason` carries the kid. Also returned for an encrypted request whose plaintext is empty, a known Go JOSE-library limitation; see the [API reference](/sdks/go/api). | | `encryption_failed` | Encrypting the reply to the buyer's key failed. | The SDK reports a nil private-key resolver as the core `malformed_input`. ## Verification refusals are decisions, not errors When a signature does not verify, `Verify` and `Guard` return a deny decision, not an error. `Reason` carries a `FailureReason` value saying why: | Reason | The request was refused because | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `missing_signature` | No `zc-signature` header at all, which is typical for traffic that did not come through ZeroClick. | | `malformed_signature` | The header does not parse as `t=…,kid=…,v1=…`: a missing or duplicate member, a malformed timestamp, or a value that is not 64 lowercase hex characters. | | `stale_timestamp` | The signature's timestamp drifted more than `ToleranceSeconds` (default 300) from your clock, in either direction. | | `missing_request_id` | The `zc-request-id` header is absent. | | `unknown_kid` | Your secrets hold no entry for the signature's key id: commonly a secret rotated away too early, or an environment mismatch. | | `invalid_signature` | The HMAC did not match: the body, path, method, or ids differ from what ZeroClick signed, or the secret is wrong. A proxy that rewrites the raw request target is a common cause; see [Go SDK middleware](/sdks/go/middleware). | Every one of these surfaces to the caller as the same `401 {"error":"invalid_zeroclick_signature"}` response. The body deliberately says nothing about why. The specific reason rides on `VerifyResult.Reason` and `GuardResult.Reason` for your logs. One verification path does produce an error rather than a refusal: a signing-secret **resolver fault**. If your `Resolve` function fails (a vault unreachable) or resolves an empty secret, `Verify` returns an error wrapping `ErrSecretResolution`. An infrastructure fault must not be silently read as a forged request: ```go theme={null} var ErrSecretResolution = errors.New("zeroclick: signing secret could not be resolved") ``` Test for it with `errors.Is(err, sellers.ErrSecretResolution)`. An *unknown* kid, by contrast, is an ordinary `unknown_kid` refusal. ## Allowance denials are decisions too `allowed: false` from the allowance API is always a `402` decision, never an error: `Guard` returns a deny result carrying the priced `payment_required` response, with `Reason` set to the denial code. The SDK exports the codes as the `Denial*` constants: `service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, `usage_exhausted`. See [usage and allowances](/concepts/usage-and-allowances) for what each means. The SDK still honors a denial whose reason it does not recognize: the decision stands, and the unfamiliar reason passes through for you to log. ## Outage classification `IsAllowanceUnavailable` answers one question: did the allowance API give **no answer**? Only that condition is subject to the configured outage `Policy` (see [configuration](/sdks/go/configuration)). Everything else fails loudly, because conflating the cases all failed in the same direction: serving unbilled work. ```go theme={null} func IsAllowanceUnavailable(err error) bool ``` | Condition | Classification | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Timeout, DNS failure, connection refused (`api_transport_error`) | No answer; the policy applies. | | `5xx`, `429`, or `408` from the API (`api_status_error`) | No answer: transient, genuinely worth failing open on. The policy applies. | | A response the SDK cannot read (`api_response_invalid`) | No answer: an answer that cannot be understood is no answer. The policy applies. | | Any other `4xx` (`api_status_error`): a revoked or wrong-environment API key, a suspended account, an unknown service | **Always an error.** Permanent, never self-heals, and every second of failing open makes the loss larger. | | `malformed_input` | **Always an error.** Your bug, not an outage. | | A cancelled context (`errors.Is(err, context.Canceled)`, at any depth) | **Not an outage.** The caller went away; authorizing work because nobody is listening is backwards. `Guard` runs the check on its own timeout regardless, so its real answer stands. | A readable decision with an unfamiliar reason never reaches this classification at all: the SDK honors it as a `402`, per the previous section. The guard consults the policy only after a signature verifies, so a fail-open allowance policy never becomes a fail-open signature policy. ## Patterns Inspect any SDK error by code: ```go theme={null} var zcErr *sellers.Error if errors.As(err, &zcErr) { log.Printf("zeroclick %s failed: code=%s status=%d reason=%s", zcErr.Operation, zcErr.Code, zcErr.Status, zcErr.Reason) } ``` `Unwrap` lets `errors.Is` reach the cause: ```go theme={null} if errors.Is(err, context.DeadlineExceeded) { // The check timeout (default 1.5 s) elapsed. } if errors.Is(err, sellers.ErrSecretResolution) { // The signing-secret lookup failed: alert on infrastructure, not on buyers. } ``` When you drive the check yourself, classify before deciding. This is exactly what `Guard` does internally with `Config.Policy`: ```go theme={null} decision, err := seller.CheckAllowance(ctx, zc.RequestID, "product-watch", []sellers.UsageItem{{MeterSlug: "requests", Quantity: 1}}) switch { case err == nil: // A real answer: decision.Allowed, decision.Reason. case sellers.IsAllowanceUnavailable(err): // No answer. Apply your own outage policy. default: // Misconfiguration or a permanent API refusal. Fix it; do not fail open. log.Printf("allowance check broken: %v", err) } ``` A failed `ReportUsage` deserves the same care in the other direction: the buyer already has their result. Log the error with its `Reason`, and retry later with the **same derived idempotency key** rather than failing the delivered request. A replay is a harmless `Duplicate: true`. See [settle usage](/integrate/settle-usage). # Go SDK middleware Source: https://docs.zeroclick.ai/sdks/go/middleware What the Go SDK's Meter and Identify middleware do on every request: body handling, 2xx-only settlement, streaming support, and failure modes. The Go SDK packages the whole guard flow (verify, check allowance, serve, settle) as `net/http` middleware. `Meter` guards a billable endpoint; `Identify` guards a free endpoint that must still know which buyer is calling. Both return a plain `func(http.Handler) http.Handler`, the standard middleware shape, so they compose with `net/http`, chi, gorilla/mux, and anything else built on `http.Handler`. ```go theme={null} // Billable: one fixed charge per request. mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(productWatch))) // Free, but identity-scoped: the caller must prove which buyer they are. mux.Handle("/v1/limits", seller.Identify()(http.HandlerFunc(limits))) ``` `Meter` needs `ServiceSlug` set on the client's [Config](/sdks/go/configuration); it reads the service, plan, and body limit from there. ## What Meter does on every request In order: it reads the request body under the configured cap, verifies the `zc-signature` header over the raw bytes, and checks the buyer's allowance for the declared usage. Only then does it call your handler, with the body restored and the buyer in the request context. When your handler responds 2xx, it attaches the `zc-usage` header so the request settles. | Behavior | Why it matters | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Restores `r.Body` after verification | The signature covers the raw bytes, so verifying consumes the body. Without this, your handler reads empty, and does so silently. | | `zc-usage` only on a 2xx | A 4xx is the buyer's bad input and a 5xx is your failure. Neither should bill. | | Bounds the body (`MaxBodyBytes`, default 10 MiB) | Verification needs the whole body in memory, and an attacker does not need a valid signature to make you buffer it. Over the cap, the middleware refuses the request with `413 {"error":"request_body_too_large"}` before verification. | | Puts the buyer in the request context | `sellers.FromContext(r.Context())` returns the verified `AgentID` and `RequestID`. | | Refuses before your handler runs | The middleware refuses an unsigned request before it ever consults the allowance API, and refuses an unpayable one before your code runs. Unpayable traffic never costs you work. | A refusal is the exact response ZeroClick expects: `401 {"error":"invalid_zeroclick_signature"}` for a failed verification, the priced `402 payment_required` body for an allowance denial, or `503 {"error":"allowance_unavailable"}` under a fail-closed outage policy. See [integrate your API](/integrate/overview) for the bodies and when each one is returned. ## Fixed quantities only `Meter` settles exactly what it declares, so it takes fixed quantities only: `sellers.PerRequest(meter, n)` items. If handed an `UpTo` item or a non-positive quantity, it **panics at wire-up**. A ceiling has no settled quantity: it would bill zero, silently, on a delivered 200, which is the exact failure this SDK exists to prevent. The panic happens when you build the middleware, not in production traffic. For work you cannot size up front, declare the fixed part to `Meter` and report the variable part after responding. ## Billing for work you can't size up front Declare a fixed charge to the guard: that is what lets ZeroClick offer the `exact` payment scheme, which is the one clients can pay today. Then report the variable part with `ReportUsage` after the response has gone out. This example charges one `requests` unit per call synchronously and reports `output_tokens` afterwards: ```go theme={null} handler := func(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) tokens := 1842 // discovered by doing the work w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"outputTokens":%d}`, tokens) // Reported AFTER the response. Two things matter here: // // 1. BackgroundContext, not r.Context(): the request context is // already cancelled, so the report would be silently dropped and // the tokens would go unbilled. // 2. An idempotency key derived from the request id, never random: // a retried report with a fresh key double-bills. _, err := seller.ReportUsage(sellers.BackgroundContext(r), sellers.ReportUsageInput{ AgentID: zc.AgentID, ServiceSlug: "product-watch", MeterSlug: "output_tokens", Quantity: tokens, IdempotencyKey: zc.RequestID + "_output_tokens", }) if err != nil { // Delivered but unbilled. Log it; do not fail a request the // buyer already has. log.Printf("output_tokens not reported: %v", err) } } http.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(handler))) ``` Two things are easy to get wrong: Use `sellers.BackgroundContext(r)`, not `r.Context()`. The server cancels the request context the moment your handler returns, so a report on it is dropped and the work goes unbilled with nothing in the logs. Derive the idempotency key: `zc.RequestID + "_output_tokens"`, never a random value. A random key double-bills on retry; a derived key makes the retry a harmless duplicate. To let the buyer authorize a variable amount up front instead, see [charge up to a maximum](/integrate/charge-up-to-a-maximum). That pattern uses `Guard` directly rather than `Meter`. ## Settling on 2xx only The middleware wraps your response writer and defers the `zc-usage` header until the status is known, because only a delivered response should be billed. A 4xx means you rejected the buyer's input, and a 5xx is your own failure. Charging for either bills someone for work they did not receive. Two edge cases the wrapper handles for you: * **A handler that writes nothing** still delivers a 200 (`net/http` emits it at the transport layer, below the wrapper), so the middleware settles it explicitly rather than letting the work ship unbilled. * **A handler that sets its own `zc-usage` header** wins: the middleware attaches the declared usage only when the header is not already present, so you can override the settled quantities for one response when you must. ## Streaming and WebSockets The wrapped writer implements `http.Flusher`, `http.Hijacker`, and `Unwrap` (for `http.ResponseController`), so streaming handlers work unchanged: * **Server-sent events**: a direct `w.(http.Flusher)` assertion, the long-standing streaming idiom, succeeds. `Flush` settles the usage header before the first byte leaves, because headers cannot follow the body. * **WebSocket upgrades**: `Hijack` works, but a hijacked connection leaves HTTP response semantics behind, so no `zc-usage` header can follow it. Meter the connection itself as the fixed charge and report anything per-message with `ReportUsage`. ## Identify `Identify` guards a free endpoint that must still know which buyer is calling, such as a limits or account route. It bounds and restores the body and verifies the signature exactly as `Meter` does, but makes no network call and settles nothing. A signed request with no `zc-agent-id` header is an anonymous probe: valid, but it has not established a buyer. `Identify` answers it with a `402` carrying an empty usage list. That makes ZeroClick issue a \$0 identity challenge and retry the request with the identity attached. Your handler then finds the buyer with `sellers.FromContext`: ```go theme={null} func limits(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) w.Header().Set("content-type", "application/json") json.NewEncoder(w).Encode(map[string]any{"zcAgentId": zc.AgentID}) } ``` See [free and identity endpoints](/integrate/free-and-identity-endpoints) for when to use this over an unguarded route. ## Failure modes | Condition | Response | | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Body exceeds `MaxBodyBytes`, or the client hung up mid-body | `413 {"error":"request_body_too_large"}`. The SDK cannot verify a signature over a body the server does not fully have. | | Signature missing, malformed, stale, or wrong | `401 {"error":"invalid_zeroclick_signature"}` | | Allowance denied | The priced `402 payment_required` body. | | Allowance API unreachable | Governed by `Policy`: served (default), `503 {"error":"allowance_unavailable"}`, or treated as an error. | | Guard returned an error: a malformed usage list, a signing-secret resolver fault, or `PolicyThrow` surfacing an outage | `500 {"error":"internal_error"}`, with the underlying error written to the configured `Logger`. Neither is the buyer's fault, and neither bills. | The full split between decisions and errors is on the [errors page](/sdks/go/errors). ## Deploying behind a proxy The signature covers the raw, percent-encoded request target. The SDK reads `r.RequestURI`, which Go leaves untouched. But a reverse proxy, ingress, or load balancer in front of you may normalize the path (`%2F` becoming `/`) before your server sees it. If that happens, every request with an encoded path segment fails verification. If you route through nginx, an ingress controller, or a managed load balancer, confirm it passes the request target through unmodified. The [signature spec](/integrate/signature-spec) specifies the exact bytes the signature covers. # Go SDK quickstart Source: https://docs.zeroclick.ai/sdks/go/quickstart Install the ZeroClick Go seller SDK and guard a net/http route with one middleware: verify the signature, check the allowance, and settle usage. The Go seller SDK ([`cdn.zeroclick.io/sdks/sellers-go`](https://pkg.go.dev/cdn.zeroclick.io/sdks/sellers-go)) implements the ZeroClick billing guard for Go backends. It verifies that a request really came from ZeroClick and checks that the buyer can pay before your handler runs. It returns the refusals ZeroClick expects and reports what was used. The usual integration is one middleware on each billable route. This page takes a `net/http` service from `go get` to guarded. The [quickstart](/quickstart) covers the full setup, including the dashboard side, and [integrate your API](/integrate/overview) describes the contract the SDK implements. ## Install ```sh theme={null} go get cdn.zeroclick.io/sdks/sellers-go ``` Requires Go 1.24 or newer. The core package imports only the standard library. The SDK's one third-party dependency (`go-jose`) belongs to the optional `jwe` subpackage for encrypted request bodies. Unless you import that package, Go's module graph pruning keeps it out of your `go.sum` and your binary. ## What you need first Two credentials from your [dashboard](https://dashboard.zeroclick.io): * A **signing secret**: a secret value (`zcsec_…`) and its key id (`hsec_…`), called the `kid`. The SDK uses it to verify the `zc-signature` header on every forwarded request. * An **API key** (`zc_…`) with the `usage:read` and `usage:write` scopes. The read scope covers allowance checks; the write scope covers usage reporting. `APIKey` carries both scopes. To follow least privilege, pass two scoped keys instead and omit `APIKey`: `UsageReadKey` for allowance checks and `UsageWriteKey` for usage reporting. The split is handy when a separate worker reports usage. A scoped key falls back to `APIKey` when it is not set, so either form works. See [keys and secrets](/integrate/keys-and-secrets) for scopes and rotation. The dashboard shows each secret once, when you create it. `SecretsFromEnv()` reads signing secrets from one environment variable of `:` pairs, comma-separated: ```sh theme={null} export ZEROCLICK_SIGNING_SECRETS="hsec_k5nq0v7m3d8p:zcsec_…" export ZEROCLICK_API_KEY="zc_…" ``` ## The whole integration This example guards `/v1/product-watch` for a service `product-watch` with a meter `requests`, and leaves `/health` open. Substitute your own slugs. ```go main.go theme={null} package main import ( "encoding/json" "io" "log" "net/http" "os" sellers "cdn.zeroclick.io/sdks/sellers-go" ) func main() { secrets, err := sellers.SecretsFromEnv() // ZEROCLICK_SIGNING_SECRETS if err != nil { log.Fatal(err) } seller, err := sellers.New(sellers.Config{ APIKey: os.Getenv("ZEROCLICK_API_KEY"), ServiceSlug: "product-watch", SigningSecrets: secrets, Logger: log.Default(), }) if err != nil { log.Fatal(err) } mux := http.NewServeMux() // Unguarded: the platform's health probe is not a buyer. mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"status":"ok"}`)) }) // Billable. One fixed charge per request. mux.Handle("/v1/product-watch", seller.Meter(sellers.PerRequest("requests", 1))( http.HandlerFunc(productWatch))) log.Fatal(http.ListenAndServe(":8080", mux)) } func productWatch(w http.ResponseWriter, r *http.Request) { zc, _ := sellers.FromContext(r.Context()) // zc.AgentID, zc.RequestID body, _ := io.ReadAll(r.Body) // still readable log.Printf("watching %d bytes for %s", len(body), zc.AgentID) w.Header().Set("content-type", "application/json") json.NewEncoder(w).Encode(map[string]any{"productCount": 3}) } ``` On each request, `Meter` verifies the `zc-signature` header over the raw bytes, checks that the buyer's allowance covers one `requests` unit, and only then calls your handler. When a request fails either step, the middleware refuses it before your code runs, with the exact response ZeroClick expects: a `401` for a bad signature, a priced `402` for a business denial. When the handler responds 2xx, the middleware attaches the `zc-usage` header so the request settles. A 4xx or 5xx bills nothing. Inside the handler, `r.Body` reads normally: verification consumed it, and the middleware put it back. `sellers.FromContext` returns the verified buyer: `AgentID` (`agt_…`) and `RequestID` (`zcreq_…`), the id that correlates this request across ZeroClick's logs and your own. Test the guard locally: it must refuse a plain request with no ZeroClick headers. ```sh theme={null} curl -i localhost:8080/v1/product-watch -d '{}' ``` ```http theme={null} HTTP/1.1 401 Unauthorized Content-Type: application/json {"error":"invalid_zeroclick_signature"} ``` Only signed requests from ZeroClick reach your handler. To see paid traffic end to end (the `402` challenge, the payment, the forwarded request), deploy the route where ZeroClick can reach it and follow the verification step in the [quickstart](/quickstart). ## Framework fit `Meter` (and its free counterpart `Identify`) returns a plain `func(http.Handler) http.Handler`, the standard middleware shape, so it composes with any router built on `net/http`: | Framework | Wire-up | | ----------- | -------------------------------------------------------------------- | | `net/http` | `mux.Handle("/v1/product-watch", seller.Meter(…)(handler))` | | chi | `r.Use(seller.Meter(…))` | | gorilla/mux | `r.Use(seller.Meter(…))` | | Echo | `echo.WrapMiddleware(seller.Meter(…))` | | Gin | Wrap the guarded handler with `gin.WrapH` inside a `gin.HandlerFunc` | | Fiber | Not supported. Fiber runs on fasthttp, which has no `http.Handler` | Apply the middleware per billable route, not globally: health probes and other free endpoints should stay unguarded, and each metered route declares its own charge. For a backend not built on `net/http` at all, every decision also flows through the SDK's framework-neutral `Request` and `Response` types. See the [API reference](/sdks/go/api). ## Next steps What Meter and Identify do on every request, streaming support, and variable usage. Every Config field: keys, signing secrets, body limits, and the outage policy. The full public surface, including Guard and the framework-neutral types. Decisions versus errors, error codes, and outage classification. # Seller SDKs Source: https://docs.zeroclick.ai/sdks/overview Official ZeroClick seller SDKs for TypeScript, Python, and Go: install, shared behavior, and how to choose. The seller SDKs implement the ZeroClick billing guard in your backend: they verify that a request really came from ZeroClick, check that the buyer can pay before you do the work, build the refusal responses ZeroClick expects, and report what was used. All three speak the same wire protocol and make the same decisions; they differ only in language idiom. | SDK | Package | Install | Requires | | ---------- | ----------------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | TypeScript | [`@zeroclickai/sellers`](https://www.npmjs.com/package/@zeroclickai/sellers) | `pnpm add @zeroclickai/sellers` | ESM; web-standard `Request`, `Response`, `fetch`, Web Crypto (current Node.js and compatible edge runtimes) | | Python | [`zeroclick-sellers`](https://pypi.org/project/zeroclick-sellers/) | `pip install zeroclick-sellers` | Python 3.10+ | | Go | [`cdn.zeroclick.io/sdks/sellers-go`](https://pkg.go.dev/cdn.zeroclick.io/sdks/sellers-go) | `go get cdn.zeroclick.io/sdks/sellers-go` | Go 1.24+ | All three are Apache-2.0 licensed. ## What every SDK does * **Verify first.** Every entry point verifies the `zc-signature` header before anything else. The signature is an HMAC-SHA256 over the raw request bytes. The SDK never checks an allowance for an unverified request. * **Decisions, not exceptions.** A guard returns an allow decision with the verified buyer context, or a deny decision that carries the exact response to return: `401` for a bad signature, the `402 payment_required` refusal for a business denial, or `503` under a fail-closed outage policy. Malformed configuration and API failures raise typed errors instead. * **Fail open by default.** If the allowance check fails without a usable answer, the configurable outage policy decides: `allow` (default), `deny`, or `throw`. A recognized `allowed: false` is always a `402`. The policy applies only after a signature verifies, so an unverified request is never served because the allowance API was down. The SDKs differ in how strictly they classify failures. The Go SDK treats any 4xx from the allowance API (a revoked key, an unknown service) as a hard error, never an outage. The TypeScript and Python SDKs route API errors through the policy. Each SDK's errors page documents its exact classification. * **Settle on success only.** Synchronous usage rides the `zc-usage` response header on 2xx responses; asynchronous usage goes through `reportUsage` with a seller-owned, derived idempotency key. No SDK retries automatically or invents idempotency keys. * **Split usage keys.** Pass one API key with both usage scopes, or a read key for guards and checks plus a write key for reporting. The split is useful when a separate worker reports usage. ## Differences that matter | | TypeScript | Python | Go | | -------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | | Integration shape | Web-standard `Request` in, `Response` out; adapt your framework at the boundary | ASGI and WSGI adapters plus sync and async clients | Drop-in `net/http` middleware (`Meter`, `Identify`) | | Framework fit | Hono, Next.js route handlers, edge runtimes, anything web-native | FastAPI and Starlette (async); Flask and Django (sync) | `net/http`, chi, gorilla/mux directly; Echo and Gin via adapters; Fiber unsupported | | Concurrency model | async (Promises) | both: `create_seller` and `create_async_seller` | synchronous, context-aware | | Runtime validation schemas | Zod schemas via `@zeroclickai/sellers/contracts` | frozen dataclasses with validation | typed structs | | Encryption helpers | `@zeroclickai/sellers/encryption` subpath | top-level functions | separate `jwe` subpackage, so the core stays stdlib-only | | Secret loading helper | None | None | `SecretsFromEnv()` parses `ZEROCLICK_SIGNING_SECRETS` | Pick the SDK that matches your backend language. The core guard flow has the same capabilities in all three. If your backend is in another language entirely, the [REST walkthrough](/integrate/rest-walkthrough) implements the same contract with plain HTTP. ## Get started Web-native guard for Node.js and edge runtimes. Sync and async clients with ASGI and WSGI adapters. Standard middleware for net/http and friends. # Python SDK API reference Source: https://docs.zeroclick.ai/sdks/python/api Every public export of zeroclick-sellers: client methods, decision and data types, response builders, adapters, module defaults, and encryption helpers. Everything on this page imports from `zeroclick_sellers`, except the framework adapters, which live in `zeroclick_sellers.adapters`. See [configuration](/sdks/python/configuration) for constructor options and [errors](/sdks/python/errors) for error codes. `create_seller(**options)` returns a `SellerClient` (blocking, for WSGI); `create_async_seller(**options)` returns an `AsyncSellerClient` (non-blocking, for ASGI). The SDK exports both classes for type annotations. The methods below have identical signatures and semantics on both clients; on `AsyncSellerClient`, `guard`, `check_allowance`, and `report_usage` are coroutines you `await`. `guard_identity` and `verify_request` make no network call, so they are synchronous on both clients. ## Guard methods ### guard ```python theme={null} def guard( request: ZcRequest, *, service_slug: str, usage: Iterable[UsageItem], plan_slug: str | None = None, ) -> GuardResult ``` The whole billing guard in one call. It verifies the `zc-signature` header first, and only then checks the declared usage against the buyer's allowance with `POST /v1/usage/check`. The SDK never consults the allowance API for an unverified request. The result is a decision, not an exception: * **Deny, `401`**: a missing, malformed, stale, or invalid signature. * **Deny, `402`**: the allowance API answered `allowed: false`. The response is the exact `payment_required` body ZeroClick converts into a payment challenge, built from `service_slug`, `usage`, and `plan_slug`. * **Deny, `503`**: the allowance API gave no answer and the client's policy is `"deny"`. * **Allow**: carries the verified `context` and an `allowance` of `"allowed"` or `"unavailable"`. `usage` must contain at least one `UsageItem` and no duplicate meters; the allowance API accepts at most 20 items per check. When set, `plan_slug` appears in a denial's `402` body as `planSlug`. `guard` raises `ZCError` only for malformed input, a failing secret resolver, or an allowance outage under the `"throw"` policy. ```python theme={null} decision = await zeroclick.guard( # no await on the sync client zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) result = do_the_work(owner=decision.context.zc_agent_id) ``` ### guard\_identity ```python theme={null} def guard_identity(request: ZcRequest, *, service_slug: str) -> GuardResult ``` Guards a [free, identity-scoped endpoint](/integrate/free-and-identity-endpoints): one that costs nothing but must know which buyer is calling. It verifies the signature exactly like `guard`, makes no allowance call (so it is synchronous on both clients), and: * denies a bad signature with the `401` response, * denies a verified request without a buyer identity (reason `identity_required`) with the `402 payment_required` body carrying `usage: []`, which ZeroClick answers with a free identity challenge, * allows an identified buyer with `allowance == "not_required"`, and in that case `decision.context.zc_agent_id` is always non-empty. ```python theme={null} decision = zeroclick.guard_identity(zc_request, service_slug="product-watch") if decision.action == "deny": return to_fastapi(decision.response) owner = decision.context.zc_agent_id ``` ### verify\_request ```python theme={null} def verify_request(request: ZcRequest) -> VerifyResult ``` Signature verification alone, using the client's secrets, tolerance, and clock. This is the first half of `guard`, for when you want to run the allowance check separately or not at all. It returns `VerifyOk` with the proven context, or `VerifyFailure` with a `reason` and the ready-to-return `401` response. The failure reasons are `missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, and `invalid_signature`; every one maps to the same `401 {"error":"invalid_zeroclick_signature"}` body, so the reason is for your logs, not the caller. The HMAC comparison is constant-time. ```python theme={null} verification = zeroclick.verify_request(zc_request) if not verification.ok: return to_fastapi(verification.response) context = verification.context # zc_request_id, zc_agent_id, timestamp, kid ``` `verify_request` is also a standalone function for use without a client. It takes the same request plus its own configuration. Provide exactly one of `signing_secrets` or `resolve_signing_secret`, and optional `tolerance_seconds` (default 300) and `clock`: ```python theme={null} from zeroclick_sellers import verify_request verification = verify_request( zc_request, signing_secrets={"hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET"]}, ) ``` ## Usage API methods ### check\_allowance ```python theme={null} def check_allowance( *, zc_request_id: str, service_slug: str, usage: Iterable[UsageItem] ) -> AllowanceDecision ``` The [allowance check](/integrate/check-allowances) alone, without verification or response-building. Use it for a second check partway through a multi-stage request, or when you have already verified. `zc_request_id` must be the id from the verified request's context. Checking records nothing and burns no credit. Unlike `guard`, any API failure raises `ZCError`: the outage policy applies only inside `guard`. ```python theme={null} decision = await zeroclick.check_allowance( zc_request_id=context.zc_request_id, service_slug="product-watch", usage=[UsageItem(meter_slug="output_tokens", max_quantity=100_000)], ) if not decision.allowed: ... # decision.reason, for example "usage_exhausted" ``` ### report\_usage ```python theme={null} def report_usage( *, zc_agent_id: str, idempotency_key: str, service_slug: str, meter_slug: str, quantity: int, occurred_at: str | None = None, ) -> ReportUsageResult ``` Reports usage asynchronously with `POST /v1/usage`, for work that finishes after the response has gone out. This is the alternative to [settling with the `zc-usage` header](/integrate/settle-usage). All arguments are keyword-only. `quantity` is an integer of at least 1 (at most 2,147,483,647); `occurred_at` is an optional ISO 8601 timestamp. Reporting is idempotent per service on `idempotency_key`. Derive the key from stable facts (`zcreq_8h2m4x0q9k1f_output_tokens`, not a random value) so a retry of your own job cannot double-bill. `report_usage` does not generate idempotency keys and does not retry. A result with `duplicate=True` means the key already landed and the API returned the stored event. That is a success, not an error. A rejected report raises `ZCError`; see [errors](/sdks/python/errors) for reading the status and reason. ```python theme={null} result = await zeroclick.report_usage( zc_agent_id="agt_x7f2kq93bh0d", idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens", service_slug="product-watch", meter_slug="output_tokens", quantity=4200, ) result.recorded # True result.duplicate # True when this key was already reported: still a success ``` ## Response builders ### payment\_required ```python theme={null} def payment_required( *, service_slug: str, usage: Iterable[UsageItem], plan_slug: str | None = None ) -> ZcResponse ``` Builds the exact `402` refusal ZeroClick converts into a payment challenge, the same response a `guard` denial carries. Use it to deny for business reasons of your own. An empty `usage` is meaningful, not a mistake: it is the free identity-scoped refusal, which ZeroClick answers with a \$0 identity challenge. It is available as a static method on both clients and as a top-level import. ```python theme={null} response = zeroclick.payment_required( service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) response.status # 402 response.json_body() # {"error": "payment_required", "serviceSlug": "product-watch", # "usage": [{"meterSlug": "requests", "quantity": 1}]} ``` ### with\_usage ```python theme={null} def with_usage(response: ZcResponse, usage: Iterable[SyncUsageItem]) -> ZcResponse ``` Returns a copy of the response with the `zc-usage` header attached: the synchronous settlement path. Attach it to `2xx` responses only; ZeroClick records the usage and strips the header before the agent sees the response. It is available as a static method on both clients and as a top-level import. ```python theme={null} stamped = zeroclick.with_usage( ZcResponse.json({"watching": True}), [SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1)], ) stamped.headers["zc-usage"] # '[{"serviceSlug":"product-watch","meterSlug":"requests","quantity":1}]' ``` ### Other builders * `usage_header(usage: Iterable[SyncUsageItem]) -> str`: the `zc-usage` header value alone, for stamping a framework response directly instead of going through `ZcResponse`. * `invalid_zeroclick_signature() -> ZcResponse`: the `401 {"error":"invalid_zeroclick_signature"}` response. Deny decisions already carry it; the export is for manual flows. * `allowance_unavailable() -> ZcResponse`: the `503 {"error":"allowance_unavailable"}` response the fail-closed outage policy uses. ## Request and response types All SDK types are frozen dataclasses: immutable after construction, validated in `__post_init__`, and comparable by value. ### ZcRequest ```python theme={null} @dataclass(frozen=True) class ZcRequest: method: str path_and_query: str headers: Mapping[str, str] = field(default_factory=dict) body: bytes = b"" ``` The framework-neutral view of an inbound request that every verification entry point consumes. Two rules matter: * `body` must be the raw bytes exactly as received. Passing anything else raises `ZCError` (`malformed_input`, "body must be bytes; decode nothing before verifying"). * `path_and_query` must be the raw, percent-encoded request target. Decoded paths fail verification; the [adapters](#framework-adapters) recover the raw target for you. The constructor lowercases header names, and `request.header(name)` looks up case-insensitively: ```python theme={null} zc_request = ZcRequest( method="POST", path_and_query="/v1/product-watch?fast=1", headers={"ZC-Request-Id": "zcreq_8h2m4x0q9k1f"}, body=b'{"productId":"prod_9d4k"}', ) zc_request.header("zc-request-id") # "zcreq_8h2m4x0q9k1f" ``` ### ZcResponse ```python theme={null} @dataclass(frozen=True) class ZcResponse: status: int body: bytes headers: Mapping[str, str] = field(default_factory=dict) ``` The framework-neutral response the SDK hands back; your adapter turns it into a real framework response. `ZcResponse.json(payload, *, status=200, headers=None)` builds one with compact JSON and a `content-type: application/json` header; `response.json_body()` parses the body back. ```python theme={null} response = ZcResponse.json({"watching": True}) response.status # 200 response.body # b'{"watching":true}' ``` ## Decision types ### GuardResult ```python theme={null} @dataclass(frozen=True) class Allow: context: ZeroClickContext allowance: Literal["allowed", "unavailable", "not_required"] action: Literal["allow"] = "allow" @dataclass(frozen=True) class Deny: reason: str response: ZcResponse action: Literal["deny"] = "deny" GuardResult = Allow | Deny ``` Discriminate on `action` (or match on the class). `Allow.allowance` tells you how the guard cleared the request: * `"allowed"`: the allowance API confirmed coverage. * `"unavailable"`: the allowance API gave no answer and the `"allow"` policy let the request through. * `"not_required"`: `guard_identity` allowed the request and made no allowance check. `Deny.reason` is one of the six verification failures (response `401`); one of the seven allowance denial reasons (`service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, `usage_exhausted`) or `allowance_denied` when the API denies without a reason (response `402`); `allowance_unavailable` under the fail-closed policy (response `503`); or `identity_required` from `guard_identity` (response `402`). `Deny.response` is always ready to return as-is. ### VerifyResult ```python theme={null} @dataclass(frozen=True) class VerifyOk: context: ZeroClickContext ok: Literal[True] = True @dataclass(frozen=True) class VerifyFailure: reason: VerifyFailureReason response: ZcResponse ok: Literal[False] = False VerifyResult = VerifyOk | VerifyFailure ``` `verify_request` returns this type. Discriminate on `ok`. ### ZeroClickContext ```python theme={null} @dataclass(frozen=True) class ZeroClickContext: zc_request_id: str zc_agent_id: str | None timestamp: int kid: str zc_anonymous_id: str | None = None zc_buyer_id: str | None = None ``` Proven facts about a verified request: the correlation id (`zcreq_…`), the id of the agent that made the call (`agt_…`), the signature's timestamp in Unix seconds, and the key id that signed it. `zc_anonymous_id` repeats `zc_agent_id` under the name that will eventually replace `zc-agent-id`. `zc_buyer_id` (`byr_…`) is the buyer that agent belongs to, and is `None` for an anonymous agent; unlike the fields above it is not covered by the signature. See [agents and access](/concepts/agents-and-access). A verified request without a buyer identity is a **signed anonymous probe**. ZeroClick forwards one to price a pay-as-you-go `402`, so it is valid, expected traffic. Test for it with truthiness, not `is None`: an absent `zc-agent-id` header gives `None`, but a present-but-empty one gives `""`, and both mean the same thing. ```python theme={null} if not decision.context.zc_agent_id: ... # anonymous ``` `guard_identity` already handles both. ### AllowanceDecision ```python theme={null} @dataclass(frozen=True) class AllowanceDecision: allowed: bool reason: str | None ``` `check_allowance` returns this type. `reason` is `None` when allowed, otherwise one of the seven allowance denial reasons. ### ReportUsageResult ```python theme={null} @dataclass(frozen=True) class ReportUsageResult: recorded: bool duplicate: bool usage_event: Mapping[str, Any] ``` `report_usage` returns this type. `usage_event` is the API's recorded event: `id`, `zcAgentId`, `zcRequestId`, `idempotencyKey`, `meterSlug`, `quantity`, `totalCostUsd`, `source`, and `occurredAt`. ## Usage items ### UsageItem ```python theme={null} @dataclass(frozen=True) class UsageItem: meter_slug: str quantity: int | None = None max_quantity: int | None = None ``` What a request will be charged for, used by `guard`, `check_allowance`, and `payment_required`. Declare at most one of `quantity` (a known amount) or `max_quantity` (a [ceiling](/integrate/charge-up-to-a-maximum) for work you cannot size up front). Declaring both raises `ZCError`. Declaring neither defers to the meter's configured per-request ceiling; if the meter has none, the API denies the check with `meter_not_priced`. Quantities must be positive integers. ```python theme={null} usage = [ UsageItem(meter_slug="requests", quantity=1), UsageItem(meter_slug="output_tokens", max_quantity=100_000), ] ``` The buyer authorizes up to a ceiling and settles at the actual amount you report, so a ceiling never overcharges. ### SyncUsageItem ```python theme={null} @dataclass(frozen=True) class SyncUsageItem: service_slug: str meter_slug: str quantity: int ``` Actual usage settled alongside a successful response, used by `with_usage` and `usage_header`. All fields are required; `quantity` must be a positive integer. ## Standalone verification `canonical_string` builds the exact string ZeroClick signs: six fields joined by newlines. You need it only to debug a verification mismatch or implement verification elsewhere; the [signature spec](/integrate/signature-spec) specifies the format. ```python theme={null} def canonical_string( *, timestamp: str, method: str, path_and_query: str, body: bytes, zc_request_id: str, zc_agent_id: str | None, ) -> str ``` ## Framework adapters ```python theme={null} from zeroclick_sellers.adapters import ( asgi_path_and_query, wsgi_path_and_query, zc_request_from_asgi_scope, zc_request_from_wsgi_environ, ) ``` * `zc_request_from_asgi_scope(scope, body) -> ZcRequest`: builds a request from an ASGI scope and the already-read raw body (FastAPI, Starlette). * `zc_request_from_wsgi_environ(environ, body) -> ZcRequest`: the same for a WSGI environ (Flask, Django). * `asgi_path_and_query(scope) -> str` and `wsgi_path_and_query(environ) -> str`: the raw request-target recovery alone, for custom integrations. The adapters' whole job is recovering the raw, percent-encoded request target that the signature covers; see [why the adapters exist](/sdks/python/quickstart#why-the-adapters-exist). In both cases `body` must be the raw bytes, read before any framework parses or re-serializes them. ## Module defaults | Constant | Value | | ------------------------------- | -------------------------------------- | | `DEFAULT_API_BASE_URL` | `"https://api.zeroclick.io"` | | `DEFAULT_TOLERANCE_SECONDS` | `300` | | `DEFAULT_CHECK_TIMEOUT_SECONDS` | `1.5` | | `COMPACT_JWE_ALG` | `"ECDH-ES+A256KW"` | | `COMPACT_JWE_ENC` | `"A256GCM"` | | `REPLY_JWK_PARAM` | `"https://zeroclick.io/jwe/reply-jwk"` | ## Encryption For services that opt into body encryption, the request body arrives as a Compact JWE and the reply goes back the same way. The signature covers the **ciphertext**, so `guard` runs first and unchanged; decrypt only after an allow decision. ```python theme={null} def decrypt_request( body: bytes | str, *, resolve_private_key: Callable[[str], Any] ) -> DecryptedEnvelope def encrypt_response(response: ZcResponse, envelope: DecryptedEnvelope) -> ZcResponse ``` `resolve_private_key` receives the `kid` from the JWE protected header and returns that private key (a JWK mapping or a `joserfc` `ECKey`), or `None` if it is unknown. `DecryptedEnvelope` carries the `plaintext` bytes plus what `encrypt_response` needs to encrypt the reply (`protected_header`, `cty`, `reply_jwk`). `encrypt_response` returns the response unchanged when the request carried no reply key, so the same handler serves encrypted and plaintext buyers. ```python theme={null} import json from zeroclick_sellers import decrypt_request, encrypt_response raw_body = await request.body() zc_request = zc_request_from_asgi_scope(request.scope, raw_body) decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) envelope = decrypt_request(raw_body, resolve_private_key=lookup_private_key) payload = json.loads(envelope.plaintext) # ... do the work, build the response, stamp usage with with_usage ... return to_fastapi(encrypt_response(stamped_response, envelope)) ``` The suite is fixed at `ECDH-ES+A256KW` key management with `A256GCM` content encryption; the SDK rejects anything else. If a reply key arrives with private key material, the SDK rejects it outright rather than using it. Failures raise `ZCError` with JWE-specific string codes. ## Errors `ZCError` and `is_zc_error(error, code=None)` are the last two exports. Guard outcomes are decisions; `ZCError` is reserved for malformed input and ZeroClick API failures. The [errors page](/sdks/python/errors) covers every code and how to handle them. # Python SDK configuration Source: https://docs.zeroclick.ai/sdks/python/configuration Every option create_seller and create_async_seller accept: signing secrets, usage keys, the outage policy, timeouts, HTTP client injection, and lifecycle. `create_seller` returns a blocking `SellerClient` for WSGI apps; `create_async_seller` returns a non-blocking `AsyncSellerClient` for ASGI apps. Both accept the same keyword arguments and make the same decisions. Configure one client at startup and reuse it for every request. The client validates its configuration at construction, not when the first request arrives: any invalid combination raises `ZCError` with code `malformed_input` (see [errors](/sdks/python/errors)). ```python theme={null} import logging import os from zeroclick_sellers import create_async_seller logger = logging.getLogger("zeroclick") zeroclick = create_async_seller( # Signature verification signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, tolerance_seconds=300, # Usage API credentials api_key=os.environ["ZEROCLICK_API_KEY"], # Allowance behavior allowance_unavailable_policy="allow", check_timeout_seconds=1.5, on_allowance_unavailable=lambda error: logger.warning( "allowance check failed: %s", error.code ), ) ``` ## Options Signing secrets keyed by key id: `{"hsec_…": "zcsec_…"}`. Every `zc-signature` header names the `kid` it was signed with, and the SDK uses it to pick the matching secret. The mapping must contain at least one entry. Provide exactly one of `signing_secrets` or `resolve_signing_secret`. Passing both, or neither, raises at construction. A lookup function in place of the static mapping, for secrets that live in a secret manager. It receives the `kid` from the request's signature header and returns the secret, or `None` for an unknown kid. If it returns `None`, the SDK denies the request with the `401` response rather than raising. If the function raises, or returns anything other than a non-empty string, the SDK raises `ZCError` with code `signing_secret_resolution_failed`. An API key carrying both the `usage:read` and `usage:write` scopes. It backfills whichever of the two scoped keys you leave unset, so a single key is enough for the whole client. A key with the `usage:read` scope, used for allowance checks (`guard` and `check_allowance`). When unset, it falls back to `api_key`. A key with the `usage:write` scope, used for usage reporting (`report_usage`). This is handy when a separate worker reports usage. When unset, it falls back to `api_key`. Base URL for the ZeroClick usage API. Override it only to point the SDK at a test double. Maximum accepted difference, in seconds, between the timestamp in the `zc-signature` header and the current clock. The SDK denies a request outside the window with the `401` response (reason `stale_timestamp`). What `guard` does when the allowance API gives no usable answer after a signature has verified: `"allow"`, `"deny"`, or `"throw"`. Any other value raises at construction. See [outage policy](#outage-policy) below. Timeout, in seconds, for calls to the ZeroClick usage API, passed to `httpx` per request. Despite the name, the client applies it to both allowance checks and usage reports. The SDK never retries a timed-out call. Called with the `ZCError` whenever the allowance API fails to answer during `guard`. The hook runs before the SDK applies the policy, whatever the policy is. Use it for operational logging and metrics; it does not change the decision. The time source for signature freshness checks. Inject a fixed clock in tests to verify recorded requests without patching `time.time`. Your own configured `httpx` client (`httpx.Client` for `create_seller`, `httpx.AsyncClient` for `create_async_seller`), for when you need proxies, event hooks, or connection limits. When omitted, the SDK constructs one. Closing the seller client closes whichever `httpx` client it holds, injected or not. ## Keys: one or two `api_key` must carry both usage scopes. To follow least privilege, pass two scoped keys instead and omit `api_key`. The read key covers allowance checks, and the write key covers usage reporting: ```python theme={null} import os from zeroclick_sellers import create_async_seller zeroclick = create_async_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, usage_read_key=os.environ["ZEROCLICK_USAGE_READ_KEY"], usage_write_key=os.environ["ZEROCLICK_USAGE_WRITE_KEY"], ) ``` The constructor validates coverage. If neither `api_key` nor the scoped key covers a side, it raises `ZCError` with code `malformed_input` naming the missing key: `Provide usage_read_key (or a both-scopes api_key) for allowance checks`, or `Provide usage_write_key (or a both-scopes api_key) for usage reporting`. ## Rotating signing secrets `signing_secrets` takes any number of entries, and the `kid` in each request selects the right one, so rotation is zero-downtime. Create the new secret. Deploy with both entries. Once ZeroClick signs only with the new kid, drop the old entry: ```python theme={null} zeroclick = create_async_seller( signing_secrets={ "hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET_CURRENT"], "hsec_2r8w1t6y4b0s": os.environ["ZEROCLICK_SIGNING_SECRET_PREVIOUS"], }, api_key=os.environ["ZEROCLICK_API_KEY"], ) ``` The key ids are not secret: they appear in every `zc-signature` header. See [keys and secrets](/integrate/keys-and-secrets) for the rotation workflow in the dashboard. ## Outage policy `allowance_unavailable_policy` applies only when the allowance API gives no answer (a timeout, a transport failure, an error status, or a malformed response), and only after a signature has verified. It never applies to a missing or invalid signature, and an `allowed: false` answer is a normal `402` denial, not an outage. * `"allow"` (default): serve the request. The decision is an allow with `allowance == "unavailable"`, so you can tell it apart from a confirmed `"allowed"`. * `"deny"`: return the SDK's `503 {"error":"allowance_unavailable"}` response. * `"throw"`: the SDK raises the `ZCError` for your application to handle. Whatever the policy, the SDK calls `on_allowance_unavailable` first with the underlying error. See [errors](/sdks/python/errors) for which error codes the policy covers. ## Lifecycle Both clients hold an `httpx` client, so a long-lived module-level instance is the normal shape. Create the client at import time and reuse it for every request, as the [quickstart](/sdks/python/quickstart) examples do. For short-lived processes (scripts, workers, tests), close the client explicitly with `close()` / `aclose()`, or use it as a context manager: ```python Sync theme={null} import os from zeroclick_sellers import create_seller with create_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) as zeroclick: zeroclick.report_usage( zc_agent_id="agt_x7f2kq93bh0d", idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens", service_slug="product-watch", meter_slug="output_tokens", quantity=4200, ) ``` ```python Async theme={null} import os from zeroclick_sellers import create_async_seller async with create_async_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) as zeroclick: await zeroclick.report_usage( zc_agent_id="agt_x7f2kq93bh0d", idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens", service_slug="product-watch", meter_slug="output_tokens", quantity=4200, ) ``` # Python SDK errors Source: https://docs.zeroclick.ai/sdks/python/errors Which operations return decisions and which raise ZCError, every error code and its context, and how to handle usage API failures. The SDK draws a hard line between expected events and errors. A request with a bad signature or an exhausted allowance is an expected event, not a programming mistake. `guard`, `guard_identity`, and `verify_request` return it as a decision that carries the exact response to send back. `ZCError` is reserved for what the caller got wrong (malformed input) or what the environment did (the ZeroClick API being unreachable or answering nonsense). ## Decisions or exceptions, by operation | Operation | Returns a decision for | Raises ZCError for | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `guard` | Bad signature (`401`), allowance denied (`402`), allowance unavailable under the `"allow"` and `"deny"` policies | Malformed usage input, a failing secret resolver, allowance unavailable under the `"throw"` policy | | `guard_identity` | Bad signature (`401`), missing buyer identity (`402`) | A failing secret resolver | | `verify_request` | Every signature failure (`401`) | Misconfigured secret sources, a non-`ZcRequest` argument, a failing secret resolver | | `check_allowance` | An allowed or denied `AllowanceDecision` | Every API failure (the outage policy applies only inside `guard`) | | `report_usage` | A duplicate replay (`duplicate=True` is a success) | Every API failure, including rejected reports | | `create_seller`, `create_async_seller` | None | Invalid configuration, at construction | | `ZcRequest`, `UsageItem`, `SyncUsageItem` | None | Invalid fields, at construction | | `decrypt_request`, `encrypt_response` | None | Every encryption failure | [GuardResult](/sdks/python/api#guardresult) documents the deny reasons that ride on decisions; this page covers the exceptions. ## ZCError ```python theme={null} class ZCError(Exception): code: ZCErrorCode # stable machine code, for example "api_status_error" operation: str # the operation that raised, for example "check_allowance" context: dict[str, Any] # {"operation": ..., plus code-specific keys} ``` `ZCError(code, message=None, operation=..., **context)` carries a stable machine `code`, the `operation` that raised it, and a `context` dict. The dict holds the operation plus code-specific keys such as `status`, `reason`, `kid`, or `meter_slug`. `str(error)` is a human-readable message; branch on `code` and `context`, never on the message text. `is_zc_error(error, code=None)` returns `True` when `error` is a `ZCError`, optionally of a specific code. It is useful at boundaries that catch broadly: ```python theme={null} from zeroclick_sellers import is_zc_error try: process_job() except Exception as error: if is_zc_error(error, "api_transport_error"): ... # the ZeroClick API was unreachable raise ``` ## Error codes `ZCErrorCode` is a `Literal` of five values: | Code | Meaning | Context keys | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | `api_response_invalid` | The ZeroClick API answered, but the body is not the shape the SDK requires: invalid JSON, missing fields, or an unrecognized denial reason. | `status`, sometimes `reason` | | `api_status_error` | The ZeroClick API returned a status of 400 or above. | `status`; `reason` on rejected usage reports | | `api_transport_error` | The request never completed: timeout, connection failure, DNS. The default check timeout is 1.5 seconds, and the SDK never retries. | None | | `malformed_input` | You passed the SDK something invalid: at construction (missing usage keys, no signing secret, a bad policy value) or at call time (duplicate meters, both `quantity` and `max_quantity`, a non-bytes body). The message names the field. | varies | | `signing_secret_resolution_failed` | Your `resolve_signing_secret` callable raised, or returned something other than a non-empty string. An unknown `kid` does not raise: it denies with the `401` response. | `kid` | ## The allowance outage policy Inside `guard`, after a signature has verified, the codes `api_transport_error`, `api_status_error`, and `api_response_invalid` all mean the same thing: the allowance API gave no usable answer. The SDK routes these three, and only these, to the client's `allowance_unavailable_policy` instead of propagating them: `"allow"` serves the request with `allowance == "unavailable"`, `"deny"` returns the SDK's `503 {"error":"allowance_unavailable"}`, and `"throw"` re-raises the error for your application. The `on_allowance_unavailable` hook sees the error first, whatever the policy. The policy never applies to a missing or invalid signature, and an `allowed: false` answer is not an outage: it is a `402` denial. Outside `guard`, in `check_allowance` and `report_usage`, these codes always raise. See [configuration](/sdks/python/configuration#outage-policy) for choosing a policy. ## Handling api\_status\_error `api_status_error` carries the HTTP status in `error.context["status"]`. When the API rejects a `report_usage` call with a typed status (`402`, `404`, or `409`), its machine reason also rides in `error.context["reason"]`: the report endpoint answers `402` for `access_inactive`, `plan_expired`, and `usage_exhausted`, `409` for `meter_not_priced`, and `404` otherwise. The [errors reference](/resources/errors) covers the platform-wide error model. ```python theme={null} import logging from zeroclick_sellers import ZCError logger = logging.getLogger("zeroclick") try: result = zeroclick.report_usage( zc_agent_id="agt_x7f2kq93bh0d", idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens", service_slug="product-watch", meter_slug="output_tokens", quantity=4200, ) except ZCError as error: if error.code != "api_status_error": raise status = error.context["status"] # for example 402 reason = error.context.get("reason") # for example "usage_exhausted" logger.warning("usage report rejected: %s %s", status, reason) else: if result.duplicate: logger.info("already reported; stored event replayed") ``` Because the SDK never retries, retrying is yours to schedule. It is also safe: reporting is idempotent per service on `idempotency_key`, so replaying the same report can never double-bill. ## Encryption error codes `decrypt_request` and `encrypt_response` raise `ZCError` with JWE-specific string codes that sit outside the five-value `ZCErrorCode` literal. Compare `error.code` directly rather than through `is_zc_error`'s typed `code` parameter: | Code | Meaning | | ------------------------------- | --------------------------------------------------------------------------------------------- | | `invalid_compact_jwe` | The request body is not a valid Compact JWE. | | `unsupported_jwe_suite` | The JWE does not use `ECDH-ES+A256KW` with `A256GCM`, the only accepted suite. | | `jwe_kid_required` | The JWE protected header has no key id. | | `invalid_reply_jwk` | The reply key is not a public P-256 key. | | `private_reply_jwk` | The reply key carries private key material. The SDK rejects it outright rather than using it. | | `private_key_not_found` | `resolve_private_key` returned `None` for the JWE's `kid`. | | `private_key_resolution_failed` | `resolve_private_key` raised. | | `decryption_failed` | The SDK could not decrypt the encrypted request. | | `encryption_failed` | The SDK could not encrypt the response to the reply key. | `private_key_not_found`, `private_key_resolution_failed`, and `decryption_failed` carry the JWE's `kid` in `error.context`. # Python SDK quickstart Source: https://docs.zeroclick.ai/sdks/python/quickstart Install zeroclick-sellers, pick the sync or async client, and guard a FastAPI, Flask, or Django route. The [`zeroclick-sellers`](https://pypi.org/project/zeroclick-sellers/) package implements the ZeroClick billing guard for Python backends: it verifies that each request came from ZeroClick, checks the buyer's [allowance](/concepts/usage-and-allowances) before you do the work, returns the refusal responses ZeroClick expects, and settles or reports what was used. This page wires the guard into FastAPI, Flask, and Django. The [integration overview](/integrate/overview) describes the contract it implements. ## Install ```sh theme={null} pip install zeroclick-sellers ``` The package requires Python 3.10 or later and is fully typed (it ships `py.typed`, so type checkers see every signature). Its HTTP calls go through `httpx`. You need three values from the [dashboard](https://dashboard.zeroclick.io): your signing secret (`zcsec_…`), its key id (`hsec_…`), and an API key (`zc_…`) with the `usage:read` and `usage:write` scopes. See [keys and secrets](/integrate/keys-and-secrets). ```sh theme={null} ZEROCLICK_SIGNING_SECRET_KID=hsec_k5nq0v7m3d8p ZEROCLICK_SIGNING_SECRET=zcsec_… ZEROCLICK_API_KEY=zc_… ``` ## Pick the right client | Your framework | Client | Guard call | | ------------------------- | --------------------- | ---------------------------- | | FastAPI, Starlette (ASGI) | `create_async_seller` | `await zeroclick.guard(...)` | | Flask, Django (WSGI) | `create_seller` | `zeroclick.guard(...)` | Use the async client in ASGI apps. The blocking client would stall the event loop during every allowance check. The two clients accept the same options and make the same decisions; they differ only in how they perform IO. ## Guard a FastAPI route ```python theme={null} import os from fastapi import FastAPI, Request from fastapi.responses import Response from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_async_seller from zeroclick_sellers.adapters import zc_request_from_asgi_scope zeroclick = create_async_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) app = FastAPI() def to_fastapi(response: ZcResponse) -> Response: return Response( content=response.body, status_code=response.status, headers=dict(response.headers), ) @app.post("/v1/product-watch") async def product_watch(request: Request) -> Response: zc_request = zc_request_from_asgi_scope(request.scope, await request.body()) decision = await zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_fastapi(decision.response) result = do_the_work(owner=decision.context.zc_agent_id) return to_fastapi( zeroclick.with_usage( ZcResponse.json(result), [ SyncUsageItem( service_slug="product-watch", meter_slug="requests", quantity=1 ) ], ) ) ``` The handler is four moves: 1. **Adapt.** `zc_request_from_asgi_scope` turns the framework request into a `ZcRequest` built from the raw path and raw body bytes. 2. **Guard.** `guard` verifies the `zc-signature` header before it calls the allowance API, and returns a decision, not an exception. 3. **Deny.** A deny decision carries the exact response to return: `401` for a bad signature, the `402 payment_required` body ZeroClick converts into a payment challenge, or `503` under a fail-closed outage policy. An allow decision carries the verified context: `decision.context.zc_agent_id` identifies the buyer. 4. **Settle.** `with_usage` stamps the `zc-usage` response header with the actual usage. ZeroClick records that usage and strips the header before the agent sees the response. ## Flask and Django The handler keeps the same shape: `create_seller`, no `await`, and the WSGI adapter in place of the ASGI one. ```python Flask theme={null} import os from flask import Flask, Response, request from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_seller from zeroclick_sellers.adapters import zc_request_from_wsgi_environ zeroclick = create_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) app = Flask(__name__) def to_flask(response: ZcResponse) -> Response: return Response( response=response.body, status=response.status, headers=dict(response.headers), ) @app.post("/v1/product-watch") def product_watch() -> Response: # request.get_data() returns the raw body without consuming it for later # handlers; never use request.json here, which would re-serialise. zc_request = zc_request_from_wsgi_environ(request.environ, request.get_data()) decision = zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_flask(decision.response) result = do_the_work(owner=decision.context.zc_agent_id) return to_flask( zeroclick.with_usage( ZcResponse.json(result), [ SyncUsageItem( service_slug="product-watch", meter_slug="requests", quantity=1 ) ], ) ) ``` ```python Django theme={null} import os from django.http import HttpRequest, HttpResponse from django.urls import path from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_seller from zeroclick_sellers.adapters import zc_request_from_wsgi_environ zeroclick = create_seller( signing_secrets={ os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[ "ZEROCLICK_SIGNING_SECRET" ] }, api_key=os.environ["ZEROCLICK_API_KEY"], ) def to_django(response: ZcResponse) -> HttpResponse: django_response = HttpResponse( content=response.body, status=response.status, content_type=response.headers.get("content-type", "application/json"), ) for key, value in response.headers.items(): if key.lower() != "content-type": django_response[key] = value return django_response def product_watch(request: HttpRequest) -> HttpResponse: # request.body is the raw bytes. request.META is the WSGI environ, which # is where the raw target lives; request.path is already decoded. zc_request = zc_request_from_wsgi_environ(request.META, request.body) decision = zeroclick.guard( zc_request, service_slug="product-watch", usage=[UsageItem(meter_slug="requests", quantity=1)], ) if decision.action == "deny": return to_django(decision.response) result = do_the_work(owner=decision.context.zc_agent_id) return to_django( zeroclick.with_usage( ZcResponse.json(result), [ SyncUsageItem( service_slug="product-watch", meter_slug="requests", quantity=1 ) ], ) ) urlpatterns = [path("v1/product-watch", product_watch)] ``` ## Why the adapters exist The signature covers the request target exactly as ZeroClick sent it, so `ZcRequest.path_and_query` must be the **raw, percent-encoded** path and query. Every framework hands you a decoded one. These are the observed values for `GET /v1/items/a%2Fb%20c`: | | Decoded (unusable) | Raw (correct) | | --------------- | ----------------------------------- | ------------------------------------------- | | ASGI / uvicorn | `scope["path"]` → `/v1/items/a/b c` | `scope["raw_path"]` → `/v1/items/a%2Fb%20c` | | WSGI / werkzeug | `PATH_INFO` → `/v1/items/a/b c` | `RAW_URI` → `/v1/items/a%2Fb%20c?…` | Using the decoded path produces a different canonical string and fails verification. The adapters handle this. They also cover a spec difference between the two interfaces: ASGI's `raw_path` excludes the query string (the adapter appends `scope["query_string"]`), while WSGI's `RAW_URI` and `REQUEST_URI` already include it. The body has the same rule: pass the raw bytes exactly as received, before any framework parses or re-serializes them. Use `await request.body()` in FastAPI, `request.get_data()` in Flask, and `request.body` in Django. On WSGI, if the server sets neither `RAW_URI` nor `REQUEST_URI`, the SDK cannot recover an encoded separator. WSGI decodes `%2F` to `/` before the SDK runs, and nothing can tell it from a literal `/`. gunicorn, werkzeug, uWSGI, and nginx all set one of them. ## Test the guard The guard must deny a plain request with no ZeroClick headers. Only signed requests from ZeroClick reach your handler: ```sh theme={null} curl -i -X POST http://localhost:8000/v1/product-watch # HTTP/1.1 401 # {"error":"invalid_zeroclick_signature"} ``` The [quickstart](/quickstart) covers the rest of the loop: deploying the guarded route and running the dashboard's API setup verification against it. ## Next steps Every constructor option: secrets, split usage keys, outage policy, timeouts. Every public export, from guard to the encryption helpers. Decisions versus exceptions, and every ZCError code. Bill work you cannot size up front, like output tokens. # TypeScript SDK API Source: https://docs.zeroclick.ai/sdks/typescript/api Every public export of @zeroclickai/sellers: client methods, standalone functions, result shapes, and the contracts and encryption subpaths. Everything in `@zeroclickai/sellers` comes two ways: as bound methods on the client `createSeller(config)` returns, and as standalone named exports. The standalone exports are for applications that place verification, allowance checking, and response construction in separate middleware layers. All public functions validate their inputs and throw a typed [`ZCError`](/sdks/typescript/errors) for malformed input or API failure. Signature failures and business denials are returned decisions, never exceptions. ## Client methods `createSeller(config)` (see the [configuration reference](/sdks/typescript/configuration)) returns these bound methods: | Method | Purpose | | --------------------------------- | -------------------------------------------------------------------------------------------------- | | `guard(request, input)` | Verify the request, check the allowance, and return an allow or deny decision. | | `guardIdentity(request, input)` | Verify the request and require a proven buyer for a free, identity-scoped call. No allowance call. | | `verifyRequest(request)` | Verify only the ZeroClick request signature without consuming the original body. | | `checkAllowance(input, options?)` | Call `POST /v1/usage/check` directly. | | `paymentRequired(input)` | Construct the exact seller `402 payment_required` response. | | `withUsage(response, usage)` | Set the validated `zc-usage` header without consuming the response body. | | `reportUsage(input, options?)` | Call `POST /v1/usage` for asynchronous usage. | ### guard ```ts theme={null} guard(request: Request, input: { serviceSlug: string; planSlug?: string; usage: UsageItem[]; // at least one item }): Promise ``` Verifies the signature first, then checks the allowance under the request's own `zcRequestId` and returns a [decision](#guardresult). It never consults the allowance API for an unverified request. Each `UsageItem` names a `meterSlug` and declares the charge one of three ways: an exact `quantity`, a `maxQuantity` ceiling, or neither. A ceiling covers work you cannot size up front; it settles later at actual usage, so it never overcharges. An item with neither defers to the meter's configured max usage per request. The SDK rejects an item that declares both. It also rejects duplicate meters and an empty `usage` array: an accidentally empty computed array must fail rather than silently skip the check. ```ts theme={null} const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [ { meterSlug: "requests", quantity: 1 }, { meterSlug: "output_tokens", maxQuantity: 100_000 }, ], }); ``` On a business denial, the SDK builds the carried response from your `input`: the exact `402 payment_required` body with the declared usage and, if you passed one, the `planSlug`. See [charge up to a maximum](/integrate/charge-up-to-a-maximum) for the ceiling pattern. ### guardIdentity ```ts theme={null} guardIdentity(request: Request, input: { serviceSlug: string }): Promise ``` The guard for free, identity-scoped endpoints: calls that cost nothing but serve only the buyer that owns the underlying records, such as polling a job the buyer created. It verifies the signature exactly like `guard`. When `zc-agent-id` is present, it allows with `allowance: { status: "not_required" }`. When it is absent, it denies with `reason: "identity_required"` and a `402` whose body carries `usage: []`. ZeroClick answers that with a free identity challenge, and the retry arrives with the buyer's `zc-agent-id` attached. `guardIdentity` makes no allowance call, and no `zc-usage` belongs on the response: free means free. ```ts theme={null} export async function GET(request: Request) { const decision = await zeroClick.guardIdentity(request, { serviceSlug: "product-watch", }); if (decision.action === "deny") return decision.response; const job = await findJob(jobId, { owner: decision.context.zcAgentId }); if (!job) return Response.json({ error: "not_found" }, { status: 404 }); return Response.json(job); } ``` Identity alone is not billability: a buyer that has only proven identity has no active access, so `reportUsage` against it fails with `access_not_found`. Gate billable work with `guard`. See [free and identity endpoints](/integrate/free-and-identity-endpoints). ### verifyRequest ```ts theme={null} verifyRequest(request: Request): Promise ``` Verifies the `zc-signature` header and nothing else: parses `t`, `kid`, and `v1`, enforces the timestamp tolerance, resolves the signing secret by `kid`, recomputes the HMAC-SHA256 over the canonical string, and compares in constant time. It reads a clone, so the original body stays readable. The result is a decision: ```ts theme={null} type VerifyRequestResult = | { ok: true; context: ZeroClickContext } | { ok: false; reason: VerifyFailureReason; response: Response }; ``` `reason` is one of `missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, or `invalid_signature`; `response` is the ready-to-return `401 {"error":"invalid_zeroclick_signature"}`. Use it when verification and allowance checking live in different layers: ```ts theme={null} const verification = await zeroClick.verifyRequest(request); if (!verification.ok) return verification.response; // verification.context: zcRequestId, zcAgentId, timestamp, kid ``` The [signature spec](/integrate/signature-spec) specifies the canonical string and header format. ### checkAllowance ```ts theme={null} checkAllowance( input: { zcRequestId: string; serviceSlug: string; usage: UsageItem[] }, options?: { signal?: AbortSignal }, ): Promise<{ allowed: boolean; reason: UsageDenialReason | null }> ``` Calls `POST /v1/usage/check` with the verified request's `zcRequestId`. Checking records nothing and burns no credit: it answers whether the declared usage is covered right now. The SDK rejects an empty or duplicate-metered `usage` array before calling; the API accepts up to 20 items. Unlike `guard`, `checkAllowance` throws on a failed call: no outage policy applies here, so the caller decides. ```ts theme={null} const allowance = await zeroClick.checkAllowance({ zcRequestId: verification.context.zcRequestId, serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (!allowance.allowed) { // allowance.reason: "usage_exhausted", "access_not_found", … } ``` See [check allowances](/integrate/check-allowances) for what each denial reason means. ### paymentRequired ```ts theme={null} paymentRequired(input: { serviceSlug: string; planSlug?: string; usage: UsageItem[]; // [] is the free identity refusal }): Response ``` Constructs the exact `402` refusal ZeroClick expects from a seller: `{"error":"payment_required","serviceSlug":"…","usage":[…]}`. For pay-as-you-go pricing, ZeroClick re-prices the refusal an anonymous probe earns into the priced challenge the agent sees. An empty `usage` array is valid here: it is the refusal for a free, identity-scoped endpoint, and ZeroClick answers it with a \$0 identity challenge. ```ts theme={null} return zeroClick.paymentRequired({ serviceSlug: "product-watch", usage: [{ meterSlug: "output_tokens", maxQuantity: 100_000 }], }); ``` ### withUsage ```ts theme={null} withUsage(response: Response, usage: { serviceSlug: string; meterSlug: string; quantity: number; // positive integer }[]): Response ``` Sets the validated `zc-usage` header for synchronous settlement without consuming the response body: it requires an unread `Response` and rebuilds it around the same body stream. Every item declares an exact quantity: this is where ceilings settle at actual usage. The header belongs on successful (`2xx`) responses only, and ZeroClick strips it before the agent sees the response. ```ts theme={null} return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, { serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4187 }, ]); ``` See [settle usage](/integrate/settle-usage) for choosing between synchronous and asynchronous settlement. ### reportUsage ```ts theme={null} reportUsage( input: { zcAgentId: string; idempotencyKey: string; serviceSlug: string; meterSlug: string; quantity: number; // positive integer occurredAt?: string; // ISO timestamp }, options?: { signal?: AbortSignal }, ): Promise ``` Calls `POST /v1/usage` for work that finishes after the response is gone. The idempotency key is seller-owned: derive it from stable facts (`zcreq_8h2m4x0q9k1f_output_tokens`), never random. Reporting is idempotent per service on that key, and `reportUsage` does not generate keys or retry automatically. ```ts theme={null} const result = await zeroClick.reportUsage({ zcAgentId: "agt_x7f2kq93bh0d", idempotencyKey: "zcreq_8h2m4x0q9k1f_output_tokens", serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4200, }); console.log(result.recorded, result.duplicate); ``` The result is `{ recorded: true, duplicate: boolean, usageEvent }`. `duplicate: true` replays the stored event: a success, not an error. A business denial at reporting time throws an `api_status_error` whose `context.reason` carries the denial reason; see [errors](/sdks/typescript/errors). ## Result shapes ### GuardResult `guard` and `guardIdentity` return the same union: ```ts theme={null} type GuardResult = | { action: "allow"; context: ZeroClickContext; allowance: { status: "allowed" | "unavailable" | "not_required" }; } | { action: "deny"; reason: GuardDenyReason; response: Response }; ``` The allow branch's `context` is the verified ZeroClick envelope: | Field | Value | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `zcRequestId` | The correlation id (`zcreq_…`) ZeroClick threads through the challenge, the paid retry, and the usage record. | | `zcAgentId` | The id (`agt_…`) of the agent that made this call, or `null` on a signed anonymous probe. | | `zcAnonymousId` | The same value as `zcAgentId`, read from `zc-anonymous-id`: the name that will eventually replace `zc-agent-id`. | | `zcBuyerId` | The buyer (`byr_…`) the agent belongs to, or `null` for an anonymous agent. Not covered by the signature. See [agents and access](/concepts/agents-and-access). | | `timestamp` | The signature timestamp, in Unix seconds. | | `kid` | The id of the signing secret that verified the request. | `allowance.status` is `"allowed"` when the API said yes, `"unavailable"` when the fail-open outage policy admitted a verified request without an answer, and `"not_required"` from `guardIdentity`, which makes no allowance call. ### Deny reasons | `reason` | Response carried | Source | | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------- | | `missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, `invalid_signature` | `401` with `{"error":"invalid_zeroclick_signature"}` | Signature verification, in every guard | | `service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, `usage_exhausted` | `402` with the `payment_required` body | The allowance check in `guard` | | `allowance_denied` | `402` with the `payment_required` body | `guard`, when the API denies without naming a reason | | `identity_required` | `402` with `usage: []` | `guardIdentity`, for a signed request without `zc-agent-id` | | `allowance_unavailable` | `503` with `{"error":"allowance_unavailable"}` | `guard` under the `"deny"` outage policy | ## Standalone exports The same primitives are named exports for applications that spread the flow across middleware layers. The guards take an options argument in place of client configuration; the API calls take [per-call options](/sdks/typescript/configuration#standalone-function-options). * `guard(request, input, options)`: options are a signing-secret source (`signingSecrets` or `resolveSigningSecret`), a required `apiKey`, and the guard settings (`apiBaseUrl`, `fetch`, `clock`, `toleranceSeconds`, `allowanceUnavailable`, `checkTimeoutMs`, `onAllowanceUnavailable`). * `guardIdentity(request, input, options)` and `verifyRequest(request, options)`: options are a signing-secret source plus optional `clock` and `toleranceSeconds`. * `checkAllowance(input, options)` and `reportUsage(input, options)`: options are `{ apiKey, apiBaseUrl?, fetch?, signal?, timeoutMs? }`. * `paymentRequired(input)` and `withUsage(response, usage)`: identical to the bound methods. * `invalidZeroClickSignature()` and `allowanceUnavailable()`: construct the bare `401` and `503` refusal responses. * `ZCError` and `isZCError`: the typed error and its narrowing helper; see [errors](/sdks/typescript/errors). ```ts theme={null} import { verifyRequest } from "@zeroclickai/sellers"; const verification = await verifyRequest(request, { signingSecrets: { hsec_k5nq0v7m3d8p: process.env.ZEROCLICK_SIGNING_SECRET!, }, }); ``` The package root exports TypeScript types for every input and result: `SellerConfig`, `GuardInput`, `GuardResult`, `ZeroClickContext`, `UsageItem`, `SyncUsageItem`, `CheckAllowanceInput`, `AllowanceDecision`, `UsageDenialReason`, `ReportUsageInput`, `ReportUsageResult`, and the rest. ## The contracts subpath `@zeroclickai/sellers/contracts` exports the seller-facing Zod schemas and their inferred types: guard input, usage items, allowance requests, responses, and denial reasons, the `payment_required` body, `zc-usage` items, signature headers, the verified context, report inputs and results, seller configuration, and API call options. ```ts theme={null} import { checkAllowanceInputSchema, paymentRequiredBodySchema, reportUsageInputSchema, syncUsageSchema, zeroClickContextSchema, } from "@zeroclickai/sellers/contracts"; const usage = syncUsageSchema.parse([ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, ]); ``` Public SDK methods already validate their inputs, so reach for the schemas only to validate data at an earlier boundary: a queue message that will become a `reportUsage` call, a stored usage draft, a test fixture. ## Encryption Helpers for encrypted request and response bodies are opt-in through the `@zeroclickai/sellers/encryption` subpath: `decryptRequest(request, { resolvePrivateKey })` and `encryptResponse(response, envelope)`. Always run `guard` or `verifyRequest` against the original encrypted request before decrypting it, so the signature covers the Compact JWE bytes. The suite is fixed: `ECDH-ES+A256KW` key management with `A256GCM` content encryption. `decryptRequest` resolves the private key by the protected header's `kid`, which supports rotation. Private encryption keys remain seller-owned: the SDK does not generate, upload, persist, or rotate them. # TypeScript SDK configuration Source: https://docs.zeroclick.ai/sdks/typescript/configuration Every createSeller option, signing-secret rotation, split usage keys, the allowance outage policy, and per-call overrides. `createSeller(config)` validates its configuration up front and returns a frozen client. Invalid configuration throws a `ZCError` with code `malformed_input` at construction, not at request time. Construction enforces two constraints: * **Exactly one signing-secret source.** Provide `signingSecrets` or `resolveSigningSecret`: not both, not neither. * **Both usage directions covered.** Allowance checks (`guard`, `checkAllowance`) use `usageReadKey`. Usage reporting (`reportUsage`) uses `usageWriteKey`. A single `apiKey` with both scopes backfills either side. `createSeller` rejects a config that leaves a direction without a key; `context.issues` names the missing side. ## Options | Option | Required | Default | Description | | ------------------------ | ----------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `signingSecrets` | One secret source | None | Record from ZeroClick signing-secret `kid` (`hsec_…`) to secret value (`zcsec_…`). Keep the current and previous keys in the record during rotation. | | `resolveSigningSecret` | One secret source | None | Async resolver `({ kid }) => secret`, returning `null` for an unknown `kid`. For a secret manager or other dynamic store. | | `apiKey` | Key coverage | None | API key with both the `usage:read` and `usage:write` scopes. Backfills whichever scoped key below is absent. | | `usageReadKey` | Key coverage | None | API key with `usage:read`, used for the allowance checks behind `guard` and `checkAllowance`. | | `usageWriteKey` | Key coverage | None | API key with `usage:write`, used for `reportUsage`. | | `apiBaseUrl` | No | `https://api.zeroclick.io` | Absolute API base URL, as a string or `URL`. | | `fetch` | No | `globalThis.fetch` | Injected Fetch-compatible implementation, useful for private deployments and tests. | | `toleranceSeconds` | No | `300` | Maximum absolute age of a request signature, in seconds. | | `clock` | No | `Date.now` | Millisecond clock function, primarily for deterministic tests. | | `checkTimeoutMs` | No | `1500` | Allowance-check timeout in milliseconds. | | `allowanceUnavailable` | No | `"allow"` | Outage policy when the allowance API gives no answer: `"allow"`, `"deny"`, or `"throw"`. | | `onAllowanceUnavailable` | No | None | Callback receiving the sanitized `ZCError` when the allowance API is unavailable. | ## Usage keys One API key with both usage scopes covers everything. To separate concerns (a request path that only checks, a worker that only reports), pass scoped keys: ```ts theme={null} import { createSeller } from "@zeroclickai/sellers"; const zeroClick = createSeller({ signingSecrets: { hsec_k5nq0v7m3d8p: process.env.ZEROCLICK_SIGNING_SECRET!, }, usageReadKey: process.env.ZEROCLICK_USAGE_READ_KEY!, usageWriteKey: process.env.ZEROCLICK_USAGE_WRITE_KEY!, }); ``` A scoped key can also override one direction while `apiKey` backfills the other. See [keys and secrets](/integrate/keys-and-secrets) for minting scoped keys. ## Signing-secret rotation ZeroClick identifies each signature with a `kid`: the signing secret's id, carried in every `zc-signature` header. Keep every key that may still sign an in-flight request available to the SDK: ```ts theme={null} const zeroClick = createSeller({ signingSecrets: { hsec_k5nq0v7m3d8p: process.env.ZEROCLICK_SIGNING_SECRET_CURRENT!, hsec_j2rw8t4c6y1z: process.env.ZEROCLICK_SIGNING_SECRET_PREVIOUS!, }, apiKey: process.env.ZEROCLICK_API_KEY!, }); ``` If a request's `kid` is missing from the record, the SDK denies it with a `401` (reason `unknown_kid`). Never log or return a signing secret. Revoke an old key only after requests signed by it can no longer be in flight. ## Resolving secrets dynamically For managed secret storage, resolve by `kid` instead of holding secrets in memory: ```ts theme={null} const zeroClick = createSeller({ resolveSigningSecret: async ({ kid }) => secretStore.get(kid), apiKey: process.env.ZEROCLICK_API_KEY!, }); ``` The resolver runs on every verification with the `kid` from the request's signature header. Return the secret, or `null` for a `kid` you do not recognize. The SDK denies that request as `unknown_kid`. A resolver that throws surfaces as a `ZCError` with code `signing_secret_resolution_failed` rather than a deny. An outage in your secret store shows up as an error instead of silently rejecting traffic. ## Allowance outage policy The default policy is fail-open after the 1.5-second check timeout: the SDK allows a verified request with `allowance.status === "unavailable"`, and `onAllowanceUnavailable` receives the sanitized error. Configure `"deny"` to return `503 {"error":"allowance_unavailable"}` instead, or `"throw"` to handle the typed error in application code: ```ts theme={null} const zeroClick = createSeller({ signingSecrets: { hsec_k5nq0v7m3d8p: process.env.ZEROCLICK_SIGNING_SECRET!, }, apiKey: process.env.ZEROCLICK_API_KEY!, allowanceUnavailable: "deny", onAllowanceUnavailable: (error) => { console.error("allowance check unavailable", error.code, error.context); }, }); ``` The policy applies when the allowance check produces no usable answer: the call failed or timed out (`api_transport_error`), the API answered with an unsuccessful status (`api_status_error`), or the response body did not validate (`api_response_invalid`). It applies only after the signature verifies: the SDK never allows an unverified request because the allowance API is unavailable. A definite `allowed: false` answer is never subject to the policy; it always denies with the `402`. ## Per-call cancellation The bound `checkAllowance` and `reportUsage` methods accept an optional `{ signal }`: ```ts theme={null} await zeroClick.reportUsage(input, { signal: request.signal }); ``` The signal combines with the built-in timeout; whichever fires first aborts the call. `guard` already ties its allowance check to the incoming request's own `AbortSignal`, so an agent that disconnects does not leave a check running. Through the client, the allowance check times out after `checkTimeoutMs`, and `reportUsage` after the fixed 1,500 ms default. To raise the reporting timeout, call the standalone export with `timeoutMs`. ## Standalone function options The `checkAllowance` and `reportUsage` named exports hold no client state. Each call carries its own options, for middleware layers or workers that never construct a client: | Option | Required | Default | Description | | ------------ | -------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | | `apiKey` | Yes | None | API key with the scope the call needs: `usage:read` for `checkAllowance`, `usage:write` for `reportUsage`. | | `apiBaseUrl` | No | `https://api.zeroclick.io` | Absolute API base URL, as a string or `URL`. | | `fetch` | No | `globalThis.fetch` | Injected Fetch-compatible implementation. | | `signal` | No | None | `AbortSignal` combined with the timeout. | | `timeoutMs` | No | `1500` | Per-call timeout in milliseconds. | ```ts theme={null} import { reportUsage } from "@zeroclickai/sellers"; const result = await reportUsage( { zcAgentId: "agt_x7f2kq93bh0d", idempotencyKey: "zcreq_8h2m4x0q9k1f_output_tokens", serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4200, }, { apiKey: process.env.ZEROCLICK_USAGE_WRITE_KEY!, timeoutMs: 5000 }, ); ``` The standalone `guard`, `guardIdentity`, and `verifyRequest` exports take the options a client would otherwise hold; the [API reference](/sdks/typescript/api#standalone-exports) lists their shapes. # TypeScript SDK errors Source: https://docs.zeroclick.ai/sdks/typescript/errors Deny decisions vs ZCError exceptions, the full error-code list, narrowing with isZCError, and which operations throw which codes. The SDK separates expected protocol outcomes from failures. Missing, stale, malformed, or invalid ZeroClick signatures are expected protocol outcomes, not exceptions. `guard` and `guardIdentity` return a deny decision that carries the exact `Response` to return. The lower-level `verifyRequest` returns `{ ok: false, reason, response }`. Business denials (`allowed: false` from the allowance API) are deny decisions too. Malformed inputs, ZeroClick API failures, signing-secret resolution failures, and encryption failures throw `ZCError`. ## ZCError `ZCError` extends `Error` with a machine-readable `code` and a sanitized `context`: ```ts theme={null} class ZCError extends Error { readonly name: "ZCError"; readonly code: ZCErrorCode; readonly context: { operation: string; // the SDK operation that failed issues?: { code: string; message: string; path: (string | number)[] }[]; kid?: string; reason?: string; status?: number; }; } ``` `context` contains only sanitized operational fields: the operation, status, reason, key id, and validation issue paths. It never includes API keys, signing secrets, or request body bytes, so it is safe to log and is what `onAllowanceUnavailable` receives. ## Narrowing with isZCError Use `isZCError` to narrow an unknown error, optionally to one error code: ```ts theme={null} import { isZCError } from "@zeroclickai/sellers"; try { await zeroClick.reportUsage({ zcAgentId: "agt_x7f2kq93bh0d", idempotencyKey: "zcreq_8h2m4x0q9k1f_output_tokens", serviceSlug: "product-watch", meterSlug: "output_tokens", quantity: 4200, }); } catch (error) { if (isZCError(error, "api_status_error")) { console.error(error.code, error.context.status, error.context.reason); } throw error; } ``` `isZCError(error)` narrows to `ZCError`; passing a code narrows further, at runtime and in types: inside the branch, `error` is `ZCError & { code: "api_status_error" }`. ## Error codes Sixteen codes cover the SDK. The first seven come from the core package; only the `@zeroclickai/sellers/encryption` subpath throws the nine JWE codes. ### Core codes | Code | Meaning | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `malformed_input` | An SDK input failed validation: configuration, guard input, usage items, or call options. `context.issues` lists each violation's path. | | `malformed_request` | The value passed as the request is not a usable web-native `Request`, or the SDK could not clone and read its body. | | `crypto_unavailable` | The runtime provides no Web Crypto (`globalThis.crypto.subtle`), so the SDK cannot verify signatures. | | `signing_secret_resolution_failed` | The signing-secret resolver threw or returned an unusable value. Returning `null` for an unknown `kid` is not an error: the SDK denies that request as `unknown_kid`. | | `api_transport_error` | The call to the ZeroClick API failed or timed out before any response arrived. | | `api_status_error` | The ZeroClick API answered with an unsuccessful status. `context.status` carries it; `reportUsage` denials add `context.reason`. | | `api_response_invalid` | The ZeroClick API answered, but with a body the SDK could not parse or validate. | ### JWE codes | Code | Meaning | | ------------------------------- | ----------------------------------------------------------------------- | | `invalid_compact_jwe` | The request body is not a valid Compact JWE. | | `unsupported_jwe_suite` | The JWE does not use the fixed `ECDH-ES+A256KW` / `A256GCM` suite. | | `jwe_kid_required` | The JWE protected header carries no key id to resolve a private key by. | | `invalid_reply_jwk` | The buyer's reply JWK is not a public P-256 key. | | `private_reply_jwk` | The reply JWK contains private-key material, so the SDK rejects it. | | `private_key_not_found` | The resolver returned no private key for the JWE's key id. | | `private_key_resolution_failed` | The private-key resolver threw or returned an unusable value. | | `decryption_failed` | The SDK could not decrypt the Compact JWE with the resolved key. | | `encryption_failed` | The SDK could not encrypt the response body for the buyer's reply key. | ## What throws where | Operation | Codes it can throw | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `createSeller` | `malformed_input`, when the config violates the one-secret-source rule or leaves a usage direction without a key. | | `verifyRequest`, `guard`, `guardIdentity` (verification step) | `malformed_input`, `malformed_request`, `crypto_unavailable`, `signing_secret_resolution_failed`. Signature outcomes never throw. | | `guard` (allowance step) | `api_transport_error`, `api_status_error`, `api_response_invalid`, routed through the outage policy. The default `"allow"` converts them into an allow with `allowance.status: "unavailable"`, surfacing them only to `onAllowanceUnavailable`. `"deny"` converts them into the `503`, and `"throw"` rethrows them. | | `checkAllowance` | `malformed_input`, `api_transport_error`, `api_status_error`, `api_response_invalid`. | | `reportUsage` | Same as `checkAllowance`. On a `402`, `404`, or `409` denial, `context.reason` carries the machine reason, such as `usage_exhausted` or `meter_not_priced`. | | `paymentRequired`, `withUsage` | `malformed_input`, including a `withUsage` call on an already-read `Response`. | | `decryptRequest` | `malformed_input`, `malformed_request`, and every JWE code except `encryption_failed`. | | `encryptResponse` | `malformed_input`, `encryption_failed`. | A duplicate usage report is not an error: `reportUsage` resolves with `duplicate: true` and replays the stored event. Only denials and infrastructure failures throw. The [error reference](/resources/errors) documents the wire-level error envelope behind `api_status_error`. # TypeScript SDK quickstart Source: https://docs.zeroclick.ai/sdks/typescript/quickstart Install @zeroclickai/sellers and guard one route end to end: verify the signature, check the allowance, serve, and settle usage. [`@zeroclickai/sellers`](https://www.npmjs.com/package/@zeroclickai/sellers) implements the ZeroClick billing guard for web-native TypeScript backends. It verifies that ZeroClick signed each forwarded request, checks the buyer's allowance before you do the work, builds the refusal responses ZeroClick expects, and settles what was used. This page guards one route with it. The [quickstart](/quickstart) covers the same flow across all three SDKs, and the [integration contract](/integrate/overview) defines what the guard implements. You need the keys from the [quickstart](/quickstart): a signing secret (`zcsec_…`) with its key id (`hsec_…`), and an API key (`zc_…`) with the `usage:read` and `usage:write` scopes. You can pass a scoped key per direction instead; see [configuration](/sdks/typescript/configuration). This page uses a service `product-watch` with a meter `requests`; substitute your own slugs. ## Install ```sh theme={null} pnpm add @zeroclickai/sellers ``` The package is ESM and uses the standard `Request`, `Response`, `fetch`, `AbortSignal`, and Web Crypto APIs. It works in any runtime that provides those, including current Node.js and compatible edge runtimes. Nothing in the SDK is framework-specific; you adapt at the boundary, covered [below](#framework-integration). ## Guard a route Configure the client once, then use `guard` before doing work and `withUsage` on the successful response: ```ts theme={null} import { createSeller } from "@zeroclickai/sellers"; const signingSecretKid = process.env.ZEROCLICK_SIGNING_SECRET_KID; const signingSecret = process.env.ZEROCLICK_SIGNING_SECRET; const apiKey = process.env.ZEROCLICK_API_KEY; if (!signingSecretKid || !signingSecret || !apiKey) { throw new Error("ZeroClick seller credentials are not configured"); } const zeroClick = createSeller({ signingSecrets: { [signingSecretKid]: signingSecret, }, apiKey, }); export async function handle(request: Request): Promise { const decision = await zeroClick.guard(request, { serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (decision.action === "deny") return decision.response; const response = Response.json({ message: "Hello from the seller", zcRequestId: decision.context.zcRequestId, }); return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1, }, ]); } ``` `withUsage` stamps the validated `zc-usage` header on the response so ZeroClick settles the usage synchronously; ZeroClick strips the header before the agent sees the response. Set it on successful (`2xx`) responses only. [Settle usage](/integrate/settle-usage) covers the asynchronous alternative for work that outlives the request. ## Decisions, not exceptions `guard` always verifies the signature before it calls the allowance API. Its result is an explicit decision: * `action: "allow"` carries the verified context (`zcRequestId`, `zcAgentId`, the signature timestamp, and the `kid` that verified) and an allowance status of `"allowed"` or `"unavailable"`. * `action: "deny"` carries a `reason` and the exact `Response` to return. Invalid signatures produce a `401`, business denials produce the seller `402 payment_required` body, and fail-closed allowance outages produce a `503`. Return `decision.response` unchanged: each body is exactly what ZeroClick expects back from your API. The [API reference](/sdks/typescript/api) lists every deny reason, and [errors](/sdks/typescript/errors) draws the line between decisions and thrown `ZCError`s. ## Anonymous probes Signed anonymous probes are valid requests. To price a pay-as-you-go challenge, ZeroClick forwards the agent's unpaid request upstream with no agent id: the signature verifies, and `decision.context.zcAgentId` is `null`. Only the paid retry carries the agent id (`agt_…`). Treat probes like any other request: run the same guard and return the same responses. The `402` refusal a probe earns is exactly what ZeroClick re-prices for the agent. ## Framework integration Adapt your framework's request object to a web-native `Request` at the boundary. Preserve the exact method, URL, headers, and body bytes. The SDK reads a clone, so the original request body remains readable in your handler. Return the SDK-provided `Response` directly, or adapt it back to the framework's response type. A framework that already speaks web standards needs no glue. Hono hands you the untouched request as `c.req.raw`: ```ts theme={null} import { Hono } from "hono"; const app = new Hono(); app.post("/v1/product-watch", async (c) => { // zeroClick is the client from createSeller above. const decision = await zeroClick.guard(c.req.raw, { serviceSlug: "product-watch", usage: [{ meterSlug: "requests", quantity: 1 }], }); if (decision.action === "deny") return decision.response; // guard read a clone, so the body is still readable here. const { productId } = await c.req.json(); const response = Response.json({ watching: productId }); return zeroClick.withUsage(response, [ { serviceSlug: "product-watch", meterSlug: "requests", quantity: 1 }, ]); }); export default app; ``` A Next.js route handler already receives a web-native `Request` and returns a `Response`. The `handle` function above drops in as `export async function POST(request: Request)` in a `route.ts` file. The signature covers the raw bytes ZeroClick sent: the percent-encoded path and query exactly as requested, and the unmodified body. An adapter that re-parses and re-serializes the body, or normalizes the URL, breaks verification. Hand the original request through untouched. ## Next steps Every `createSeller` option: rotation, split keys, timeouts, outage policy. Every export, decision shape, and deny reason. `ZCError` codes and what throws where. Bill work you can't size up front, like output tokens. # Enable agent traffic Source: https://docs.zeroclick.ai/website/enable-agent-traffic Make your storefront discoverable by AI agents: head tags, the agent callout, an llms.txt entry, markdown for agent fetchers, and the visible badge. Agents find your storefront through your website. They read your pages as raw HTML, top to bottom, without running JavaScript, so a storefront that only humans can find gets no agent traffic. This page walks through the website changes that point agents at your pay URL, with each step marked required, highly recommended, or optional. Examples on this page use a seller named Acme whose storefront runs on the custom domain `agents.acme.com` from step 1. Substitute your own values as you paste: your storefront domain everywhere `agents.acme.com` appears (or your default pay URL, like `https://acme.pay.zeroclick.io`, if you skip the custom domain), your store name in the callout and badge text, and your seller id in the head snippet's `?seller=` parameter. Required Agents click through to a domain they recognize more often than a generic one, so serve your storefront on a subdomain you own rather than the default `.pay.zeroclick.io`. Acme, the example seller on this page, uses `agents.acme.com`, and every snippet below carries it. Configure yours under **Custom domains** in your store's settings before you paste anything: the domain is baked into the head tags, the callout, the badge, and the llms.txt links, so setting it up first means you paste each snippet once. Required Paste into every page's ``: ```html theme={null} ``` The `alternate` link advertises your storefront's llms.txt to crawlers. The inline script marks the document as JavaScript-capable and removes the agent callout (next step) for human visitors once the page loads. The badge script advertises the storefront to crawlers that do run JavaScript. If your site sets a strict Content-Security-Policy, allow `img-src` for your storefront domain (the badge image) and `script-src` for `cdn.zeroclick.io`. Required Agents read raw HTML top to bottom, so this block must lead the document. Paste it immediately after the opening `` tag: ```html theme={null}

Acme sells to AI agents - no signup

The agent storefront at https://agents.acme.com/llms.txt publishes prices, payment instructions, and setup. Agents can purchase autonomously or with their human's approval, with a range of payment methods; an agent without a payment method can set one up there instantly, no signup. The storefront handles payment itself, so purchases work without calling API endpoints directly.

``` The callout is text for agents, and it does not need to look like part of your page. The head script removes it for human visitors once the page loads, and it stays visible without JavaScript, so nothing on your site is served only to agents. Of everything on this page, this step does the most to route agent purchases through your storefront. Keep the wording as written, with only the names and URLs substituted.
Highly Recommended Add this block near the top of your site's `/llms.txt`, right after the title and summary. If your site does not serve one yet, create the file with it: ```markdown theme={null} ## Agentic commerce - [Acme agent storefront](https://agents.acme.com/llms.txt): AI agents can purchase from Acme autonomously or with their human's approval through ZeroClick. Agents without a payment method can set one up on the spot. Complete purchasing guide: https://agents.acme.com/llms-full.txt - live pricing: https://agents.acme.com/manifest.json ``` Highly Recommended Many agent tools fetch pages with an `Accept` header that prefers `text/markdown` over `text/html`; browsers never do. Returning your llms.txt content for those requests reaches summarizing fetchers that would otherwise drop head tags and links during HTML conversion. Apply it to every page, not just the homepage: agents land on pricing and docs pages just as often. This is content negotiation, not cloaking: the same page in the representation the client asked for. It does change what your existing URLs serve to markdown-preferring clients, so decide deliberately. Send `Vary: Accept` so caches keep the two representations apart. ```js theme={null} // Express-style example; adapt to your framework. // Middleware so every page negotiates, not just the homepage. app.use((req, res, next) => { const accept = req.headers.accept ?? ""; const markdown = accept.indexOf("text/markdown"); const html = accept.indexOf("text/html"); if (req.method === "GET" && markdown !== -1 && (html === -1 || markdown < html)) { res.set("vary", "accept").type("text/markdown"); return res.sendFile("llms.txt", { root: "public" }); } next(); }); ``` Optional The visible ZeroClick badge is a plain link and image for humans, typically in the footer. The image is served from your storefront domain, so it carries your store name and updates automatically; swap `badge-light.svg` for `badge-dark.svg` on dark footers: ```html theme={null} Acme - Agent Storefront, powered by ZeroClick ``` Nothing moves it at runtime, so render it however your site renders content, including natively inside a React or Vue app. The callout and head tags carry the agent traffic.
## Verify your changes * Fetch the homepage raw, the way an agent does. The callout heading appears near the top of the HTML and the `alternate` link tag is in the head: ```sh theme={null} curl -s https://www.acme.com/ | head -40 ``` * Load the page in a browser. The callout is gone, and the badge renders where you placed it. * Your site's `/llms.txt` serves the agentic commerce block near the top. * If you serve markdown to agent fetchers, a markdown-preferring request gets it: ```sh theme={null} curl -s -H "Accept: text/markdown, text/html, */*" https://www.acme.com/ ``` ## Next steps The billing guard contract: verify, check allowance, serve, settle usage. What happens after an agent lands on your storefront.