---
id: webhooks-payload
slug: /apis/webhooks/payload
title: 'Webhook Payload'
description: 'The JSON Agility POSTs to your endpoint, the headers it sends, and how to verify the HMAC signature.'
sidebar_label: 'Webhook Payload'
sidebar_position: 3
tags: ['api', 'webhooks', 'events', 'security']
---

This is what arrives at the `url` on your subscription.

## Request

Agility sends an HTTP `POST` with a JSON body.

### Headers Sent to Your Endpoint

| Header                    | Value                                                        | Sent when                                              |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
| `Content-Type`            | `application/json`                                            | Always.                                                |
| `Authorization`           | Verbatim value of the subscription's `authorizationHeader`    | Only if the subscription has a non-empty one.          |
| `X-SIGNATURE-HMAC-SHA512` | Base64 HMAC-SHA512 of the raw body, keyed with your `secret`  | Only if the subscription has a non-empty `secret`.     |
| `X-SIGNATURE`             | Legacy signature, computed differently                        | Only where the instance has legacy signatures enabled. |

No fixed `User-Agent` is set, so do not filter on one.

## Body

```json
{
  "events": [
    {
      "webhookId": "YYY",
      "sequenceId": 1,
      "eventType": "AssetChanged",
      "timestamp": "UTC timestamp",
      "instigator": {
        "_oid": "Member:20",
        "href": "https://V1Host/V1Instance/assetdetail.v1?oid=Member:20",
        "name": "Administrator",
        "nickName": "Admin",
        "email": "admin@admin.com",
        "role": "Role:2",
        "avatar": "https://V1Host/V1Instance/Image.mvc/Show?imageOid=Image:192923"
      },
      "targetAsset": {
        "_oid": "Story:123:456",
        "assetType": "Story",
        "href": "https://V1Host/V1Instance/assetdetail.v1?oid=Story:123"
      },
      "changes": [
        {
          "name": "Name",
          "old": "Original Name",
          "new": "New Name",
          "act": "Set"
        },
        {
          "name": "Owners",
          "new": "Member:20",
          "act": "Added"
        }
      ],
      "snapshot": [
        {
          "_oid": "Story:123",
          "Owners.Name": ["Administrator"],
          "Owners.ID": [{ "_oid": "Member:20" }],
          "SubsAndMe.Owners.Name": ["Administrator"]
        }
      ]
    }
  ]
}
```

The `href` values are [asset permalinks](/apis/asset-permalinks) —
`assetdetail.v1` URLs that open the asset in the product UI, built for the
humans on the receiving end of a delivery rather than for programs.

:::note `events` is an array
A single delivery can carry more than one event. Iterate; do not read `events[0]` and stop.
:::

### Event Fields

| Field         | Type    | Description                                                                                 |
| ------------- | ------- | ------------------------------------------------------------------------------------------- |
| `webhookId`   | string  | The identifier **you** chose when creating the subscription. Use it to route the delivery.  |
| `sequenceId`  | integer | Monotonic sequence number. Use it to restore ordering.                                       |
| `eventType`   | string  | `AssetCreated` or `AssetChanged` — which event type rule fired.                              |
| `timestamp`   | string  | UTC timestamp of the change.                                                                 |
| `instigator`  | object  | The member who made the change.                                                              |
| `targetAsset` | object  | The asset that was created or changed.                                                       |
| `changes`     | array   | What actually changed. Empty or absent for `AssetCreated`.                                   |
| `snapshot`    | array   | Projection of the asset, shaped by the event type's `select`.                                |

### `instigator`

| Field      | Description                                                       |
| ---------- | ----------------------------------------------------------------- |
| `_oid`     | Oid token of the member, e.g. `Member:20`.                        |
| `href`     | Deep link to the member's detail page in the Agility UI.          |
| `name`     | Full name.                                                        |
| `nickName` | Short name.                                                       |
| `email`    | Email address.                                                    |
| `role`     | Oid token of the member's role, e.g. `Role:2`.                    |
| `avatar`   | URL to the avatar image.                                          |

### `targetAsset`

