> ## 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 webhooks

> Create and manage outgoing shop webhooks with a Bearer API key.

All routes require:

```txt theme={null}
Authorization: Bearer mk_live_<your_key>
```

The key must include the scope below **and** access to that store. The dashboard permission ceiling is `manageSettings`.

These endpoints manage outgoing Join Markt events. They are separate from payment-provider IPN.

```http theme={null}
GET    /api/merchant/v1/stores/{storeId}/webhooks
POST   /api/merchant/v1/stores/{storeId}/webhooks
GET    /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
PATCH  /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
DELETE /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
POST   /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/rotate
POST   /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/test
GET    /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/deliveries
```

`{webhookId}` is the webhook record id, or the public id `WHK-` plus five groups of four characters from `23456789ABCDEFGHJKMNPQRSTUVWXYZ`. Letter case does not matter for the public id. A public id that is not in this store is handled as a missing webhook (`404`).

A store can have 20 webhooks. The 21st create returns `400` with `error` `Maximum of 20 webhooks per shop`.

## Webhook object

List, get, update, create, and rotate return this object. Create and rotate add `secret`. List and get do not.

**id** `string`

Webhook record id.

**publicId** `string`

Public id shaped `WHK-XXXX-XXXX-XXXX-XXXX-XXXX`.

**storeId** `string`

Store record id.

**name** `string`

Label, 1–120 characters.

**url** `string`

Endpoint URL.

**secretPrefix** `string`

Last four characters of the signing secret.

**events** `string[]`

Subscribed events. Each value is `order.created`, `order.completed`, `order.refunded`, or `product.updated`.

**enabled** `boolean`

Event delivery selects rows with `enabled` true, `disabledAt` null, and the event name in `events`. `POST .../test` checks `enabled` only. A 2xx delivery sets `failureCount` to `0`, sets `lastDeliveredAt`, and clears `disabledAt`. After 10 failed deliveries the row is saved with `enabled` false and `disabledAt` set. `JOIN_MARKT_WEBHOOK_AUTO_DISABLE` overrides 10 when it is a positive number.

**failureCount** `number`

Consecutive delivery failures. A 2xx delivery sets this to `0`.

**disabledAt** `string | null`

ISO time when the endpoint was disabled, or `null`.

**lastDeliveredAt** `string | null`

ISO time of the last 2xx delivery, or `null`.

**createdAt** `string`

ISO create time.

**updatedAt** `string`

ISO update time.

**secret** `string`

Full signing secret. Present on create (`201`) and rotate only. It starts with `whsec_`.

```json theme={null}
{
  "id": "cm123",
  "publicId": "WHK-2345-6789-ABCD-EFGH-JKMN",
  "storeId": "STORE_ID",
  "name": "Orders to backend",
  "url": "https://example.com/webhooks/markt",
  "secretPrefix": "a1b2",
  "events": ["order.completed"],
  "enabled": true,
  "failureCount": 0,
  "disabledAt": null,
  "lastDeliveredAt": "2026-09-21T12:00:00.000Z",
  "createdAt": "2026-09-21T11:00:00.000Z",
  "updatedAt": "2026-09-21T12:00:00.000Z"
}
```

## URL rules

Create and update run the same URL check when `url` is set.

* The value must be an absolute URL, max 2048 characters, with no username or password.
* The protocol is `https`.
* Loopback, unspecified, link-local, and cloud-metadata hosts are rejected.
* Private addresses and internal hostnames are rejected.

`400` messages: `Invalid webhook URL`, `Webhook URL must not include credentials`, `Webhook URL must use HTTPS`, `Webhook URL host is not allowed`.

## List webhooks

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/webhooks
```

Scope: `webhook:read`. No query parameters.

**storeId** `string` *required*

Store record id.

```json theme={null}
{
  "webhooks": []
}
```

`webhooks` is an array of [webhook objects](#webhook-object) without `secret`.

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks \
  -H "Authorization: Bearer mk_live_<your_key>"
```

