# Webhooks

Webhooks notify your backend when an order completes or needs more photos. They
reduce polling, but you should still periodically reconcile active orders with
`GET /orders/{id}`.

Managing webhook endpoints requires the `webhooks:manage` scope. An endpoint
receives events only for orders created with the same API key.

## Events

| Event | Sent when |
| --- | --- |
| `order-outcome` | An order completes and its outcome is available. |
| `update-photos` | An order needs replacement or additional photos. |

Register the endpoint before creating orders so you do not miss events.

## Register an endpoint

`POST /v1/integrations/webhooks` creates an endpoint. HMAC signatures are used
when `auth` is omitted:

```bash
curl --request POST "$LEGITGRAILS_BASE_URL/webhooks" \
  --header "Authorization: Bearer $LEGITGRAILS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Order events",
    "url": "https://partner.example.com/webhooks/legitgrails",
    "events": ["order-outcome", "update-photos"]
  }'
```

The response includes a `signing_secret` only once. Store it securely:

```json
{
  "id": "<webhook-endpoint-id>",
  "api_key_id": "<api-key-id>",
  "name": "Order events",
  "url": "https://partner.example.com/webhooks/legitgrails",
  "events": ["order-outcome", "update-photos"],
  "created_at": "2026-08-17T10:20:30.000Z",
  "signing_secret": "<signing-secret>"
}
```

Webhook URLs must be publicly reachable over HTTPS. Each API key can have up
to 10 endpoints, and a URL can be registered only once for that key.

## Authenticate deliveries

Every delivery includes:

| Header | Value |
| --- | --- |
| `X-LG-Event` | `order-outcome` or `update-photos`. |
| `X-LG-Delivery-Id` | Stable delivery ID used for deduplication. |

Choose one of the following authentication methods.

### HMAC signatures

HMAC deliveries also include `X-LG-Timestamp` and `X-LG-Signature`. Calculate
the expected signature from the exact raw request body:

```text
HMAC_SHA256(signing_secret, X-LG-Timestamp + "." + raw_body)
```

Hex-encode the result, prefix it with `v1=`, and compare it with
`X-LG-Signature` using a constant-time comparison. Verify the signature before
parsing the JSON body, and reject old timestamps.

### OAuth2 client credentials

If your endpoint expects a bearer token, include `auth` when registering it:

```json
{
  "name": "Order events",
  "url": "https://partner.example.com/webhooks/legitgrails",
  "events": ["order-outcome", "update-photos"],
  "auth": {
    "type": "oauth2",
    "token_url": "https://partner.example.com/oauth/token",
    "client_id": "legitgrails-webhooks",
    "client_secret": "<client-secret>",
    "scope": "webhooks.write"
  }
}
```

The token endpoint must support the client-credentials grant with HTTP Basic
client authentication and return a non-empty `access_token`. `scope` is
optional. Deliveries include `Authorization: Bearer <access-token>` instead of
the HMAC headers. The client secret is never returned by the API.

## Process an event

The payload includes the delivery ID and current order data:

```json
{
  "webhook_event": "order-outcome",
  "delivery_id": "<delivery-id>",
  "id": "<order-id>",
  "external_id": "listing-10442-line-1",
  "order_number": "<order-reference>",
  "created_at": "2026-08-17T10:30:00.000Z",
  "status": "completed",
  "outcome": "authentic",
  "updated_at": "2026-08-17T10:29:59.000Z"
}
```

- Use `webhook_event` to choose the handler.
- Use `delivery_id` as the idempotency key. The same ID is used for retries.
- Use `id` for subsequent LegitGrails API requests and `external_id` to match
  your own record.
- For `update-photos`, read `photos_to_resubmit` and follow the
  [photo-update flow](/photos#respond-to-an-update-photos-request).

## Acknowledge deliveries

Return any `2xx` response within 10 seconds. Other responses and connection
failures are retried up to three times, for four total attempts. Retry delays
are approximately 1, 3, and 7 minutes.

Process each `delivery_id` only once, but return `2xx` for duplicates. The order
`error` state does not send an `order-outcome` event, so retain scheduled
reconciliation for active orders.

## List and delete endpoints

- `GET /v1/integrations/webhooks` lists endpoints for the current API key. It
  never returns `signing_secret` or `client_secret`.
- `DELETE /v1/integrations/webhooks/{id}` deletes an endpoint and returns
  `204 No Content`.
- To change a URL, event selection, or authentication method, delete the
  endpoint and create a new one.

See the [API Reference](/api/webhooks) for complete request and response
schemas.
