Authentication & scopes
Every call to the Curobi Admin API carries a secret key as a bearer token.
Authorization: Bearer curobi_sk_live_7f3aQ2…There is no other credential type on this surface. A publishable key (curobi_pk_…, reserved for
the future browser-safe Storefront API) is refused here with
invalid_credential.
Minting a key
Keys are created in your Curobi admin on the API keys page.
Name it after the system that will use it
“Netsuite nightly sync” tells you what breaks when you revoke it. “Key 1” does not.
Grant only the scopes that system needs
Scopes are checked per endpoint. A BI pull that only reads subscriptions has no reason to hold
read:customers.
Copy the key immediately
It is shown once. Curobi stores only sha256(pepper + token), so nobody — including us — can
recover the original. Lost a key? Revoke it and mint a new one.
Set an expiry if the key is for a contractor or a one-off migration
An expired key fails with credential_expired instead of quietly
outliving the engagement.
A secret key is a full-power read credential for your subscription data. Keep it in a secrets
manager or an environment variable — never in client-side JavaScript, a mobile app bundle, a
theme file, or a committed .env. Anything a browser can read, a customer can read.
What gets checked, and in what order
Five facts are checked on every single request, in this order. The order matters: the auth ladder
runs before an endpoint looks at its own scope, so a downgraded shop with a fully-scoped key
sees plan_upgrade_required rather than insufficient_scope.
| # | Check | Failure code | Status |
|---|---|---|---|
| 1 | A Authorization: Bearer header is present | missing_credential | 401 |
| 2 | The key resolves, and is neither revoked nor expired | invalid_credential · credential_revoked · credential_expired | 401 |
| 3 | Curobi is still installed on the shop | shop_unavailable | 401 |
| 4 | The rate-limit bucket has a token left | rate_limited | 429 |
| 5 | The shop’s plan includes the API | plan_upgrade_required | 403 |
| 6 | The key holds the scope this endpoint requires | insufficient_scope | 403 |
The plan is re-read on every request, cached for at most 60 seconds. A shop that downgrades from Pro loses API access within the minute, not at the next key rotation. Plan and scope are separate facts and neither substitutes for the other.
Scopes
Exact string matching. No wildcards, no hierarchy, and write: does not imply read:.
read:subscriptions read:plans read:bundles read:mixmatch
read:gifts read:winback read:notifications
read:analytics read:customers manage:webhooks| Scope | Unlocks |
|---|---|
read:subscriptions | GET /subscriptions, GET /subscriptions/{contract_gid} |
read:plans | GET /plans |
read:bundles | GET /bundles, GET /bundles/{id}/boxes |
read:mixmatch | GET /mix-match, GET /mix-match/{id} |
read:gifts | GET /gifts |
read:winback | GET /winback/campaigns, GET /winback/campaigns/{id}/sends |
read:notifications | GET /notifications |
read:analytics | GET /analytics/mrr, /churn, /cohorts, /recovery |
read:customers | Customer names, email addresses and gift messages on the resources above |
manage:webhooks | The whole /webhooks/* family — the only write in v1 |
The write:* scopes named in the API design are published vocabulary for a later phase. They
are not offered when you mint a key, because a checkbox for an endpoint that does not exist would
mint keys whose stated power is fiction.
read:customers is separate on purpose
It is the only scope that returns Protected Customer Data: customer names, email addresses, and gift messages.
A reporting pull can page through every subscription, gift and notification without it — those
resources are keyed by Shopify customer GID, which identifies a customer without describing them.
Keep read:customers off the BI key, and the warehouse gets the full picture with none of the
liability.
Without read:customers, PCD fields come back as null rather than as a "[redacted]"
placeholder. That’s deliberate: paired with the scopes you know your own key holds, you can tell
“there is no value” from “the value is withheld”.
Gift claim tokens are never serialized, at any scope. A claim token is a bearer credential for the recipient’s claim page — returning it would turn a merchant’s read key into the ability to accept or decline gifts on a customer’s behalf.
Revoking
Revoke a key from the API keys page at any time. The next request with it fails with
credential_revoked; there is no grace period.
Uninstalling Curobi revokes every key for the shop immediately. An uninstalled merchant can’t reach the admin to revoke their own keys, so a key that outlived an uninstall would be a live credential nobody could turn off. Uninstalling also disables every webhook endpoint — revoking credentials stops other people calling us, disabling endpoints stops us calling other people.
Key hygiene
- One key per integration. Shared keys can’t be revoked without collateral damage, and “last used” tells you nothing when four systems share one credential.
- Rotate by overlap: mint the new key, deploy it, confirm traffic on it, then revoke the old one. Revoking first means downtime.
- The API keys page shows each key’s prefix and last used time, so you can find the dead one before you revoke the live one. Last-used is written at most once a minute per key.