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:

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:

{
  "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:

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:

{
  "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:

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:

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.

JotBird Logo
Published with JotBird