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

# Receive webhooks

> Get Shipment updates pushed to your endpoint — setup, triggers, verification, payload formats, and field behaviors.

Outgoing webhooks push Shipment tracking updates to your systems the moment Perform.AI processes new events — no polling, no rate-budget cost. This guide covers the full lifecycle: configuring your endpoint, choosing triggers, verifying requests, and reading the payload.

## Set up your webhook endpoint

Webhooks are configured in the portal under **Integrations > Webhooks**:

<Steps>
  <Step title="Prepare a receiving endpoint">
    A publicly reachable **HTTPS** URL that accepts `POST` requests with a JSON body and responds `200` quickly. Perform.AI must be able to reach it directly — allowlist by hostname rather than IP if you filter inbound traffic.
  </Step>

  <Step title="Register the endpoint">
    In **Integrations > Webhooks**, add your endpoint URL.
  </Step>

  <Step title="Copy the Authentication Key">
    The page provides the **Authentication Key** used to sign every webhook with HMAC-SHA256 — store it in your secret manager; you'll verify signatures with it (below). HMAC is the default scheme; if you need a different one (OAuth 2.0, JWT, Basic, static key), raise it with your account team.
  </Step>

  <Step title="Activate triggers">
    Choose which notification triggers fire webhooks (next section). No active triggers means no webhooks.
  </Step>
</Steps>

<Note>
  The **payload format version** (5.0 / 5.2 / 5.3) is set per account, not per endpoint — confirm or change yours with your account team before integrating, and parse accordingly.
</Note>

## Triggers

Triggers define *when* a webhook fires — on every event, or only on specific milestones. They're managed with your notification configuration in the portal.

Each delivered payload's `trigger` array names every activated trigger that the update cycle satisfied. Values derive from the trigger's name (lowercased, punctuation replaced by underscores). Standard triggers include:

| Trigger value                            | Fires when                        |
| ---------------------------------------- | --------------------------------- |
| `all_events`                             | Any new event is processed.       |
| `parcel_updated_picked_up`               | The shipment is picked up.        |
| `parcel_updated_out_for_delivery`        | The shipment is out for delivery. |
| `parcel_updated_delivered`               | The shipment is delivered.        |
| `parcel_updated_failed_delivery_attempt` | A delivery attempt fails.         |
| `parcel_updated_customs`                 | A customs event occurs.           |
| `parcel_updated_delay`                   | A delay is detected.              |
| `parcel_updated_exception`               | An exception event occurs.        |

Accounts using Returns also have return-lifecycle triggers (for example `returns_all_events`), and custom triggers can be set up with your account team — their payload values follow the same naming derivation (for example a "Successfully delivered + all events" trigger emits `successfully_delivered_all_events`).

**One webhook per update cycle:** when several activated triggers fire from the same batch of new events, you receive a single webhook whose `trigger` array lists them all — deduplicate by shipment and `updated_date`, not by trigger.

## How delivery works

* **HTTPS POST** with a JSON body.
* **Respond with HTTP 200.** Anything else — including a timeout — counts as a failure: Perform.AI retries up to **3 times, 5 minutes apart**, then stops. Missed webhooks are not replayed afterwards; reconcile gaps with the [List Shipments API](/guides/list-shipments) filtered by `updated_date`.

<Tip>
  Acknowledge fast, process async: return the 200 immediately on receipt and queue the payload. Slow handlers cause timeouts, which burn your three retries.
</Tip>

## Verify the sender

Every request carries an `HTTP-X-Hmac-SHA256` header: the HMAC-SHA256 of the **raw request body**, keyed with your Authentication Key. Recompute and compare:

<CodeGroup>
  ```python Python theme={null}
  import hmac, hashlib

  def verify(raw_body: bytes, header_value: str, auth_key: str) -> bool:
      digest = hmac.new(auth_key.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(digest, header_value)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verify(rawBody, headerValue, authKey) {
    const digest = crypto.createHmac("sha256", authKey).update(rawBody).digest("hex");
    return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(headerValue));
  }
  ```
</CodeGroup>

