Five HTTP endpoints take a merchant from signup to accepting agentic checkouts. This page covers every one you need for day one.
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
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
}
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.
| Field | Type | Notes |
|---|---|---|
workspace_id | string | must match the API key's workspace |
event_type | string | e.g. checkout.completed — matches a Stripe meter event_name |
quantity | i64 | defaults to 1 |
gmv_cents | i64 | order total; aggregated by Stripe into invoice amounts |
currency | string | ISO 4217, defaults to USD |
idempotency_key | string | stable per business event (e.g. Shopify order id) |
occurred_at | RFC3339 | the 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.
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.
{shop_domain}, redirect the merchant to the returned URL./dashboard/onboarding?shopify=connected with step 3 unlocked.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.
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
}
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:
StateSet-Event — event name (same as body event).StateSet-Event-Id — stable event id (dedupe by this).StateSet-Signature — t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<body>").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.
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.
All non-public endpoints accept either:
sks_…, 256-bit entropy, SHA-256-hashed at rest) bound to one workspace.DEPLOY_API_KEY for operator flows.Authorization: Bearer … header. Workspace keys are hashed on arrival and compared constant-time.