Authentication & keys
A private key alone, bound to one store, carrying scopes — and every refusal it can meet.
Base URL:
https://api.usequeek.com/api/v1/merchantEvery request carries the store's private API key in X-Client-Key, and should ask for JSON:
curl https://api.usequeek.com/api/v1/merchant/store \
-H "X-Client-Key: sk_live_..." \
-H "Accept: application/json"That header is the whole credential. There is no OAuth flow, no token exchange,
no session beside it — a key presented next to a Bearer [REDACTED] or a session only
narrows that credential, exactly as in the dashboard. And the key belongs to one
store: no vendor header and no vendor_id is sent, because there is nothing to
choose. A header naming another store is refused outright.
One kind of key
The merchant mints the key in the dashboard under Settings → API keys (the Connections list) and chooses its scopes at mint time. Only a private key reaches this API:
| Key | Reaches the Merchant API |
|---|---|
sk_live_… | yes — its one store, within its scopes |
sk_test_… | yes — its dev store only; keys never cross modes |
pk_live_…, pk_test_… | never — a public key is a browser credential |
A private key needs no Origin, and it must never appear in a browser bundle, a
public repository, or a client-side environment variable. A pk_ key sent here
is a 403 private_key_required, with the fix named: mint an sk_ key.
Going live is swapping the key — a _test_ key on a live store, or a _live_
key on a dev store, is a 401 api_key_mode_mismatch. No path, header or body
changes.
Scopes are the whole permission
A key's scopes decide every operation it may call — each operation requires exactly one scope, stated on the operation itself in the reference and collected in the scopes table. A key acts for its whole store, never only for the records one person created; a write scope also returns the record it wrote in that write's own response, while listing or reading records needs the matching read scope.
Two limits on what a key can hold, enforced at mint time and on every call:
- The minter can only grant scopes they hold themselves — nobody can mint a key broader than their own permission.
- Some families are never grantable:
api_keys(a key that could mint a broader key would make every scope cosmetic),roles,users,employeesandpos. Those routes stay dashboard-only.
Customer contact is a separate scope
Customer rows arrive with their contact fields — name, email, phone and address —
set to null unless the key deliberately holds
merchant-customers-contact-read. Ids, counts, dates and flags are unaffected.
The masking is not just cosmetic: a masked key cannot recover contact through
filters either. Using search, or sorting by name, email or phone on the
customer or follower list without the scope, is refused with
403 insufficient_scope naming the missing scope — rather than returning
wrong-looking data.
When authentication fails
The reply carries error_code — it names exactly what to fix.
error_code | Status | What it means |
|---|---|---|
invalid_client_key | 401 | The key is unknown. Check it under Dashboard → Settings → API keys. |
api_key_mode_mismatch | 401 | A _test_ key on a live store, or a _live_ key on a dev store. Keys never cross modes. |
api_key_revoked | 403 | The key was revoked. Mint a fresh one. |
api_key_expired | 403 | The key expired. Mint a fresh one. |
private_key_required | 403 | A public pk_ key was sent. Mint a private sk_ key. |
insufficient_scope | 403 | The key does not carry the scope this operation needs. The message names it. |
route_not_available | 403 | Not part of the Merchant API — the endpoints are exactly those in the reference. A key sent to a retired api/v1/biz/… path is refused here too, and the message names the new /api/v1/merchant/… path to call instead. Anything else stays dashboard-only. |
VENDOR_ACCESS_DENIED | 403 | A vendor header named a different store than the key's. Drop the header: the key already names its store. |
plan_inactive | 403 | The store's plan does not include API access. |
VENDOR_ACCESS_DENIED answers in the older {"error", "message"} shape rather
than the error.code envelope — see Errors for both
shapes. An insufficient_scope failure is the merchant's to fix, not yours:
they mint a key with the scope, or narrow the integration to the scopes the key
holds.
Refused credentials are throttled per IP — past the limit the address answers
429 before the refusal is even worked out. A live key is never throttled
there, not even from an address that is over the limit. See
Idempotency & rate limits for the
quotas a live key draws from.