# Outbound Webhooks (https://eulabel.eu/docs/webhooks/outbound-webhooks)

> Fetch the complete documentation index at: https://eulabel.eu/docs/llms.txt
> Use this file to discover all available pages before exploring further.
> Full content: https://eulabel.eu/docs/llms-full.txt
> Append .md to any page URL for markdown, or send Accept: text/markdown.



EUlabel sends outbound webhooks to notify your systems when passport lifecycle events occur.

At a glance [#at-a-glance]

* Verify signatures for every event.
* Respond within 30 seconds with `2xx` to avoid retries.
* Make handlers idempotent (events may be delivered more than once).

Subscribable events [#subscribable-events]

| Event                    | Trigger                                                                             |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `passport.draft_created` | A new passport is created in `draft` status (first call with partial data)          |
| `passport.enriched`      | An existing `draft` passport receives additional data fields via an enrichment call |
| `passport.published`     | A passport reaches full compliance and transitions from `draft` to `published`      |
| `passport.updated`       | A `published` passport is modified (new data version)                               |
| `passport.deleted`       | A passport is removed                                                               |
| `scan.threshold`         | Scan count crosses a configured threshold                                           |

The progressive creation flow produces a sequence of events as data arrives from different source systems. A typical three-call integration emits: `passport.draft_created` → `passport.enriched` → `passport.published`.

Example payload (draft created) [#example-payload-draft-created]
```json
{
  "event": "passport.draft_created",
  "id": "ps_w9x2kf",
  "gtin": "05601234567890",
  "timestamp": "2026-03-25T10:00:00Z",
  "compliance": {
    "status": "incomplete",
    "regulation": "EU Wine e-label Regulation (Reg. 2021/2117)",
    "completeness": 20,
    "missingFields": [
      { "field": "data.nutrition", "message": "Nutrition information per 100 mL is required for wine e-labels." },
      { "field": "data.allergens", "message": "Allergen declarations are mandatory under Reg. 1169/2011." },
      { "field": "data.origin", "message": "Country of origin is required for wine e-labels." },
      { "field": "data.producers", "message": "At least one producer or bottler must be identified." }
    ],
    "documentationUrl": "https://eulabel.eu/docs/api-reference/passports#compliance"
  },
  "passportUrl": "https://eulabel.eu/01/05601234567890"
}
```
Example payload (compliance achieved) [#example-payload-compliance-achieved]
```json
{
  "event": "passport.published",
  "id": "ps_w9x2kf",
  "gtin": "05601234567890",
  "timestamp": "2026-03-25T10:15:00Z",
  "compliance": {
    "status": "compliant",
    "regulation": "EU Wine e-label Regulation (Reg. 2021/2117)",
    "completeness": 100,
    "missingFields": [],
    "documentationUrl": "https://eulabel.eu/docs/api-reference/passports#compliance"
  },
  "version": 2,
  "passportUrl": "https://eulabel.eu/01/05601234567890"
}
```
Signature verification [#signature-verification]

Outbound webhooks include a signature header for verification:
```text
X-EUlabel-Signature: sha256=<HMAC-SHA256 of request body>
```
Recommended handler layout [#recommended-handler-layout]

- src/

- webhooks/
  - eulabel.ts
- app/

- api/

- webhooks/

- eulabel/
  - route.ts
Verification example [#verification-example]
```typescript
import { EUlabelWebhooks } from "@eulabel/webhooks";

const webhooks = new EUlabelWebhooks({ secret: "whsec_..." });

app.post("/webhooks/eulabel", async (req, res) => {
  const event = webhooks.verify(req.body, req.headers["x-eulabel-signature"]);

  switch (event.type) {
    case "passport.draft_created":
      await handleDraftCreated(event);
      break;
    case "passport.enriched":
      await handleEnriched(event);
      break;
    case "passport.published":
      await handlePublished(event);
      break;
    case "passport.updated":
      await handlePassportUpdate(event);
      break;
    case "scan.threshold":
      await notifyTeam(event);
      break;
  }

  res.status(200).send("OK");
});
```
Retry policy [#retry-policy]

> **Note**
> Your endpoint must return a `2xx` status within 30 seconds. Responses outside this window are treated as failures and trigger retries.

Failed deliveries are retried with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 30 seconds |
| 3       | 2 minutes  |
| 4       | 15 minutes |
| 5       | 1 hour     |
| 6       | 4 hours    |

After 6 failed attempts, the webhook is marked as failed and the organization admin is notified via email.

Managing subscriptions [#managing-subscriptions]

Configure webhook subscriptions in the [EUlabel Dashboard](https://app.eulabel.eu) under **Settings > Webhooks**. Provide:

* **Endpoint URL**: The URL where EUlabel sends events
* **Events**: Which event types to subscribe to
* **Secret**: A shared secret for signature verification

