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

# GDPR Webhooks

> Mandatory GDPR webhook topics, payload schemas, and compliance deadlines

# GDPR Webhooks

Every app installed on a LaunchMyStore merchant **must** respond to three
mandatory GDPR webhook topics. These webhooks are dispatched by the
platform when a merchant initiates a GDPR request through their admin
(or when the merchant closes their store), and your app is legally
required to acknowledge receipt and process the data within fixed
deadlines.

| Topic                    | Article                         | Trigger                                          | Deadline (acknowledge / complete) |
| ------------------------ | ------------------------------- | ------------------------------------------------ | --------------------------------- |
| `customers/data_request` | GDPR Art. 15 (right of access)  | Merchant initiates data export for a customer.   | **30 days / 90 days**             |
| `customers/redact`       | GDPR Art. 17 (right to erasure) | Merchant initiates data deletion for a customer. | **30 days / 90 days**             |
| `shop/redact`            | GDPR Art. 17 (store closure)    | Merchant closes their store.                     | **30 days / 90 days**             |

<Warning>
  Failure to acknowledge within 30 days marks the request as `failed` for
  your app and notifies the merchant. Failure to complete within 90 days
  also marks the request as `failed` and is grounds for app removal from
  the marketplace.
</Warning>

## Webhook Delivery

GDPR webhooks are POSTed to **two** sets of endpoints:

1. Any callback URLs registered via the standard webhook subscription
   API (enabled webhook subscriptions whose `topic` matches one of the
   three GDPR topics).
2. The mandatory `gdprUrls` configured on your `App` record:
   * `gdprUrls.customerDataRequest` for `customers/data_request`
   * `gdprUrls.customerRedact` for `customers/redact`
   * `gdprUrls.shopRedact` for `shop/redact`

Webhooks are HMAC-signed with the app's `clientSecret` and have a 10s
timeout. Failures are logged but **do not** automatically retry — your
app must acknowledge receipt via `POST /apps/gdpr/acknowledge/:requestId`
within the deadline, regardless of HTTP delivery success.

## Request Headers

| Header                  | Value                                                                             |
| ----------------------- | --------------------------------------------------------------------------------- |
| `Content-Type`          | `application/json`                                                                |
| `X-LMS-Topic`           | One of `customers/data_request`, `customers/redact`, `shop/redact`                |
| `X-LMS-Gdpr-Request-Id` | The `requestId` (UUID) needed for the acknowledge/complete callbacks              |
| `X-LMS-Hmac-SHA256`     | `base64(HMAC-SHA256(clientSecret, body))` — verify this matches before processing |

## Payload: `customers/data_request`

```json theme={null}
{
  "shop_id": "f73049dc-b4d4-4f85-99c2-681a5e351a8a",
  "shop_domain": "acme-supply.launchmystore.io",
  "customer": {
    "id": "ac1f2d3e-4b5c-6789-0123-456789abcdef",
    "email": "jane@example.com",
    "phone": "+15551234567"
  },
  "orders_requested": true,
  "data_request": {
    "id": "9f8e7d6c-5b4a-3210-1234-56789abcdef0"
  }
}
```

| Field              | Type           | Notes                                                                           |
| ------------------ | -------------- | ------------------------------------------------------------------------------- |
| `shop_id`          | string (UUID)  | Merchant's `storeId`.                                                           |
| `shop_domain`      | string \| null | The merchant's primary `storeURL`.                                              |
| `customer.id`      | string \| null | Customer record ID. Null for unidentified customer lookups by email/phone only. |
| `customer.email`   | string \| null | Customer email (always present unless redacted).                                |
| `customer.phone`   | string \| null | Customer phone if provided.                                                     |
| `orders_requested` | boolean        | `true` means the merchant wants order history included in the export.           |
| `data_request.id`  | string (UUID)  | The `requestId`. Use this for acknowledge/complete callbacks.                   |

## Payload: `customers/redact`

```json theme={null}
{
  "shop_id": "f73049dc-b4d4-4f85-99c2-681a5e351a8a",
  "shop_domain": "acme-supply.launchmystore.io",
  "customer": {
    "id": "ac1f2d3e-4b5c-6789-0123-456789abcdef",
    "email": "jane@example.com"
  },
  "orders_to_redact": [
    "ord_1a2b3c4d5e6f7g8h",
    "ord_9i8j7k6l5m4n3o2p"
  ]
}
```

| Field              | Type           | Notes                                                                                                                                             |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shop_id`          | string (UUID)  | Merchant's `storeId`.                                                                                                                             |
| `shop_domain`      | string \| null | Primary storefront domain.                                                                                                                        |
| `customer.id`      | string         | Customer record ID.                                                                                                                               |
| `customer.email`   | string         | Customer email at the time of redaction.                                                                                                          |
| `orders_to_redact` | string\[]      | List of all order IDs belonging to this customer. Your app should redact PII from any records tied to these orders, not just the customer record. |

## Payload: `shop/redact`

```json theme={null}
{
  "shop_id": "f73049dc-b4d4-4f85-99c2-681a5e351a8a",
  "shop_domain": "acme-supply.launchmystore.io"
}
```

Fired when a merchant closes their store. Your app must delete **all**
data associated with `shop_id` within 90 days.

## Required Response

Webhook handlers should return HTTP `200`-`299` quickly (the platform
timeout is 10s). The HTTP response itself does **not** acknowledge the
request — you must call the merchant-facing acknowledge endpoint
explicitly:

```bash theme={null}
curl -X POST "https://api.launchmystore.io/apps/gdpr/acknowledge/{requestId}" \
  -H "Authorization: Bearer YOUR_APP_ACCESS_TOKEN"
