Authentication
Every API request is authenticated with an API key sent as an HTTP Bearer token. Each key has an explicit, non-empty scope set, while quota and rate limits remain shared by the account.
Sending your key
$ curl -H "Authorization: Bearer sr_live_…" https://api.worksinprod.dev/v1/catalog/sports
Buyer credentials are accepted only through the standard
Authorization: Bearer scheme. Never place a key in a URL, query
parameter, cookie, or custom header.
Key anatomy
| Property | Value |
|---|---|
| Format | sr_live_ + 32 random characters (192 bits of entropy) |
| Issued | The primary key is delivered through a private one-time claim after confirmed admission; timing depends on access verification and service availability. Additional keys can be created from the dashboard while account and key limits allow. |
| Storage (our side) | Only a SHA-256 hash is used for key lookups. Server-decryptable ciphertext exists only until an initial claim is first delivered or expires. Interrupted claim, rotation, and account-recovery deliveries use short-lived caller-sealed bytes that cannot be opened without the exact client-held bearer or credential pair. Recovery-email verification is a separate hash-only capability and never contains an API key. |
| Limit | Up to 5 active keys per account |
Least-privilege scopes
| Scope | Allows |
|---|---|
data:read | Calls to the Pinnacle-sourced data endpoints under /v1. |
account:manage | Account details, usage, key inventory, creation, revocation, rotation, recovery-email verification resend, and hosted billing management. |
Initial primary keys and support-issued keys receive both scopes. Additional
buyer keys default to data:read, which is safest for production
workers because a leaked worker key cannot inspect the account, mint broader
credentials, revoke another key, or rotate itself. An account-managing key may
explicitly request any valid non-empty subset:
$ curl -X POST https://api.worksinprod.dev/v1/account/keys \
-H "Authorization: Bearer sr_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"production poller","scopes":["data:read"]}'
Keeping keys safe
- Server-side only. Never embed a key in browser JavaScript, mobile apps, or anything shipped to end users — anyone with the key spends your quota.
- Environment variables or a secrets manager, never source control. If a key ever reaches a git history, rotate it immediately.
- One key per system (e.g. production, staging, research). With separate keys you can revoke one without touching the others — and the dashboard shows per-key last-use times.
Rotating and revoking keys
Rotation requires account:manage, preserves the calling key's
exact scopes, and immediately revokes the old key. There is no overlap
window, so rotate when you can deploy the new key promptly. Every rotation
also requires a fresh idempotency nonce: rot_ followed by the
canonical unpadded base64url encoding of exactly 32 cryptographically random
bytes.
$ curl -X POST https://api.worksinprod.dev/v1/account/keys/rotate \ -H "Authorization: Bearer sr_live_OLD…" \ -H "Idempotency-Key: rot_BASE64URL_OF_32_RANDOM_BYTES" # → { "api_key": "sr_live_NEW…", "scopes": ["data:read", "account:manage"], # "recovery_expires_at": "…", "message": "…" }
Keep the old key and nonce in protected memory until the new key is stored.
If the response is interrupted, retry with that exact old key and exact
nonce before recovery_expires_at (15 minutes). The old key stays
revoked; this pair can recover only the already-created replacement. Never
generate a new nonce for an ambiguous attempt.
You can also create extra keys (POST /v1/account/keys) and
revoke individual keys (POST /v1/account/keys/{id}/revoke) — all
available in the dashboard with one click, no curl needed.
Verify recovery before you need it
Your account email address has no lost-key recovery authority until someone
with access to that mailbox explicitly verifies it. This does not delay normal
API use and it does not reveal, create, revoke, replace, or email an API key.
Check the current state in the dashboard or in
GET /v1/account:
{
"recovery_email": {
"verified": false,
"verified_at": null
}
}
New buyers receive a separate verification message after provisioning.
While you still have an account:manage key, you can request another
link from the dashboard or with an exactly empty JSON object:
$ curl -X POST https://api.worksinprod.dev/v1/account/recovery-email/verification/resend \
-H "Authorization: Bearer sr_live_…" \
-H "Content-Type: application/json" \
-d '{}'
The endpoint is durably limited to one request per customer every 10 minutes.
It returns the same generic 202 when verification is already complete
or a resend is cooling down, so acceptance does not prove that a new message was
sent.
The single-use link expires after 24 hours and keeps its emv_…
bearer in the /verify-recovery-email URL fragment. The first-party
page removes that fragment before any request and asks for explicit confirmation.
Only verify a mailbox controlled by the buyer. If you received the message but
are not the buyer, ignore it; never forward the link or paste it into support,
chat, logs, or analytics. Verification advances a separate recovery-authority
generation. Every later recovery link is bound to that exact generation and
account address; replacing or resetting the address permanently invalidates
older links, and recovery stays disabled until the new mailbox is verified
separately.
Lost every management key?
If no active key with account:manage remains, open the
secure account-recovery page. After a hostname-bound
Turnstile challenge, the page always gives the same accepted response; it does
not reveal whether an address has an account or is verified. The synchronous
request is placed in a fixed-size encrypted blind queue without an account
lookup and is resolved asynchronously; allow a few seconds for delivery. A request for an
unverified mailbox issues nothing. Only an eligible account whose exact current
account mailbox was verified beforehand can receive a 30-minute same-origin
fragment link by email. Delivery, claim, and interrupted-response retry all
fail closed if the verified address or its recovery-authority generation has
changed since that link was issued.
Claiming that link revokes every existing API key and creates exactly
one replacement with both scopes. If delivery is interrupted, the exact same
link bearer can return that replacement for 10 minutes. Paid accounts must be
active or past_due inside their grace window; private-beta access
must not have reached its fixed expiry. Suspended, canceled, expired, held,
unverified, or reconciliation-required accounts must contact support. The
replacement appears only on the first-party page and is
never emailed. Never paste a recovery link or bearer into a support ticket.
Managing billing
Paid Stripe-backed accounts can update payment details, view invoices, or use the configured cancellation flow from a fresh hosted billing session. Open it from the dashboard, or call the authenticated endpoint below. The API chooses the Stripe customer and return address from your account; it accepts no redirect input.
$ curl -X POST https://api.worksinprod.dev/v1/account/billing-portal \ -H "Authorization: Bearer sr_live_…" # → { "portal_url": "https://billing.stripe.com/p/session/…" }
The URL is short-lived. Open it immediately and never log, cache, bookmark, or share it. This endpoint is not registered during private beta. Contact support for plan changes.
When authentication fails
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_api_key | Missing, malformed, or revoked key. Check the header and that the key is still active. |
| 403 | insufficient_scope | The key is valid but lacks the capability required by this endpoint. Use a correctly scoped key; do not broaden worker keys unnecessarily. |
| 403 | account_suspended | Your account was suspended, expired, or the subscription ended. Contact support for the applicable access review. |
Every error response includes a request_id. Quote it when
contacting support and we can trace the exact request. See
Errors & retries.