## Create webhook

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/webhooks
```

Scope: `webhook:write`. Status `201`. JSON object. Fields other than the ones below are ignored. Invalid JSON returns `400` with `error` `Invalid JSON`.

**storeId** `string` *required*

Store record id.

**name** `string` *required*

Trimmed label, 1–120 characters.

**url** `string` *required*

Endpoint URL. See [URL rules](#url-rules).

**events** `string[]` *required*

At least one of `order.created`, `order.completed`, `order.refunded`, `product.updated`.

**enabled** `boolean`

Defaults to `true`. `false` creates a disabled endpoint. Create does not set `disabledAt`.

```json theme={null}
{
  "name": "Orders to backend",
  "url": "https://example.com/webhooks/markt",
  "events": ["order.created", "order.completed", "order.refunded", "product.updated"]
}
```

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks \
  -H "Authorization: Bearer mk_live_<your_key>" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"Orders to backend\",\"url\":\"https://example.com/webhooks/markt\",\"events\":[\"order.completed\"]}"
```

The `201` body is the webhook object plus `secret`.

## Get webhook

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
```

Scope: `webhook:read`.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN \
  -H "Authorization: Bearer mk_live_<your_key>"
```

Returns the [webhook object](#webhook-object) without `secret`. `404` with `error` `Webhook not found` when the id is not in this store.

## Update webhook

```http theme={null}
PATCH /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
```

Scope: `webhook:write`. JSON object. At least one of `name`, `url`, `events`, or `enabled` is required. Other properties are ignored. Invalid JSON returns `400` with `error` `Invalid JSON`. An empty update returns `400` with `error` `Provide at least one field to update`.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

**name** `string`

Trimmed label, 1–120 characters.

**url** `string`

Endpoint URL. See [URL rules](#url-rules).

**events** `string[]`

At least one event id when this field is sent. Same enum as create.

**enabled** `boolean`

`false` sets `disabledAt` to the current time. `true` clears `disabledAt` and sets `failureCount` to `0`.

```json theme={null}
{
  "name": "Renamed",
  "url": "https://example.com/webhooks/markt",
  "events": ["order.completed"],
  "enabled": false
}
```

```bash theme={null}
curl -X PATCH https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN \
  -H "Authorization: Bearer mk_live_<your_key>" \
  -H "Content-Type: application/json" \
  -d "{\"enabled\":false}"
```

Returns the [webhook object](#webhook-object) without `secret`.

## Delete webhook

```http theme={null}
DELETE /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}
```

Scope: `webhook:delete`. No body is read.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

```bash theme={null}
curl -X DELETE https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN \
  -H "Authorization: Bearer mk_live_<your_key>"
```

```json theme={null}
{ "success": true }
```

## Rotate secret

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/rotate
```

Scope: `webhook:write`. No body is read. Returns the [webhook object](#webhook-object) plus a new `secret`. `secretPrefix` becomes the last four characters of that secret. Later deliveries are signed with the new secret.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN/rotate \
  -H "Authorization: Bearer mk_live_<your_key>"
```

## Send test

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/test
```

Scope: `webhook:write`. No body is read. Sends one `webhook.test` delivery and waits for that attempt (the outbound request aborts after 10 seconds). A disabled endpoint returns `400` with `error` `Webhook is disabled`. A missing stored secret returns `400` with `error` `Webhook secret is missing; rotate the endpoint`.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

**ok** `boolean`

`true` when the endpoint returned a status from 200 through 299.

**deliveryId** `string`

Delivery row id.

**responseCode** `number | null`

HTTP status from the endpoint, or `null` when there was no HTTP response.

**disabled** `boolean`

Present when `ok` is `false`. `true` when this failure reached the auto-disable threshold.

```json theme={null}
{
  "ok": true,
  "deliveryId": "cm456",
  "responseCode": 200
}
```

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN/test \
  -H "Authorization: Bearer mk_live_<your_key>"
```

The posted JSON is the [event envelope](#event-envelope) with `type` `webhook.test` and `data` `{ "ok": true }`.

## List deliveries

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/webhooks/{webhookId}/deliveries
```

Scope: `webhook:read`. No query parameters. Returns the newest 50 delivery rows.

**storeId** `string` *required*

Store record id.

**webhookId** `string` *required*

Record id or `WHK-` public id.

**deliveries** `object[]`

Newest first.

**delivery**

**id** `string`

Delivery row id.

**webhookId** `string`

Webhook record id.

**eventId** `string`

Envelope `id` (`evt_…`).

**event** `string`

Event name, including `webhook.test`.

**attempt** `number`

Attempt number, starting at 1. A later retry is a separate row with status `queued` until it runs. Retries stop once the next attempt would pass 6, or the endpoint is disabled.

**status** `string`

`pending`, `success`, `failed`, `queued`, or `skipped`.

**responseCode** `number | null`

HTTP status, or `null`.

**responseBodyPreview** `string | null`

Response body with whitespace collapsed, at most 300 characters.

**errorMessage** `string | null`

Delivery error, whitespace collapsed, at most 300 characters.

**durationMs** `number | null`

Attempt duration in milliseconds.

**createdAt** `string`

ISO create time.

```json theme={null}
{
  "deliveries": [
    {
      "id": "cm456",
      "webhookId": "cm123",
      "eventId": "evt_abc",
      "event": "order.completed",
      "attempt": 1,
      "status": "success",
      "responseCode": 200,
      "responseBodyPreview": "ok",
      "errorMessage": null,
      "durationMs": 84,
      "createdAt": "2026-09-21T12:00:00.000Z"
    }
  ]
}
```

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/webhooks/WHK-2345-6789-ABCD-EFGH-JKMN/deliveries \
  -H "Authorization: Bearer mk_live_<your_key>"
```

## Event envelope

Deliveries `POST` JSON with `Content-Type: application/json` and `User-Agent: JoinMarkt-Webhooks/1.0`.

| Header | Value |
| - | - |
| `X-JoinMarkt-Signature` | `t=<unix seconds>,v1=<hex>` |
| `X-JoinMarkt-Event` | Event name |
| `X-JoinMarkt-Delivery` | Delivery row id |
| `X-JoinMarkt-Webhook-Id` | Webhook record id (`id`, not `publicId`) |

`v1` is HMAC-SHA256 of the signing secret over the string `<unix seconds>.<raw body>`, hex-encoded. The raw body is the exact JSON bytes. Verification rejects a timestamp more than 5 minutes from the current time.

```json theme={null}
{
  "id": "evt_abc",
  "type": "order.completed",
  "created": 1761364800,
  "storeId": "STORE_ID",
  "data": {}
}
```

`id` starts with `evt_`. `created` is unix seconds. `type` is the event name. `data` is the event payload. For `webhook.test`, `data` is `{ "ok": true }`.

## Errors

Failed responses use `{ "success": false, "error": "...", "code": "..." }`. A `500` also includes `errorId`. Zod field errors are the issue messages joined with `; `.

| Status | Code | When |
| - | - | - |
| 400 | `BAD_REQUEST` | Invalid JSON, validation, URL rejected, 20-webhook cap, disabled test, or missing signing secret |
| 401 | `UNAUTHORIZED` | Missing, invalid, or revoked token. `error` is `Unauthorized`. Expired keys use `API key expired`. |
| 403 | `FORBIDDEN` | Key cannot access this store, or the scope / `manageSettings` is missing. `error` is `Forbidden`. |
| 404 | `NOT_FOUND` | `Webhook not found` |
| 429 | `RATE_LIMITED` | More than 120 requests per minute. `error` is `Too many requests`. |
| 500 | `INTERNAL_ERROR` | Unexpected failure. `error` is `Failed to list webhooks`, `Failed to create webhook`, `Failed to load webhook`, `Failed to update webhook`, `Failed to delete webhook`, `Failed to rotate webhook secret`, `Failed to send test webhook`, or `Failed to list webhook deliveries`. |

## Related

* [Outgoing webhooks](https://docs.joinmarkt.com/developers/outgoing-webhooks.md)
* [API keys](https://docs.joinmarkt.com/developers/api-keys.md)
* [Merchant orders](https://docs.joinmarkt.com/api-reference/merchant-orders.md)
* [Payment-provider IPN](https://docs.joinmarkt.com/developers/webhooks.md)


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