# 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**, such as "Sage 200 connector" or "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, with the 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, so 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) accept an API key, gated by the same scopes as plugin tokens: products, orders, customers, taxonomies, categories, brands, navigation, blogs and blog posts, media, SEO and redirects, analytics, custom fields, carts, webhooks, the B2B surfaces (companies, catalogues, payment terms, invoices) and the rest of the `/v1` surface. 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 the resource scopes the API enforces, except the families below. 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:*`, 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), `settings:*` (a plugin's own settings storage) and `customer-events:create` (timeline events recorded under a plugin's name) only make sense for an installed plugin. Writes that change what every shopper sees, rewrite the catalogue in bulk, or change who controls the shop (`page:*`, `store-settings:*`, `tracking-script:*`, `import:*`, `theme:*`, `template:*`, `currency:*`, `domain:*`, `plugin:*`, `global-overlay:*`, `team:*`, `custom-field-definition:*`, `site-verification:update`) are only ever granted on a screen where the merchant approves them by name (see the [Scope Catalog](/scope-catalog)), so a key cannot hold them either. The dashboard's scope picker lists exactly the scopes the API accepts for a key, read from the API itself, so it never offers any of these; 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. It also deletes the key's [webhook subscriptions](/guides/webhooks). 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. A key's webhook subscriptions belong to that key and go when it is revoked, so subscribe again with the new key, and store each new subscription's signing secret, before you revoke the old one. While both keys are subscribed, each event arrives twice under the same event id, which your [idempotency check](/guides/webhooks#idempotency) already drops. 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).
