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_…) belongs to the
browser-safe Storefront and Customer APIs and is refused here
with invalid_credential — the message does not say which kind was
expected, because naming it would confirm to a probe that the key is real.
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, and no reason at all to hold write:subscriptions.
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, and one holding
write:subscriptions can cancel any contract on the shop. 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
Six 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
write:subscriptions ← the only write scope that exists| 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 |
write:subscriptions | Pause, resume, cancel, skip, reschedule and line edits on /subscriptions/{contract_gid}/… |
write:subscriptions is off by default
It is the one scope the minting form leaves unticked, and ticking it raises a warning. Nobody ticks a
box they were never shown, so a write scope that arrived on the sensible default would land on every
key minted for a reporting job — the same reasoning that keeps read:customers out of the defaults,
applied to the ability to end a contract.
Two consequences worth holding on to:
write:does not implyread:. A key holding onlywrite:subscriptionscan pause, resume, cancel, skip, reschedule and edit lines, and cannot read a single contract. That is why a write answers with a receipt rather than the resource.- Every subscription write requires an
Idempotency-Key. Not honoured-if-present — required. A re-sent skip would move the next charge a second interval and leave nothing behind to say so.
The remaining write scopes — write:plans, write:bundles, write:mixmatch, write:gifts and
write:winback — are published vocabulary, not working endpoints. 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.