| Field       | Description                                                                                |
| ----------- | ------------------------------------------------------------------------------------------ |
| `_oid`      | Oid token of the asset — often **including a moment**, e.g. `Story:123:456`.                |
| `assetType` | The asset type system name.                                                                 |
| `href`      | Deep link to the asset in the Agility UI.                                                   |

:::warning `targetAsset._oid` may carry a moment
`Story:123:456` is Story 123 **at version 456**, not Story 123. If you use this value as a
correlation key or pass it back into the API expecting current data, strip the third segment
first. Note that `href` and `snapshot[]._oid` in the same payload use the momentless form
(`Story:123`) — the two are not interchangeable.
:::

### `changes[]`

One entry per attribute affected.

| Field  | Description                                                             |
| ------ | ----------------------------------------------------------------------- |
| `name` | The attribute definition name.                                          |
| `old`  | Previous value. Absent where there was none — including on relation adds. |
| `new`  | New value.                                                              |
| `act`  | What was done: `Set`, `Added`, `Removed`.                               |

Scalar attributes are `Set` and carry both `old` and `new`. Multi-value relations are `Added` or
`Removed` and carry only `new` — the Oid token of the value added or removed, not the whole
resulting collection.

### `snapshot[]`

The projection requested by the event type's [`select`](/apis/webhooks/event-types#include-asset-details-in-the-delivery),
in the same shape a [`query.v1`](/apis/query-v1) result would take: `_oid` plus one key per
selected path. Multi-value projections are arrays, and relation projections without a further
attribute come back as `{ "_oid": ... }` objects.

With no `select` on the event type, `snapshot` carries little more than the `_oid`.

## Verifying the Payload

If you set a `secret` on the subscription, every delivery carries
`X-SIGNATURE-HMAC-SHA512`: the Base64-encoded HMAC-SHA512 of the **raw UTF-8 request body**,
keyed with your secret.

:::danger Verify against the raw body
Compute the HMAC over the exact bytes received, before any JSON parsing or re-serialization.
Parsing and re-encoding will change whitespace and key order and the signature will not match.
Capture the raw body first, verify, then parse.
:::

```python title="Verifying a delivery in Python"
import base64
import hashlib
import hmac

def is_signature_valid(raw_body: bytes, secret: str, header_value: str) -> bool:
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha512).digest()
    return hmac.compare_digest(
        base64.b64encode(expected).decode("ascii"),
        header_value,
    )
```

Always compare in constant time, as above.

:::note `secret` was undocumented
Neither the `secret` subscription field nor the signature headers appeared in the published
reference material.

`secret` is write-only from the API's point of view: it is accepted on
[create](/apis/webhooks/create) and [update](/apis/webhooks/update), but it is never returned by
[query](/apis/webhooks/query) or included in [delivery receipts](/apis/webhooks/status). Store
your copy of it when you set it.
:::

### `authorizationHeader` Versus `secret`

They solve different problems and are worth using together:

| Field                 | Direction                            | Purpose                                                                 |
| --------------------- | ------------------------------------ | ----------------------------------------------------------------------- |
| `authorizationHeader` | Agility proves itself **to you**     | Sent verbatim as the `Authorization` header so your endpoint can authenticate the caller. |
| `secret`              | Agility proves the **payload**       | Signs the body, so you know it was not altered and really came from your instance.         |

A static `Authorization` value proves the caller knows a shared token, but does nothing about
payload integrity, and it is replayable if intercepted. The HMAC signature covers the body.

## Responding

Return **`200 OK`**. Any other status counts as a failed delivery, and consecutive failures
take the subscription out of service — see
[delivery semantics](/apis/webhooks#delivery-semantics).

Acknowledge quickly and process asynchronously: enqueue the payload, return `200`, and do the
work outside the request. A slow handler produces timeouts, which count as failures.

Make handlers **idempotent**. Deduplicate on `webhookId` plus `sequenceId`; a retry after a
response you actually processed will redeliver the event.

## Related Topics

- [Event types](/apis/webhooks/event-types) — controlling what gets delivered
- [Delivery status](/apis/webhooks/status) — what your endpoint sent back
- [Create a subscription](/apis/webhooks/create)
