> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinmarkt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Merchant orders

> List, read, and update store orders with a Bearer API key.

Base URL: `https://dash.joinmarkt.com`

```txt theme={null}
Authorization: Bearer mk_live_...
Accept: application/json
```

The user must be able to access `{storeId}`. Store scope `SELECTED` must include that shop. `ALL_ACCESSIBLE` does not add a shop list. Access mode `RESTRICTED` must include the route scope exactly. Access mode `FULL` skips the scope list. Both modes still require the dashboard ceiling in the table.

| Scope | Ceiling | Routes |
| - | - | - |
| `order:read` | `viewOrders` | `GET` list, `GET` one |
| `order:update` | `editOrders` | `PATCH` |

Limiter: 120 requests per 60 seconds per client IP (`merchant-api`). A limited call returns `429` before the key is checked.

Successful bodies are the JSON objects below. Errors are:

```json theme={null}
{ "success": false, "error": "Unauthorized", "code": "UNAUTHORIZED" }
```

`code` is `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `RATE_LIMITED`, or `INTERNAL_ERROR`. A `500` also includes `errorId`. Unexpected failures use the route fallback string as `error`.

## `GET /api/merchant/v1/stores/{storeId}/orders`

Scope: `order:read`

Query parameters this route reads:

| Param | Rule |
| - | - |
| `limit` | Integer 1–100. Default `50`. |
| `cursor` | Opaque string, 1–500 characters, from `nextCursor`. |
| `status` | Exact match: `pending`, `paid`, `processing`, `completed`, `cancelled`, `canceled`, `failed`, `expired`, `refunded`. |
| `from` | `createdAt` greater than or equal to this date. |
| `to` | `createdAt` less than or equal to this date. |

Omitted `limit`, `cursor`, `status`, `from`, and `to` are left unset (`limit` defaults to 50). An invalid value returns `400`. The message is the Zod issues joined with `; ` (`path: message` when the issue has a path). An unreadable cursor returns `400` with `error` `Invalid cursor`.

Rows are newest first (`createdAt` descending, then `id` descending). The handler loads `limit + 1` rows. `hasMore` is true when an extra row exists. `nextCursor` is set only then, and is the base64url of the last order in the returned `orders` array: `{createdAt ISO}|{id}`. The next page is rows older than that cursor (same `createdAt` and a smaller `id` still match).

```json theme={null}
{
  "orders": [
    {
      "id": "clxyzorder",
      "orderId": "ORD-2345-6789-ABCD-EFGH-JKMN",
      "storeId": "cmstore",
      "status": "pending",
      "productName": "Premium key",
      "price": 9.99,
      "paid": 0,
      "paymentMethod": "stripe",
      "customerEmail": "buyer@example.com",
      "country": "DE",
      "keyCount": 0,
      "createdAt": "2026-09-21T10:00:00.000Z",
      "completedAt": null,
      "updatedAt": "2026-09-21T10:00:00.000Z"
    }
  ],
  "limit": 50,
  "hasMore": false,
  "nextCursor": null
}
```

| Field | Type |
| - | - |
| `id` | Order row id |
| `orderId` | Public order id stored on the row |
| `storeId` | Store id |
| `status` | Stored status string |
| `productName` | String |
| `price` | Number |
| `paid` | Number or `null` |
| `paymentMethod` | String |
| `customerEmail` | String |
| `country` | String or `null` |
| `keyCount` | Number of product-key rows on the order. Key strings are not returned. |
| `createdAt` | ISO-8601 |
| `completedAt` | ISO-8601 or `null` |
| `updatedAt` | ISO-8601 |

List items do not include `note` or `customFieldResponses`.

```bash theme={null}
curl "https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/orders?status=pending&limit=20" \
  -H "Authorization: Bearer mk_live_..." \
  -H "Accept: application/json"
