Docs

Five HTTP endpoints take a merchant from signup to accepting agentic checkouts. This page covers every one you need for day one.

Quick start

Three curl calls — signup, deploy, emit a GMV event — turn into a live handler with metered billing.

# 1. Create a workspace + mint the first API key
curl -s -X POST https://deploy.stateset.cloud/api/v1/signup \
  -H 'content-type: application/json' \
  -d '{"slug":"acme","display_name":"Acme","contact_email":"ops@acme.com"}'
# → { "workspace": {...}, "api_key": { "token": "sks_..." }, "dashboard_url": "..." }

export SKS=sks_...
export WS=ws_...

# 2. Provision the ACP handler (vault + Cloud SQL + TLS)
curl -s -X POST https://deploy.stateset.cloud/api/v1/provision \
  -H "authorization: Bearer $SKS" \
  -H 'content-type: application/json' \
  -d "{\"service\":\"acp-handler\",\"customer_id\":\"acme\",\"config\":{\"METER_URL\":\"https://deploy.stateset.cloud\",\"METER_WORKSPACE_ID\":\"$WS\",\"METER_API_KEY\":\"$SKS\"}}"

# 3. Emit a billable GMV event from the handler
curl -s -X POST https://deploy.stateset.cloud/api/v1/meter \
  -H "authorization: Bearer $SKS" \
  -H 'content-type: application/json' \
  -d "{\"workspace_id\":\"$WS\",\"event_type\":\"checkout.completed\",\"quantity\":1,\"gmv_cents\":4299,\"idempotency_key\":\"order_5521\",\"occurred_at\":\"$(date -u +%FT%TZ)\"}"
# → 202, headers: X-Plan-Events-Used: 1, X-Plan-Events-Limit: 500, X-Plan-Events-Remaining: 499

Signup

POST/api/v1/signup — public, rate-limited.

Atomically creates a workspace, links a Stripe customer (if Stripe is configured on the server), and mints the first API key. The plaintext token is returned once and never again.

{
  "slug": "acme",            // 3-40 chars, lowercase alnum + hyphens, DNS-safe
  "display_name": "Acme",
  "contact_email": "ops@acme.com",  // optional
  "plan": "free"              // free | growth | scale | enterprise
}

Meter ingest

POST/api/v1/meter — workspace-scoped, requires meter:write.

Emits a billable GMV or usage event. Idempotent on (workspace_id, event_type, idempotency_key): pass the same key twice and the second call returns the original row with duplicate: true. Responses carry X-Plan-Events-Used, X-Plan-Events-Limit, and X-Plan-Events-Remaining so handlers can self-govern.

FieldTypeNotes
workspace_idstringmust match the API key's workspace
event_typestringe.g. checkout.completed — matches a Stripe meter event_name
quantityi64defaults to 1
gmv_centsi64order total; aggregated by Stripe into invoice amounts
currencystringISO 4217, defaults to USD
idempotency_keystringstable per business event (e.g. Shopify order id)
occurred_atRFC3339the event time, not the send time

POST/api/v1/meter/batch — up to 100 events per request.

Per-event rejects (validation, quota, workspace mismatch) count against the rejected field rather than failing the batch.

Provisioning

POST/api/v1/provision — admin or workspace-scoped.

Deploys a catalog service (e.g. acp-handler) to a tenant-isolated Kubernetes namespace with auto-provisioned Cloud SQL and automatic TLS.

Plan quotas: Free → 1 handler, Growth → 3, Scale → 10, Enterprise → unlimited. Hitting the cap returns 429 plan_limit_exceeded.

Shopify connect

  1. POST/api/v1/workspaces/:id/integrations/shopify/authorize — send {shop_domain}, redirect the merchant to the returned URL.
  2. Shopify redirects back to GET/api/v1/integrations/shopify/callback which verifies an HMAC-signed state token, exchanges the code for an access token, encrypts it with AES-256-GCM, and persists the integration.
  3. Merchant lands on /dashboard/onboarding?shopify=connected with step 3 unlocked.

Billing

POST/api/v1/workspaces/:id/billing/portal-session — mints a one-time Stripe Customer Portal URL. Use it instead of building your own billing UI.

Stripe webhooks at POST/webhooks/stripe drive plan changes automatically: subscription lookup_key growth/scale/enterprise maps directly to the workspace's plan and therefore its quotas.

Webhooks

Subscribe to workspace events and receive HMAC-signed POST deliveries.

POST/api/v1/workspaces/:id/webhooks — returns the signing secret (whsec_…) exactly once.

{
  "url": "https://your-app.com/webhooks/stateset",
  "description": "Prod Zapier pipe",
  "events": ["workspace.signup", "shopify.connected"]   // omit for all events
}

Delivery format

Every delivery is a POST with JSON body:

{
  "id": "evt_7f3c4…",
  "event": "shopify.connected",
  "workspace_id": "ws_…",
  "created_at": "2026-04-18T12:34:56Z",
  "data": { /* event-specific */ }
}

Three headers ride along:

Node verification example (the scheme matches Stripe's so any Stripe-webhook library reads too):

import crypto from 'node:crypto';

function verify(raw, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${raw}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Events currently emitted: workspace.signup, shopify.connected, provision.running, provision.failed, meter.quota.warning. More land as the platform ships.

Retries + auto-disable

A failed delivery is retried up to 4 times with increasing backoff (0s, 1s, 3s, 10s). 4xx responses short-circuit — those are permanent merchant-side errors and no amount of retrying helps. Each attempt carries an incrementing StateSet-Delivery-Attempt header.

Five consecutive deliveries that exhausted their retries flip the endpoint to disabled (visible in the dashboard + listing), matching Stripe's behavior. Reactivate via POST /api/v1/workspaces/:id/webhooks/:webhook_id/reactivate or the dashboard after fixing the receiver.

Auth

All non-public endpoints accept either:

Tokens are presented in the Authorization: Bearer … header. Workspace keys are hashed on arrival and compared constant-time.