Authentication
Two credentials — an OAuth token for the hosted MCP server and an API key for everything else. The header forms, rate limits, and the one error envelope.
Marka has two programmatic credentials, and which one you want depends on whether there is a browser in the loop.
| OAuth 2.1 access token | API key | |
|---|---|---|
| Works at | /api/mcp | /api/mcp and /api/v1/* |
| You get it by | letting your AI client open a browser | creating one in Settings → API keys |
| Looks like | an opaque bearer token | mk_… |
| Lives for | 1 hour, refreshable for 30 days | until it expires or you revoke it |
| You revoke it in | Settings → Connected apps | Settings → API keys |
| Use it for | an AI client you sign in to | scripts, CI, anything headless |
Prefer the OAuth token where it applies. Nothing is pasted anywhere, the token is short-lived,
and disconnecting one client does not disturb any other. The whole flow is described on
MCP overview — your client discovers it from the 401 the gateway answers with.
Everything below is about the API key, which is the credential /api/v1 speaks.
Create a key under Settings → API keys;
Getting started walks through it and your first call.
Sending the key
Two header forms work, and they are equivalent:
x-api-key: mk_YOUR_KEY_HEREAuthorization: Bearer mk_YOUR_KEY_HEREx-api-key is the primary form — it is what the OpenAPI security scheme declares, what the SDK
sends, and what the hosted MCP config snippet emits. The Authorization: Bearer spelling is
accepted because it is what most HTTP clients and MCP configurations reach for. Both forms work at
both doors: /api/v1/* and /api/mcp.
Two details worth knowing:
- If both headers are present,
x-api-keywins. They are never merged. - Only
mk_-prefixed bearer tokens are treated as API keys. AnAuthorizationheader carrying anything else is left alone rather than fed to the key validator, so an unrelated token does not turn into a spurious "invalid key".
Browser sessions
/api/v1/* additionally accepts a signed-in browser cookie, so you can explore your own API from a
logged-in tab without minting a key first. This is an interactive convenience and is deliberately
not part of the contract a client programs against — treat the API key as the credential.
/api/mcp takes an OAuth access token or an API key, and not a cookie session. Its tools
mutate listings, and it stays a credential-bearing machine endpoint rather than a second,
cookie-authenticated door into those mutations. A request with no usable credential answers 401
with a WWW-Authenticate header naming the discovery document — that header is how a client that
knows nothing but the URL finds its way to the browser sign-in.
Keys cannot manage keys
Key creation, listing and revocation are a signed-in-browser surface. A request authenticated by an API key is refused there outright, so a leaked key cannot mint successor keys that would outlive revoking the original.
The same rule covers OAuth: an API key cannot start an authorization, and Settings → Connected apps is a signed-in-browser surface. A leaked key can neither mint a successor key nor trade itself for an OAuth grant that would survive revoking it.
Key lifecycle
| Prefix | mk_ — fixed, and not settable per key |
| Name | up to 32 characters |
| Expiry | 30 days, 90 days, 1 year, or never. Default: 1 year |
| Visibility | shown once at creation; only the first 10 characters are stored for display |
| Revocation | delete the key from settings |
A key that is missing, malformed, never issued, revoked, or expired all answer the same way: 401.
OAuth token lifecycle
| Granted by | you, on the consent screen, in your browser |
| Access token | 1 hour |
| Refresh token | 30 days |
| Client registration | dynamic (RFC 7591) — clients register themselves |
| PKCE | required, S256 only |
| Revocation | Settings → Connected apps → Disconnect |
Disconnecting removes the grant and every token issued under it, so it takes effect on the next
request rather than when the current access token happens to expire. A client whose token has been
disconnected gets the same 401 and discovery challenge it got before it was ever connected, so it
can simply ask again.
Rate limits
Each key carries its own request budget: 1000 requests, over a 60-second window.
That window is worth reading carefully, because it does not measure elapsed time — it measures idle time. The counter resets only after 60 consecutive seconds during which the key made no request at all; every allowed request pushes the reset out again. In practice the budget behaves like 1000 requests per burst of activity, and a client that keeps polling without ever going quiet will eventually spend it.
Exceeding it answers 429:
{ "error": "Rate limit exceeded for this API key.", "code": "rate_limited" }The remedy is to stop for a minute, or to spread work across more than one key. There is no published retry-after header on this surface.
Limits are stamped at mint time
The budget and window are recorded on each key when it is created. Changing the platform defaults affects keys minted afterwards, not keys you already hold.
Errors
Every non-2xx response on /api/v1 uses one envelope:
{
"error": "Human-readable message.",
"code": "optional_refinement"
}error is always present and is written for you — it never carries provider or database internals.
code is optional and is not an enum: it carries either an API-level code or the originating
domain error code (listing.authority_locked, subscription.tier_too_low, …), and new codes appear
without a contract change. Branch on the status first; treat code as a refinement.
Statuses
| Status | Meaning |
|---|---|
400 | Validation failed — the body or query did not match the schema |
401 | Missing, malformed, or revoked API key |
402 | The key's plan does not carry the entitlement this endpoint requires |
403 | Authenticated, but not permitted to act on this resource |
404 | No such resource, or it is not visible to this key |
429 | Rate limit exceeded for this API key |
Anything outside that set (a 502 or 503 from a failed upstream) is an operational condition, not
part of the contract.
Codes you will actually branch on
| Code | Status | When |
|---|---|---|
invalid_request | 400 | The request body or query failed schema validation, or the body was not JSON |
entitlement_required | 402 | A key-authenticated write on a plan without api_write |
forbidden | 403 | Authenticated, but the resource belongs to someone else — e.g. attaching another seller's shipping profile |
email_unverified | 403 | Publishing requires a confirmed email address on the account the key belongs to |
rate_limited | 429 | The key spent its request budget |
invalid_response | — | Raised by the SDK, not the API: a 2xx whose body was not JSON at all |
Where keys are not enough
Reading is available to any valid key. Key-authenticated writes — createListing,
updateListing, publishListing — additionally require the api_write entitlement, which is a
Pro-or-higher feature. See Plans & limits.