```

Fallback `error` on an unexpected failure: `Failed to list orders`.

## `GET /api/merchant/v1/stores/{storeId}/orders/{orderId}`

Scope: `order:read`

No query parameters.

`{orderId}` is looked up as the row `id` or the stored `orderId`. The path value is trimmed. That string is tried, then the uppercased form when it differs. A missing order, or an order whose `storeId` is not this shop, returns `404` with `error` `Order not found`.

The body is one order object. It has every list field, plus:

| Field | Type |
| - | - |
| `note` | Seller-written note text, or `null`. Tax-snapshot and embed system lines are removed before the response. |
| `customFieldResponses` | JSON stored on the order. `[]` when the column is empty. |

```json theme={null}
{
  "id": "clxyzorder",
  "orderId": "ORD-2345-6789-ABCD-EFGH-JKMN",
  "storeId": "cmstore",
  "status": "paid",
  "productName": "Premium key",
  "price": 9.99,
  "paid": 9.99,
  "paymentMethod": "stripe",
  "customerEmail": "buyer@example.com",
  "country": "DE",
  "keyCount": 1,
  "createdAt": "2026-09-21T10:00:00.000Z",
  "completedAt": null,
  "updatedAt": "2026-09-21T10:05:00.000Z",
  "note": "Follow up with the buyer",
  "customFieldResponses": []
}
```

These names are not in the object: `ipAddress`, `userAgent`, `browser`, `operatingSystem`, `asn`, `ipIntel`, `stripeCheckoutUrl`, `cryptomusPaymentAddress`, `cryptomusQrCode`, `nowpaymentsPaymentAddress`, `paypalFfEmail`, `venmoFfEmail`, `cashAppEmail`, `customPaymentProofUrls`, `gatewayCheckoutUrl`. Product-key strings are not included.

Fallback `error`: `Failed to load order`.

## `PATCH /api/merchant/v1/stores/{storeId}/orders/{orderId}`

Scope: `order:update`

No query parameters. `Content-Type: application/json`.

The body must be a JSON object. Invalid JSON returns `400` `Invalid JSON`. An array or other non-object returns `400` `JSON body must be an object`. A body `storeId` is dropped before validation.

Allowed keys (unknown keys fail because the schema is strict):

| Field | Rule |
| - | - |
| `note` | String, at most 5000 characters, or `null`. Optional. |
| `markDelivered` | Boolean. Optional. |

At least one of these must be present: `note` (including `null`), or `markDelivered` set to `true`. Otherwise `400` with `Provide note and/or markDelivered: true`. `markDelivered: false` by itself fails that check. `markDelivered: false` together with `note` updates the note and does not change delivery.

```json theme={null}
{ "note": "Follow up with the buyer" }
```

```json theme={null}
{ "note": null, "markDelivered": true }
```

`note` is saved when the field is present. Blank text is stored as no human note. System lines already on the order are kept in the database. The response `note` is the human text only.

`markDelivered: true` runs after the note write. It only proceeds when the stored status is `paid`, `processing`, or `completed`. Any other status, including `pending`, returns `400` `Order is not eligible for fulfillment updates`. If the order is already marked delivered, `400` `Order is already delivered`. If there is no product key, manual message, download link, download file, or delivery instructions, `400` `Add keys, a message, link, or files before marking delivered`. When the status is not already `completed`, the order is completed. This route does not accept fulfillment content fields (`manualMessage`, `downloadLink`, `downloadFiles`, `deliveryInstructions`, `status`).

The response is the same object as `GET` one order, loaded again by the row `id`. If delivery fails after a note write, the note is already saved.

```bash theme={null}
curl -X PATCH "https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/orders/ORD-2345-6789-ABCD-EFGH-JKMN" \
  -H "Authorization: Bearer mk_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d "{\"note\":\"Follow up with the buyer\"}"
```

Fallback `error`: `Failed to update order`.

## Errors

| Status | When |
| - | - |
| 400 | Invalid query or JSON, unknown body field, cursor, or fulfillment rule |
| 401 | Missing header, token is not `Bearer`, token does not start with `mk_live_`, unknown key, revoked key, expired key (`Unauthorized` or `API key expired`), or the account is deleted |
| 403 | Shop not allowed for the user or the key, scope missing, or dashboard ceiling missing (`Forbidden`) |
| 404 | Order not in this store (`Order not found`) |
| 429 | `Too many requests` |
| 500 | Unexpected failure (`errorId` included) |

## Related

* [API keys](https://docs.joinmarkt.com/developers/api-keys.md)
* [Merchant invoices](https://docs.joinmarkt.com/api-reference/merchant-invoices.md)
* [Merchant products](https://docs.joinmarkt.com/api-reference/merchant-products.md)
* [Merchant webhooks](https://docs.joinmarkt.com/api-reference/merchant-webhooks.md)
* [Orders overview](https://docs.joinmarkt.com/api-reference/orders.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.