Compute over the raw bytes — re-serializing the parsed JSON changes the digest. Reject non-matching requests.

## The payload

```json theme={null}
{
  "data": { "shipment_uuid": "…", "status": "active", "latest_event": { "…": "…" }, "new_events": [], "all_events": [] },
  "trigger": ["all_events", "parcel_updated_delivered"],
  "version": "…",
  "notification_type": "shipment_notification"
}
```

`data` carries the full Shipment — identifiers, carrier, status and phase, addresses, costs, documents — plus three event views:

| Field          | Contents                                                                                      |
| -------------- | --------------------------------------------------------------------------------------------- |
| `latest_event` | The single most recent event.                                                                 |
| `new_events`   | Only the events processed in **this** update cycle — usually what your handler should act on. |
| `all_events`   | The complete event history — useful for rebuilding state, expensive to reprocess every time.  |

The field-by-field payload is documented per format version in the reference:

<Columns cols={3}>
  <Card title="Format 5.0" href="/api-reference/webhooks/shipment-update-v5-0">
    The baseline payload.
  </Card>

  <Card title="Format 5.2" href="/api-reference/webhooks/shipment-update-v5-2">
    Adds returns, line items, collection point, rating, enriched event locations.
  </Card>

  <Card title="Format 5.3" href="/api-reference/webhooks/shipment-update-v5-3">
    Restructured EDD by source + Performance Aspiration results.
  </Card>
</Columns>

## Format versions

| Format  | Adds                                                                                                                                                                                                       | Enablement                                                          |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| **5.0** | The baseline Shipment payload.                                                                                                                                                                             | Default.                                                            |
| **5.2** | `shipment_type`, `returns`, `line_items`, `collection_point`, `rating`, `tracking_page_url`, `pod_url`, `parcel_recipient_information`, enriched `event.location` (full address + coordinates + timezone). | Automatic with the Returns module; otherwise via your account team. |
| **5.3** | `expected_delivery` split by source and `pa_results` (Performance Aspiration evaluations).                                                                                                                 | Via your account team.                                              |

Formats are additive on the same envelope — build your parser to ignore unknown fields and upgrades are non-breaking, with one exception: **5.3 restructures `expected_delivery`** (below).

## Field behaviors worth knowing

* **Event timestamps vary in precision.** Each event's `time` reflects what the source provided — full timestamp with offset, timestamp without timezone, or date only — with `timezone` set only when known. See [timestamps](/essentials/timestamps).
* **`expected_delivery` (formats 5.0/5.2)** is a simple `from`/`to` window and is **omitted entirely** when neither merchant nor carrier provided an EDD — don't assume the key exists.
* **`expected_delivery` (format 5.3)** becomes four source objects — `merchant`, `carrier`, `predict_engine`, and `display` — and unlike earlier formats the object is **always present**, with a source set to `null` when it has no estimate (the value shown in the product, following your account's configurable EDD display priority). Each carries `start`, `end`, a `type`, and timezones. **Precision differs by source:** `merchant` and `carrier` estimates are always **date-only ranges** (`type: date_range` — no time component, and merchant timezones are always `Etc/UTC`), while `predict_engine` provides full **datetime ranges** (`datetime_range`). Check `type` before parsing rather than assuming a time exists.
* **`pa_results` (format 5.3)** is an empty array unless the Performance Aspiration module is enabled on your account. When populated, `expected_result_date` can be `null` for hour-based transit-time rules.
* **Units are normalized:** `dimensions` always in centimeters, `weight` always in grams, money fields as `{amount, currency}` objects.
* **`notification_type`** is currently always `shipment_notification` — treat it as a discriminator for future notification types, not information.
* **Address objects** mirror the Shipment's stored addresses; their `email`/`phone` are contact data, not notification subscriptions.

## Test your integration

Webhooks fire on real update cycles, so the simplest end-to-end test needs no carrier: create a test Shipment with a tracking number and carrier, then push events yourself with the [Create Events API](/guides/create-events) — each call is an update cycle that delivers a webhook to your endpoint. Verify your HMAC check against the received header before going live, and test your non-200 handling by temporarily returning an error to observe the retry behavior.
