> For the complete documentation index, see [llms.txt](https://documentation.pushly.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://documentation.pushly.com/platform/event-webhooks.md).

# Event Webhooks

## Event Webhooks

Event Webhooks deliver real-time HTTP callbacks when subscriber events occur. Configure one or more endpoints per domain to receive structured event data as it happens.

### Configuration

Navigate to **Domain Settings → Event Webhooks** to create, edit, or archive webhook endpoints.

Each webhook specifies:

* **URL**: the destination endpoint
* **Headers**: custom headers, with optional Liquid macros for secrets
* **Events**: one or more event types to subscribe to

All deliveries are POST with a JSON body containing the full event payload.

<figure><img src="/files/JuRQdak4kDZJ3fhEM9Sf" alt=""><figcaption></figcaption></figure>

### Supported Events

| Group        | Event                    | Description                                  |
| ------------ | ------------------------ | -------------------------------------------- |
| Notification | `notification.displayed` | A subscriber's device renders a notification |
| Notification | `notification.clicked`   | A subscriber clicks a notification           |

Each event fires once per subscriber per action. A notification sent to 10,000 subscribers produces up to 10,000 individual events.

### Payload Structure

Fixed envelope with event-specific data. Null fields are omitted.

{% tabs %}
{% tab title="Notification Events" %}
{% code expandable="true" %}

```javascript
{
  "version": 1,
  "event_type": "notification.displayed",
  "event_id": "d426d74a-b0cc-46e3-9a6d-d12ac346effe",
  "event_timestamp": "2026-05-08T14:17:30.452Z",
  "domain": {
    "id": 1234,
    "name": "Cafe 80s",
    "url": "cafe80s.com"
  },
  "user": {
    "id": "atk3i6E0NtohVy4UlvvkGQfl6J5wNTYp",
    "external_id": "11298b50-c1a7-4db3-aebc-829fed8b0d94",
    "device": {
      "platform": "web",
      "type": "desktop",
      "os": "macos",
      "os_version": "14.4",
      "browser": "chrome",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
      "time_zone": "America/Chicago",
      "location": {
        "city": "Hill Valley",
        "province": "California",
        "country_code": "US",
        "postal_code": "19851",
        "continent_code": "NA"
      }
    }
  },
  "data": {
    "notification": {
      "id": 21015,
      "title": "Save the clock tower!",
      "body": "Mayor Wilson's preservation society needs your help — donate before lightning strikes",
      "keywords": ["hill-valley", "preservation", "clock-tower"],
      "send_timestamp": "2026-05-08T14:17:25.000Z",
      "source": "manual",
      "delivery_type": "scheduled",
      "segment_ids": [1955, 1985],
      "segment_names": ["Hill Valley Residents", "Preservation Society Members"],
      "campaign": {
        "id": 88,
        "step_id": 1
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Envelope

| Field             | Type                   | Description                                                                  |
| ----------------- | ---------------------- | ---------------------------------------------------------------------------- |
| `version`         | int                    | Payload schema version                                                       |
| `event_type`      | string                 | Dot-namespaced event identifier (e.g. `notification.clicked`)                |
| `event_id`        | string (UUID)          | Stable identifier for this event. Use as your deduplication key.             |
| `event_timestamp` | string (ISO 8601, UTC) | When the event occurred                                                      |
| `domain`          | object                 | The Pushly domain the event belongs to                                       |
| `user`            | object                 | The subscriber who triggered the event                                       |
| `data`            | object                 | Event-specific content. Contains exactly one key matching the event subject. |

The `data` object contains a single key matching the event's subject. For `notification.*` events, that key is `notification`.

All timestamps are ISO 8601 UTC with millisecond precision.

### Header Macros

Custom header values support Liquid template syntax for referencing stored domain secrets:

```
Authorization: Bearer {{ secret.analytics_token }}
```

Only secrets explicitly referenced in header templates are loaded. System-managed secrets (aliases starting with `_`) are not accessible in templates.

### Headers

Every delivery includes:

| Header                        | Description                                                  |
| ----------------------------- | ------------------------------------------------------------ |
| `X-Pushly-Webhook-Id`         | UUID identifying which webhook config produced this delivery |
| `X-Pushly-Event-Id`           | The unique event ID from the payload. Use for deduplication. |
| `X-Pushly-Signature`          | HMAC-SHA256 signature of the request body (see below)        |
| `X-Pushly-Delivery-Timestamp` | Unix timestamp (seconds) when the delivery was initiated     |
| `X-Pushly-Attempt-Number`     | 1-indexed attempt number                                     |
| `Content-Type`                | `application/json`                                           |

Reserved header names (`Content-Type`, `Host`, `Content-Length`, `Transfer-Encoding`, anything starting with `X-Pushly-`) cannot be overridden by custom headers in your webhook configuration.

### Signature Verification

Every delivery includes an `X-Pushly-Signature` header for authenticity verification.

Format: `sha256=<hex_digest>`

Verification algorithm:

1. Encode your signing secret as UTF-8 bytes
2. Compute HMAC-SHA256 over the raw request body using the secret as the key
3. Hex-encode the digest
4. Compare to the value after `sha256=` in the header using constant-time comparison

**Important:** Verify against the raw request body, not a parsed and re-serialized version. JSON key ordering and whitespace affect the digest.

**Replay prevention (optional):** Compare `X-Pushly-Delivery-Timestamp` to your server's current time. Reject deliveries older than your tolerance window (e.g. 5 minutes).

Your signing secret is available in **Domain Settings → Event Webhooks → Signing Secret**.

### Idempotency

Pushly produces one event per subscriber per action. Events may be delivered more than once if your endpoint fails or times out — use `event_id` for deduplication, and use a durable queue on your side if processing happens asynchronously after acknowledgment.

### Delivery

#### Timeout

Your endpoint must respond within 5 seconds. Acknowledge immediately and process asynchronously if your logic takes longer.

#### Retries

| Attempt | Delay                           |
| ------- | ------------------------------- |
| 1       | Immediate                       |
| 2       | 4 seconds after first failure   |
| 3       | 16 seconds after second failure |

After 3 attempts, the event moves to a dead-letter queue.

Retried: HTTP 5xx, timeouts, connection errors. Not retried: HTTP 4xx, SSRF blocks.

#### Ordering

Events may arrive out of order. Use `event_timestamp` for chronological ordering, not delivery order.

### Network Security

The primary authentication mechanism for webhook deliveries is HMAC signature verification (above). Every delivery includes an `X-Pushly-Signature` header you can validate against the request body using your signing secret. This is cryptographic proof the request came from Pushly — independent of network origin and resilient to infrastructure changes on our side.

#### IP Allowlisting

We don't publish a static list of egress IPs. If your security or compliance policy requires IP-based network controls, contact your Pushly account manager to discuss your requirements.

#### TLS

Webhook endpoints must be reachable over HTTPS. Pushly does not deliver to plain HTTP URLs. We require TLS 1.2 or higher and validate your endpoint's certificate against standard public certificate authorities. Self-signed certificates are not supported.

#### SSRF Protection

Pushly validates every destination URL against private and reserved IP ranges before connecting. Requests to private (RFC 1918), loopback, link-local, and instance-metadata addresses are rejected.

### Versioning

New fields and event types are added without incrementing `version`. Ignore unknown fields in your handlers.

Breaking changes (field removals or renames) increment `version` with a deprecation window.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://documentation.pushly.com/platform/event-webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