```

Then, once you have processed the request, mark it complete:

```bash theme={null}
curl -X POST "https://api.launchmystore.io/apps/gdpr/complete/{requestId}" \
  -H "Authorization: Bearer YOUR_APP_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dataExportUrl": "https://your-app.com/exports/abc.zip"
  }'
```

`dataExportUrl` is only relevant for `customers/data_request` — it is
displayed in the merchant admin so they can hand the export to the
customer.

## Merchant Endpoints

These are used by the LaunchMyStore admin (not your app), but are
documented here for completeness.

### `POST /apps/gdpr/data-request`

Merchant-initiated. Creates a new `customers/data_request` and dispatches
webhooks to every installed app.

```json theme={null}
{
  "customerId": "ac1f2d3e-...",
  "customerEmail": "jane@example.com",
  "customerPhone": "+15551234567"
}
```

Response data:

```json theme={null}
{
  "requestId": "9f8e7d6c-...",
  "status": "pending",
  "acknowledgeDeadline": "2026-06-15T00:00:00.000Z",
  "completionDeadline": "2026-08-14T00:00:00.000Z",
  "appsNotified": 7
}
```

### `POST /apps/gdpr/customer-redact`

```json theme={null}
{
  "customerId": "ac1f2d3e-...",
  "customerEmail": "jane@example.com"
}
```

Response includes `ordersToRedact` count.

### `POST /apps/gdpr/shop-redact`

No body. Returns the same shape as the others.

### `GET /apps/gdpr/requests`

Lists GDPR requests for the merchant's store.

| Query         | Type    | Notes                                                          |
| ------------- | ------- | -------------------------------------------------------------- |
| `page`        | integer | Default `1`                                                    |
| `limit`       | integer | Default `20`                                                   |
| `status`      | enum    | `pending`, `dispatched`, `acknowledged`, `completed`, `failed` |
| `requestType` | enum    | `data_request`, `customer_redact`, `shop_redact`               |

### `GET /apps/gdpr/requests/:requestId`

Returns the full request including per-app acknowledgment state:

```json theme={null}
{
  "requestId": "9f8e7d6c-...",
  "requestType": "data_request",
  "status": "acknowledged",
  "customerId": "ac1f2d3e-...",
  "customerEmail": "jane@example.com",
  "requestedAt": "2026-05-16T12:34:56.000Z",
  "acknowledgeDeadline": "2026-06-15T12:34:56.000Z",
  "completionDeadline": "2026-08-14T12:34:56.000Z",
  "completedAt": null,
  "appAcknowledgments": [
    {
      "appId": "lms_app_foundry_reviews",
      "appName": "Foundry Reviews",
      "status": "completed",
      "acknowledgedAt": "2026-05-16T12:35:11.000Z",
      "completedAt": "2026-05-18T09:00:00.000Z"
    },
    {
      "appId": "lms_app_loyalty_pro",
      "appName": "Loyalty Pro",
      "status": "pending",
      "errorMessage": null
    }
  ],
  "dataExportUrl": "https://foundry.example/exports/abc.zip",
  "metadata": { "ordersRequested": true }
}
```

## Deadline Enforcement

A cron runs daily at midnight (`@Cron(EVERY_DAY_AT_MIDNIGHT)`) that:

1. Finds requests past `acknowledgeDeadline` still in `pending` /
   `dispatched`, marks the lagging apps as `failed`, sets the request
   status to `failed`, and re-dispatches the webhook as a final notice.
2. Finds requests past `completionDeadline` still in `dispatched` /
   `acknowledged`, marks lagging apps as `failed`, request → `failed`.
3. Finds requests with `completionDeadline` within the next 7 days and
   re-dispatches the webhook as a reminder.

## HMAC Verification

```javascript theme={null}
import crypto from 'crypto';

function verifyGdprWebhook(req, clientSecret) {
  const hmacHeader = req.get('X-LMS-Hmac-SHA256');
  const body = req.rawBody; // raw bytes, NOT JSON.stringify(req.body)
  const expected = crypto
    .createHmac('sha256', clientSecret)
    .update(body)
    .digest('base64');

  // constant-time compare
  return (
    hmacHeader.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(expected))
  );
}
```

## Status Lifecycle

```
pending ─► dispatched ─► acknowledged ─► completed
                  │             │
                  └──► failed ◄─┘   (deadline missed)
```

Per-app `appAcknowledgments[].status` is one of `pending`,
`acknowledged`, `completed`, `failed`.
