# Quickstart: your first order in 10 minutes

This page takes you from zero to a personalized order inside Meykt using nothing but `curl`. The base URL of the API is `https://api.meykt.com/v1`; every request is authenticated with an API key in the `Authorization` header. Start in the **test** environment — test orders are visible in the order list but never reach production.

## Step 1: Create a test key

In the Meykt dashboard go to **Integrations → API access → "Create key"**.

1. Choose the environment **Test**. Test keys start with `mk_test_`, live keys with `mk_live_`.
2. Select the scopes you need. For this quickstart: `orders:write` (send orders), `orders:read` (read them back), `files:write` (upload files). There is **no scope hierarchy** — `orders:write` does not include `orders:read`.
3. Copy the key immediately. The plaintext key is shown **exactly once**. If you lose it, create a new key and revoke the old one.

Keep the key out of your source code and out of URLs — pass it only as a header.

```bash
export MEYKT_API_KEY="mk_test_a1b2c3d4e5f6g7h8i9j0k1l2"
```

## Step 2: Verify the connection

`GET /v1/ping` confirms which key and which environment you are actually using. It requires no scope.

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

Response `200`:

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

Every authenticated response carries the rate limit headers `X-RateLimit-Limit: 120`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the window resets). The limit is 120 requests per minute per key.

If you get `401` with the code `invalid_api_key`, the header is malformed or the key is unknown, revoked or expired — all four cases return the same code on purpose. See [/developers/errors/invalid_api_key](/en/developers/errors/invalid_api_key).

## Step 3: Send your first order

`POST /v1/orders` creates an order. `external_order_id` is your own order number and is the key for duplicate protection.

```bash
curl -X POST https://api.meykt.com/v1/orders \
  -H "Authorization: Bearer $MEYKT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_order_id": "ORDER-2026-1044",
    "customer": { "name": "Anna Beispiel" },
    "items": [{
      "sku": "BECHER-GROSS",
      "name": "Becher gross",
      "quantity": 2,
      "personalization": { "gravur": "Alles Gute", "name": "Anna" }
    }]
  }'
```

Response `201`:

```json
{
  "data": {
    "order": {
      "id": "8f2c1d0a-6b44-4c11-9a3e-1f0c9d7a5b21",
      "external_order_id": "ORDER-2026-1044",
      "order_number": "ORDER-2026-1044",
      "status": "pending",
      "is_test": true,
      "currency": null,
      "total_amount": null,
      "ordered_at": "2026-08-07T09:12:04.311Z",
      "created_at": "2026-08-07T09:12:44.512Z",
      "updated_at": "2026-08-07T09:12:44.512Z",
      "customer": { "name": "Anna Beispiel", "email": null, "phone": null },
      "items": [
        {
          "id": "b7413f52-0e9a-4d77-8c62-2a5f6c31d904",
          "sku": "BECHER-GROSS",
          "name": "Becher gross",
          "quantity": 2,
          "price": null,
          "product_linked": false,
          "production_status": null,
          "personalization": { "gravur": "Alles Gute", "name": "Anna" },
          "files": []
        }
      ]
    },
    "warnings": [
      {
        "code": "item_product_unmatched",
        "field": "items[0].sku",
        "value": "BECHER-GROSS",
        "message": "SKU \"BECHER-GROSS\" is not linked to any product yet. The order was accepted; link it in the dashboard under Products → assignment queue."
      }
    ]
  }
}
```

Two things to note:

- **Warnings are not errors.** `product_linked: false` plus `item_product_unmatched` means Meykt does not know that SKU yet. The order was still accepted. Link the SKU once in the dashboard under **Products → assignment queue**; from then on it matches automatically.
- **Orders are all-or-nothing.** If any item is invalid, the whole order is rejected with `422` `validation_failed` and nothing is created. The response lists *all* field errors at once, with dot-notation paths such as `items.0.quantity`. See [/developers/errors/validation_failed](/en/developers/errors/validation_failed).

## Step 4: Check the order in the dashboard

Open **Orders** in the dashboard. The new row carries the **Test** badge. Open it: customer, quantities, the personalization fields and any attached files are all there.

Test orders behave differently from live orders on purpose:

- They appear **only** in the order list — not on the dashboard home, not in statistics, not in billing counters.
- They never start production and never trigger workflows or webhooks.
- Test and live are separate numbering spaces: `ORDER-2026-1044` used in test does **not** block the same number in live.

## Step 5: Retry the same request

Send the exact same request from step 3 again. You get `200` — not a duplicate, not a `409`.

```bash
curl -X POST https://api.meykt.com/v1/orders \
  -H "Authorization: Bearer $MEYKT_API_KEY" \
  -H "Content-Type: application/json" \
  -d @order.json
```

The body has the same shape as in step 3; `data.order.id` is identical and `data.warnings` is empty (the warning was reported when the order was first created). Fields are omitted below for brevity:

```json
{
  "data": {
    "order": {
      "id": "8f2c1d0a-6b44-4c11-9a3e-1f0c9d7a5b21",
      "external_order_id": "ORDER-2026-1044",
      "status": "pending",
      "is_test": true
    },
    "warnings": []
  }
}
```

This is what makes retries safe: after a timeout or a dropped connection, resend the identical request. Same `external_order_id` **and** same content → the existing order is returned with `200`.

Same `external_order_id` but **different** content → `409` `order_already_exists_with_different_content`. Orders are never overwritten through the API. Do not invent a new order number to work around this — use a new `external_order_id` only for a genuinely new order, and change existing orders in the dashboard. There is no endpoint for updating an order that was already sent. See [/developers/errors/order_already_exists_with_different_content](/en/developers/errors/order_already_exists_with_different_content).

## Step 6: Go live

1. Create a second key in **Integrations → API access**, this time with the environment **Live** (prefix `mk_live_`), with the same scopes.
2. Replace the key in your system. Nothing else changes: same base URL, same paths, same request bodies.
3. Verify with `GET /v1/ping` that the response now says `"environment": "live"`.

From that point on, orders you send are real: they start production, appear in statistics, trigger automation workflows and, if you have registered endpoints, emit the `order.received` webhook.

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

> Keep both keys. The test key stays useful for every future change to your integration — you can rehearse the full flow without touching the shop floor.

## Next steps

- [Authentication](/en/developers/authentication) — key format, scopes, rate limits, test vs. live.
- [Orders](/en/developers/orders) — full field reference, limits (max. 200 items, quantity 1–10,000, max. 50 personalization fields per item), listing with `updated_since` and cursor paging.
- [Files](/en/developers/files) — the two-step upload: `POST /v1/uploads`, then `PUT` the file content to the returned `upload_url`, then reference the `upload_id` in `items[].files`.
- [Webhooks](/en/developers/webhooks) — event types, signature verification, retries. There is deliberately **no** "shipped" event, because Meykt has no shipping flow. If you cannot expose a public endpoint, poll `GET /v1/orders?updated_since=…` instead — it is a fully supported path.
- [Error reference](/en/developers/errors) — every stable error code with cause and fix. Each API error response also carries a `doc_url` pointing directly at the matching page.
- [OpenAPI specification](https://api.meykt.com/v1/openapi.json) — the machine-readable source; generate a client from it instead of transcribing endpoints.
