---
title: Terminal
url: https://share.jotbird.com/terminal
updated_at: 2026-04-20T20:56:01.456653+00:00
---

# Webhook Integration Guide

Reference for configuring and consuming webhooks from the Relay event pipeline. All payloads are JSON over HTTPS with HMAC-SHA256 signatures.

## Authentication

Every webhook request includes a signature header for verification:

```
X-Relay-Signature: sha256=a1b2c3d4e5f6...
```

Verify the signature against your endpoint secret:

```python
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)
```

Reject any request where the signature does not match. Replay attacks are mitigated by the `timestamp` field in the payload — reject events older than 5 minutes.

## Event Types

| Event | Trigger | Retry Policy |
|-------|---------|--------------|
| `pipeline.started` | Pipeline execution begins | 3 attempts, exponential backoff |
| `pipeline.completed` | All stages finished successfully | 3 attempts, exponential backoff |
| `pipeline.failed` | Any stage exits with non-zero code | 5 attempts, 30s intervals |
| `stage.started` | Individual stage begins execution | No retry |
| `stage.completed` | Individual stage finished | No retry |
| `artifact.created` | Build artifact uploaded to storage | 3 attempts, exponential backoff |
| `deployment.promoted` | Artifact promoted to production | 5 attempts, 30s intervals |

## Payload Structure

All events share a common envelope:

```json
{
  "id": "evt_a1b2c3d4",
  "type": "pipeline.completed",
  "timestamp": "2026-03-10T14:32:00Z",
  "project_id": "proj_x7y8z9",
  "data": {
    "pipeline_id": "pipe_m4n5o6",
    "duration_ms": 34521,
    "stages": [
      {
        "name": "build",
        "status": "passed",
        "duration_ms": 18200
      },
      {
        "name": "test",
        "status": "passed",
        "duration_ms": 12100
      },
      {
        "name": "deploy",
        "status": "passed",
        "duration_ms": 4221
      }
    ],
    "commit": {
      "sha": "e7f8a9b0c1d2",
      "message": "fix: resolve race condition in queue consumer",
      "author": "alex@relay.dev"
    }
  }
}
```

## Endpoint Configuration

Register a webhook endpoint via the API:

```bash
curl -X POST https://api.relay.dev/v1/webhooks \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/relay",
    "events": ["pipeline.completed", "pipeline.failed"],
    "secret": "whsec_your_signing_secret"
  }'
```

Response:

```json
{
  "id": "wh_p1q2r3",
  "url": "https://your-app.com/webhooks/relay",
  "events": ["pipeline.completed", "pipeline.failed"],
  "status": "active",
  "created_at": "2026-03-10T14:00:00Z"
}
```

## Handling Failures

Your endpoint must return a `2xx` status code within **10 seconds**. Any other response triggers the retry policy for that event type.

| Response | Behavior |
|----------|----------|
| `200–299` | Event acknowledged, no retry |
| `301–399` | Redirects are **not** followed |
| `400–499` | Client error, retried per policy |
| `500–599` | Server error, retried per policy |
| Timeout | No response in 10s, retried per policy |

If all retries are exhausted, the event is written to the dead letter queue. Retrieve failed events with:

```bash
curl https://api.relay.dev/v1/webhooks/wh_p1q2r3/dead-letter \
  -H "Authorization: Bearer $RELAY_API_KEY"
```

## Rate Limits

| Tier | Max Events/min | Max Endpoints |
|------|---------------|---------------|
| Free | 60 | 2 |
| Pro | 600 | 10 |
| Enterprise | 6,000 | Unlimited |

Events that exceed your rate limit are queued and delivered with a best-effort delay of up to 60 seconds.

## Testing

Send a test event to your endpoint without triggering a real pipeline:

```bash
curl -X POST https://api.relay.dev/v1/webhooks/wh_p1q2r3/test \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -d '{"type": "pipeline.completed"}'
```

The test payload uses synthetic data and includes `"test": true` in the envelope. Your handler should check for this flag and skip any production side effects.