MeyktAPI Documentation
OpenAPI specllms-full.txtDashboard
  • Overview
  • Quickstart
  • Authentication
  • Orders
  • Files
  • Webhooks
  • Errors

Authentication

Every request to https://api.meykt.com/v1 is authenticated with an API key sent in the Authorization header. A key belongs to one Meykt organization, carries a fixed set of scopes, and is bound to one environment — live or test. There is no session, cookie, or OAuth flow in v1.

#API key format

A key looks like this:

text
mk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
mk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
  • mk_ is a fixed marker so leaked keys are machine-detectable by secret-scanning services.
  • live or test is part of the key itself, so a key can never be used against the wrong environment by accident.
  • The remainder is 32 random characters.

The first 14 characters (mk_live_a1b2c3) are the key prefix. The prefix is not secret: it is shown in the key list in the dashboard and returned by GET /v1/ping as key_prefix, so you can tell which key a running system is actually using.

Keys are created in the Meykt dashboard under Integrations → API access.

#Authorization header

Send the key as a Bearer token on every request:

bash
curl https://api.meykt.com/v1/ping \
  -H "Authorization: Bearer mk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

GET /v1/ping requires no scope and is the correct first call of any integration. It answers "did I pick the right key and the right environment?" in one line:

json
{
  "data": {
    "organization": { "name": "Musterwerkstatt" },
    "environment": "test",
    "scopes": ["orders:read", "orders:write", "files:write"],
    "key_prefix": "mk_test_a1b2c3",
    "api_version": "v1"
  }
}

One exception: the PUT request that uploads file content to an upload_url from POST /v1/uploads is authenticated by the signed URL itself. Do not send your API key to that URL.

#Never send the key in a URL

The API only reads the Authorization header. There is no ?api_key= query parameter, and adding one does not work.

This is deliberate. Query strings end up in places you do not control:

  • server and proxy access logs, on both sides of the connection
  • browser history and Referer headers
  • error trackers, screenshots, and pasted support tickets
  • CDN and cache keys

A header is not stored in any of those by default. Keep keys in environment variables or a secret store, never in source control, front-end code, or a URL.

#One-time display

The plaintext key exists exactly once: in the response of the "create key" action in the dashboard. Meykt stores only a hash of it and can never show or recover the plaintext again.

If a key is lost or exposed:

  1. Create a new key with the same scopes and environment.
  2. Deploy the new key to your system.
  3. Revoke the old key in the dashboard. From that point on it is rejected.

Revoking is immediate and applies to every request made with that key.

#Scopes

Each key carries an explicit list of scopes. A scope always describes what your system is allowed to do at Meykt — never what Meykt is allowed to do in your system. Meykt does not call into your systems at all, with one exception you configure yourself: webhook deliveries to URLs you register.

ScopeWhat a key with this scope may do
orders:readYour system may read and list the orders it created at Meykt through this connection.
orders:writeYour system may send orders to Meykt.
files:writeYour system may request upload URLs and reference the resulting files on order items.
webhooks:manageYour system may create, list, test and delete webhook endpoints and read the delivery log.

Which endpoint needs which scope:

EndpointRequired scope
GET /v1/pingnone
POST /v1/ordersorders:write
GET /v1/ordersorders:read
GET /v1/orders/{id}orders:read
POST /v1/uploadsfiles:write
POST /v1/webhook-endpointswebhooks:manage
GET /v1/webhook-endpointswebhooks:manage
DELETE /v1/webhook-endpoints/{id}webhooks:manage
POST /v1/webhook-endpoints/{id}/testwebhooks:manage
GET /v1/webhook-deliverieswebhooks:manage

Read access is additionally bound to the connection the key belongs to: a key only ever sees the orders that were created through it. An order id that belongs to a different channel or a different organization answers 404 not_found, never 403 — a 403 would confirm that the id exists.

#There is no scope hierarchy

orders:write does not include orders:read. A system that sends orders and later polls their status needs both scopes checked on the same key. Implicit rights are a common source of surprises in access systems, so they were left out on purpose.

A missing scope is rejected with 403 insufficient_scope, and the message names exactly which scopes are missing:

json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key is missing the required scope(s): orders:read.",
    "doc_url": "https://www.meykt.com/en/developers/errors/insufficient_scope"
  }
}

Scopes cannot be changed on an existing key. Create a new key with the right scopes and retire the old one.

#Every rejected key is one error

Missing header, unknown key, revoked key, expired key, malformed key — all of them return the same response:

json
{
  "error": {
    "code": "invalid_api_key",
    "message": "Missing or invalid API key. Send it as \"Authorization: Bearer mk_live_…\".",
    "doc_url": "https://www.meykt.com/en/developers/errors/invalid_api_key"
  }
}

HTTP status is always 401. This is intentional: distinguishing "unknown key" from "revoked key" would confirm to anyone probing the API that a given key exists or once existed. The distinction is recorded on the Meykt side, not returned.

Practical consequence for your error handling: on 401, do not retry with a backoff — nothing about the request will change. Check the header spelling, check the environment prefix, and check in the dashboard whether the key is still active. See invalid_api_key.

#Test vs. live

The environment is decided by the key, not by a flag in the request body. A mk_test_ key writes into the same organization and matches against your real products and mappings, so the test exercises the identical code path as production.

What a test order does:

  • appears in the order list in the dashboard, marked with a "Test" badge
  • carries is_test: true in the API response
  • resolves personalization, files, and product matching exactly like a live order

What a test order never does:

  • start production of any kind
  • appear in the dashboard home screen or in any statistics
  • trigger workflows
  • count towards billing

Test and live are separate number spaces. ORDER-2026-1044 sent with a test key does not block ORDER-2026-1044 sent later with a live key — the duplicate protection on external_order_id is scoped per environment. This means you can rehearse a real order number and then send it for real.

Going live is a key swap: create a second key with environment live and the same scopes, replace the value in your system, and verify with GET /v1/ping that environment now reads "live".

Test keys are not a sandbox with fake data. They write real order rows into your real organization; they are only excluded from production, statistics, workflows, and billing.

#Rate limits

The limit is 120 requests per minute per key, counted across all endpoints. Two keys of the same organization have independent budgets.

Every response that got past key verification carries the current state (a 401 for an unknown key and a 503 cannot carry them — there is no key to count against yet):

HeaderMeaning
X-RateLimit-LimitRequests allowed per window, currently 120
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetSeconds until the window resets, currently 60

Exceeding the limit returns 429 with code rate_limited and a Retry-After header in seconds:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60

Handling rule: on 429, wait for Retry-After seconds and repeat the identical request. Repeating a POST /v1/orders is safe — the same external_order_id with the same content returns the existing order with 200 instead of creating a duplicate.

If you are hitting the limit, the usual cause is polling one order at a time. Replace it with one list call per interval:

bash
curl "https://api.meykt.com/v1/orders?updated_since=2026-08-07T09:00:00Z&limit=100" \
  -H "Authorization: Bearer $MEYKT_API_KEY"

Or register a webhook endpoint so Meykt pushes status changes to you instead.

#Next steps

  • Send your first order
  • Attach files to order items
  • Receive status events
  • Error reference

Machine-readable version of this page: https://www.meykt.com/developers/authentication.md

On this page

  • API key format
  • Authorization header
  • Never send the key in a URL
  • One-time display
  • Scopes
  • There is no scope hierarchy
  • Every rejected key is one error
  • Test vs. live
  • Rate limits
  • Next steps

Base URL https://api.meykt.com/v1 · Machine-readable: openapi.json · llms-full.txt

Imprint · Privacy · Terms