Register a new partner project with a single API call. Your account manager issues a one-time enrollment token; your bootstrap script exchanges it for OAuth credentials and a webhook signing secret in one round-trip.
POST /api/v1/enroll → receives client_id, client_secret, webhook signing metadata (JWKS URL — no shared secret).POST /api/v1/enroll/confirm.Authorization is the one-time token, sent as a Bearer header. Body fields:
name — display name of your projectwebhook_url — must be HTTPS, public host (no localhost / private ranges)requested_scopes — subset of scopes whitelisted on the ticketpayment_code — 2 uppercase letters, used in QR purpose markers (e.g. SR)payment_purpose_template — must contain {{ID}} placeholderenvironment — production or stagingcurl -X POST https://sunrift-hub.com/api/v1/enroll \
-H "Authorization: Bearer ent_<ONE_TIME_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Sunrift.kz",
"webhook_url": "https://sunrift.kz/api/hub/webhook",
"contact_email": "ops@sunrift.kz",
"requested_scopes": ["qr:create","identity:read","identity:write","ledger:read","webhooks:receive"],
"payment_code": "SR",
"payment_purpose_template": "SR-{{ID}}-K00",
"environment": "production"
}'Response (HTTP 201):
{
"ok": true,
"data": {
"project": { "id": "uuid", "slug": "sunrift-kz", "name": "Sunrift.kz" },
"oauth": {
"client_id": "ck_live_xxx",
"client_secret": "cs_live_xxx", // shown ONCE — store immediately
"environment": "production",
"scopes": ["qr:create","identity:read","identity:write","ledger:read","webhooks:receive"],
"token_endpoint": "https://sunrift-hub.com/oauth/token"
},
"webhook": {
"url": "https://sunrift.kz/api/hub/webhook",
"signature_alg": "ed25519",
"signature_header": "X-Hub-Signature",
"signature_kid_header": "X-Hub-Signature-Kid",
"signature_timestamp_header": "X-Hub-Signature-Timestamp",
"signed_message_format": "${X-Hub-Signature-Timestamp}.${raw_body}",
"delivery_id_header": "X-Hub-Delivery",
"event_header": "X-Hub-Event",
"verification": {
"jwks_url": "https://sunrift-hub.com/api/public/jwks.json",
"notes": "Fetch JWKS (cache 5 min), pick JWK by kid, verify Ed25519(public_key, `${ts}.${raw_body}`, base64url_decode(sig)). Reject if |now-ts| > 5 min."
}
},
"defaults": { "bank_account_id": "uuid|null", "qr_template_id": "uuid|null" },
"next_step": "POST /api/v1/enroll/confirm with HTTP Basic auth (client_id:client_secret)..."
}
}client_secret is returned exactly once — persist it into your secret store immediately. The Hub keeps only a bcrypt hash and cannot recover it later.${ts}.${raw_body}; verify them with the public keys served at /api/public/jwks.json (cache 5 min, pick the JWK whose kid matches X-Hub-Signature-Kid). No shared secret to store, lose, or rotate.Once your service has stored the credentials and (optionally) verified an inbound test webhook, mark the project as active using HTTP Basic auth with the new credentials:
curl -X POST https://sunrift-hub.com/api/v1/enroll/confirm \ -u "ck_live_xxx:cs_live_xxx"
From now on, follow the standard OAuth2 client_credentials flow. Tokens are short-lived (~1h); cache them and refresh on 401.
curl -X POST https://sunrift-hub.com/oauth/token \ -u "ck_live_xxx:cs_live_xxx" \ -d "grant_type=client_credentials&scope=qr:create identity:write"
401 invalid_token — token missing, malformed, expired, or already used403 invalid_scope — none of the requested scopes are whitelisted on the ticket400 invalid_request — body validation failed (Zod issues joined with ; )500 server_error — internal failure; safe to retry the same token (it remains unconsumed unless a project was successfully created)