# Store API keys

Connect a store's own systems — an ERP, a warehouse, an accounting export — straight to the Orbit API with a credential the merchant mints themselves. No partner account, no plugin, no OAuth handshake.

## When to use an API key (and when to build a plugin)

Everything else on this site describes **plugins**: software one party builds and many merchants install, authorised through OAuth consent. That machinery earns its keep when there is distribution — a marketplace listing, many stores, per-install consent.

An API key is for the other case: **one store, connecting to systems its own team runs or commissioned**. There is nothing to distribute and nobody to consent except the merchant, who creates the key in their own dashboard.

| | Store API key | Plugin |
|---|---|---|
| Who creates the credential | The merchant, in their dashboard | Issued at install, via OAuth |
| Stores it works for | Exactly one | Every store that installs |
| Browser required to obtain it | No | Yes (the install + iframe handshake) |
| Lifetime | Long-lived, until revoked | Access token ~90 days, refresh-rotated |
| Listed on a marketplace | No | Optionally |

If you are building for many stores, stop here and read [Getting started](/guides/getting-started). If you are wiring up one store you control, read on.

## Creating a key

In the store dashboard: **Settings → API Keys → Create API key**.

1. **Name it after the system that will hold it** — "Sage 200 connector", "Warehouse sync". The name appears in the store's Activity Log next to everything the key changes, so make it recognisable.
2. **Pick scopes.** The key can only call endpoints its scopes cover — same vocabulary and enforcement as plugin tokens, documented in the [Scope Catalog](/scope-catalog). Grant the least the system needs.
3. **Copy the key immediately.** It is shown exactly once, at creation. Orbit stores only a fingerprint, so a lost key cannot be recovered — only revoked and replaced.

Keys look like `oc_sk_…` followed by 43 characters. A store can hold up to 10 active keys — enough for one per connected system with room to rotate. An optional expiry date is available at creation; by default a key stays valid until revoked.

## Making calls

Send the key as a bearer token to the public API. The store is derived from the key itself — no store id header or parameter is needed on `/v1` endpoints.

```http
GET /v1/orders?limit=20 HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer oc_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

```ts
const response = await fetch(
  'https://api.myorbitcommerce.net/v1/orders?limit=20',
  { headers: { Authorization: `Bearer ${process.env.ORBIT_API_KEY}` } },
);
const { data } = await response.json(); // { items, meta }
```

Responses arrive in the platform's standard envelope `{ statusCode, message, data, timestamp }` — read the `data` field, exactly as described in [Authentication & tokens](/guides/authentication).

The `/v1` resource endpoints in the [API Reference](/api-reference) — products, orders, customers, taxonomies, categories, brands, navigation, blogs and blog posts, media, SEO and redirects, analytics, custom fields, carts, tracking scripts, webhooks, the B2B surfaces (companies, catalogs, payment terms, invoices), and the rest of the `/v1` surface — accept an API key, gated by the same scopes as plugin tokens. (The plugin-only surfaces — billing, email sending, plugin settings — require scopes a key cannot hold, so they stay out of reach by construction.) Polling `GET /v1/orders?updatedFrom=…` on a schedule is the simplest reliable integration shape — an on-premise service behind a corporate firewall needs nothing inbound at all.

## Scopes a key can hold

A key can be granted any resource scope the API enforces — the [Scope Catalog](/scope-catalog) documents the main families, including `product:*`, `order:*`, `customer:*`, `taxonomy:*`, `category:*`, `brand:*`, `navigation:*`, `blog:*`, `blog-post:*`, `seo:read`, `redirect:*`, `media:*`, `analytics:read`, `custom-field:*`, `cart:*`, `tracking-script:*`, and the B2B families (`company:*`, `b2b-catalog:*`, `payment-term:*`, `b2b-invoice:*`) — plus `webhook:*` for managing the store's own [webhook subscriptions](/guides/webhooks).

A few families are **not** grantable to keys. `billing:*` (plugin monetisation), `email:send` (sends attributed to a plugin), and `settings:*` (a plugin's own settings storage) only make sense for an installed plugin. `page:*` and `store-settings:*` are granted only through the consent flow a merchant completes when connecting an AI assistant to their store, and `import:*` is reserved with no endpoint behind it — see the [Scope Catalog](/scope-catalog) for both. The dashboard's scope picker simply does not offer any of them; the API rejects them with `400` if requested directly.

## Rate limits

Limits are per key, per minute, and generous enough for bulk work:

| Requests | Limit |
|---|---|
| Reads (`GET`) | 600 / minute |
| Writes (`POST`, `PATCH`, `DELETE`) | 300 / minute |

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (unix time). Past the limit you receive `429 Too Many Requests` with a `Retry-After` header — back off until then.

## Errors you will meet

| Status | Meaning |
|---|---|
| `401 Unauthorized` — `Invalid or expired token` | The key is malformed, revoked, expired, or does not exist. The message is deliberately identical for all of those. |
| `401 Unauthorized` — `Missing required scopes: …` | The key is valid but was not granted the scope this endpoint needs. The body names the missing scope. |
| `404 Not Found` | The id does not exist **in this store**. Another store's resource ids answer the same 404 as ids that never existed. |
| `429 Too Many Requests` | Rate limit hit; honour `Retry-After`. |

## Lifecycle

**Revocation is immediate.** Deleting a key in the dashboard takes effect on its next request — there is no cache to wait out. If a key may have leaked, revoke first and rewire after.

**Scopes are fixed at creation.** A key's access never changes after it is minted — there is deliberately no way to edit scopes on a live key, because widening them would silently escalate a credential some system already holds, and it would blur the activity log's answer to "what could this key do at the time?". To change access, use the rotation recipe below: create a new key with the scopes you want, switch your system over, revoke the old one.

**Rotation is a recipe, not a mechanism.** Keys do not rotate themselves — an unattended service has nobody present to re-consent when an automatic rotation fails, so Orbit deliberately keeps keys long-lived. When you want to rotate: create the successor key, deploy it to the system, confirm traffic, then revoke the predecessor. Two keys can be live side by side for as long as the migration needs.

**Watch *last used*.** The key list shows when each key last made a call. A key whose activity you cannot account for should simply be revoked — creating a replacement costs a minute.

**Writes are attributed.** Every change a key makes appears in the store's Activity Log under the key's name, alongside changes made by staff and plugins. Name keys so that log reads well.

## Security practices

- **Server-side only.** An API key in a browser, mobile app, or public repository is public. Keep it in your server's secret manager or environment.
- **One key per system.** Separate keys for the ERP and the warehouse mean revoking one does not break the other — and the Activity Log tells them apart.
- **Least scopes.** A reporting job needs `order:list`, not `order:update`.
- **Never log it.** Redact the key from request logs and error reports; the `oc_sk_` prefix makes it easy to scan for.

## Next steps

- Push events out instead of polling: [Webhooks](/guides/webhooks) — a key with `webhook:*` scopes manages the store's own subscriptions, signed with a per-subscription secret.
- The full endpoint surface: [API Reference](/api-reference).
- The exact scope strings: [Scope Catalog](/scope-catalog).
