# Webhooks Webhooks send real-time HTTP notifications to your server when events happen in RelayBook. Use them to trigger workflows, sync data to external systems, or build integrations without polling the API. > [!quote] Two-Way Data Flow > The API lets external systems ==push data into RelayBook==. Webhooks let RelayBook ==push events out==. Together, they make RelayBook the central hub for contact data. --- ## How Webhooks Work 1. A team admin creates a webhook with a **URL** and a list of **events** to subscribe to 2. When a subscribed event occurs (e.g. a contact is created), RelayBook sends an HTTP POST request to your URL 3. Your server receives the payload, verifies the signature, and processes the event 4. RelayBook logs every delivery attempt for debugging --- ## Available Events | Event | Trigger | |-------|---------| | `contact.created` | A contact is added to a team book | | `contact.updated` | A contact's details are changed | | `contact.deleted` | A contact is removed from a team book | | `member.joined` | A new member joins the team | | `member.removed` | A member is removed from the team | | `book.created` | A book is assigned to the team | | `book.deleted` | A book is removed from the team | --- ## Payload Format Every webhook delivery is an HTTP POST with a JSON body. The payload follows a consistent structure across all event types: ```json { "event": "contact.created", "timestamp": "2026-02-18T16:52:56Z", "team_id": "0c03c7ac-2ba0-42a7-b946-4c7e6daee4db", "book_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "data": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "book_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "first_name": "Jane", "last_name": "Cooper", "company": "Acme Corp", "job_title": "Head of Partnerships", "emails": [ { "label": "work", "value": "[email protected]" } ], "phones": [], "notes": "", "starred": false, "birthday": null, "website": "", "addresses": [], "socials": [], "created_at": "2026-02-18T16:52:56Z", "updated_at": "2026-02-18T16:52:56Z" } } ``` ### Payload Fields | Field | Type | Description | |-------|------|-------------| | `event` | string | The event type (e.g. `contact.created`) | | `timestamp` | string | ISO 8601 timestamp of when the event occurred | | `team_id` | UUID | The team that owns the resource | | `book_id` | UUID | The book the resource belongs to | | `data` | object | The resource data (varies by event type) | ### Event-Specific Data **`contact.created` and `contact.updated`** — The `data` field contains the full contact object with all fields. **`contact.deleted`** — The `data` field contains only the contact's `id`, since the full record has been removed: ```json { "event": "contact.deleted", "timestamp": "2026-02-18T17:10:00Z", "team_id": "0c03c7ac-2ba0-42a7-b946-4c7e6daee4db", "data": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } } ``` --- ## Request Headers Every webhook request includes these headers: | Header | Description | |--------|-------------| | `Content-Type` | `application/json` | | `X-Relay-Signature` | HMAC-SHA256 signature for verification | | `X-Relay-Event` | The event type (e.g. `contact.created`) | | `User-Agent` | `RelayBook-Webhooks/1.0` | --- ## Verifying Signatures Every webhook includes an `X-Relay-Signature` header containing an HMAC-SHA256 signature of the request body. Always verify this signature to confirm the request came from RelayBook and hasn't been tampered with. ### How It Works 1. RelayBook computes `HMAC-SHA256(request_body, your_webhook_secret)` 2. The hex-encoded result is sent in the `X-Relay-Signature` header 3. Your server computes the same HMAC and compares the values ### Example: Node.js ```javascript const crypto = require('crypto'); function verifySignature(body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(body, 'utf8') .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // In your webhook handler: app.post('/webhook', (req, res) => { const signature = req.headers['x-relay-signature']; const isValid = verifySignature(req.rawBody, signature, WEBHOOK_SECRET); if (!isValid) { return res.status(401).send('Invalid signature'); } const event = req.body; console.log(`Received ${event.event} for contact ${event.data.id}`); res.status(200).send('OK'); }); ``` ### Example: Python ```python import hmac import hashlib def verify_signature(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode('utf-8'), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) # In your webhook handler: @app.route('/webhook', methods=['POST']) def webhook(): signature = request.headers.get('X-Relay-Signature') if not verify_signature(request.data, signature, WEBHOOK_SECRET): return 'Invalid signature', 401 event = request.json print(f"Received {event['event']} for contact {event['data']['id']}") return 'OK', 200 ``` ### Example: Go ```go func verifySignature(body []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected)) } ``` > [!tip] Always use constant-time comparison > Use `crypto.timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), or `hmac.Equal` (Go) to prevent timing attacks. --- ## Delivery & Retries RelayBook automatically retries failed webhook deliveries with exponential backoff. ### Retry Schedule | Attempt | Delay | Total Elapsed | |---------|-------|---------------| | 1 (initial) | Immediate | 0s | | 2 (retry 1) | 10 seconds | ~10s | | 3 (retry 2) | 60 seconds | ~70s | | 4 (retry 3) | 5 minutes | ~6 min | ### Retry Conditions | Response | Action | |----------|--------| | `2xx` (success) | Delivery marked as successful. No retry. | | `4xx` (client error) | Delivery marked as failed. **No retry** — fix your endpoint. | | `5xx` (server error) | Delivery retried up to 3 times. | | Network error / timeout | Delivery retried up to 3 times. | > [!note] Timeout > Each delivery attempt has a **10-second timeout**. If your server doesn't respond within 10 seconds, the attempt is treated as a network error and retried. ### Best Practices - **Respond quickly.** Return a `200 OK` as soon as you receive the webhook. Process the payload asynchronously if needed. - **Handle duplicates.** In rare cases, the same event may be delivered more than once. Use the `event` + `data.id` + `timestamp` combination to deduplicate. - **Use HTTPS.** Always use an HTTPS endpoint in production to protect webhook payloads in transit. --- ## Delivery Logs Every webhook delivery is logged and accessible from the Team Dashboard. Use delivery logs to debug failed webhooks and monitor integration health. Each log entry includes: | Field | Description | |-------|-------------| | Event type | Which event triggered the delivery | | Payload | The full JSON payload that was sent | | Response status | The HTTP status code your server returned | | Response body | The first 4 KB of your server's response | | Duration | How long the request took (in milliseconds) | | Success | Whether the delivery was successful | | Timestamp | When the delivery was attempted | ### Viewing Deliveries via API Team admins can also query delivery logs programmatically: ``` GET /api/teams/{team_id}/webhooks/{webhook_id}/deliveries?limit=50 ``` This endpoint uses JWT authentication (not API key auth) and requires team admin access. ### Query Parameters | Parameter | Type | Default | Max | Description | |-----------|------|---------|-----|-------------| | `limit` | integer | 50 | 200 | Number of deliveries to return | ### Example Response ```json { "data": [ { "id": "e4071c22-75e5-48b2-86d9-80a6b2bfe90d", "webhook_id": "16fbf52b-a34b-469c-aa35-a7eac07ea975", "event_type": "contact.created", "payload": { "event": "contact.created", "..." : "..." }, "response_status": 200, "response_body": "OK", "duration_ms": 150, "success": true, "created_at": "2026-02-18T16:52:56Z" } ] } ``` --- ## Managing Webhooks Webhooks are managed by team admins through the Team Dashboard or the team management API. These endpoints use JWT authentication (not API key auth). ### Create Webhook ``` POST /api/teams/{team_id}/webhooks ``` ```json { "url": "https://your-server.com/webhook", "events": ["contact.created", "contact.updated", "contact.deleted"] } ``` **Validation rules:** - `url` is required and must be a valid HTTPS URL - `events` is required and must contain at least one valid event type The response includes a `secret` field — this is your **signing secret** for verifying webhook signatures. It is only shown once at creation. ```json { "data": { "webhook": { "id": "16fbf52b-a34b-469c-aa35-a7eac07ea975", "team_id": "0c03c7ac-2ba0-42a7-b946-4c7e6daee4db", "url": "https://your-server.com/webhook", "events": ["contact.created", "contact.updated", "contact.deleted"], "active": true, "created_by": "5e1c820d-b605-4f9e-ae4f-47be164176ca", "created_at": "2026-02-18T16:41:36Z", "updated_at": "2026-02-18T16:41:36Z" }, "secret": "whsec_2beae64c928c7ef9f49e560d70bee05ec3369c1c..." } } ``` > [!note] Store your secret securely > The signing secret (prefixed with `whsec_`) is only returned once when the webhook is created. Store it in a secure location — RelayBook cannot retrieve it later. ### Update Webhook ``` PATCH /api/teams/{team_id}/webhooks/{webhook_id} ``` Update the URL, subscribed events, or active/inactive status. All fields are optional. ```json { "url": "https://new-server.com/webhook", "events": ["contact.created"], "active": false } ``` ### Delete Webhook ``` DELETE /api/teams/{team_id}/webhooks/{webhook_id} ``` Permanently removes the webhook. Returns `204 No Content`. ### List Webhooks ``` GET /api/teams/{team_id}/webhooks ``` Returns all webhooks for the team, including their subscribed events and active status. --- ## Webhook Object | Field | Type | Description | |-------|------|-------------| | `id` | UUID | Unique webhook identifier | | `team_id` | UUID | The team this webhook belongs to | | `url` | string | The endpoint URL that receives events | | `events` | array | List of subscribed event types | | `active` | boolean | Whether the webhook is currently active | | `created_by` | UUID | The admin who created the webhook | | `created_at` | timestamp | When the webhook was created | | `updated_at` | timestamp | When the webhook was last modified | --- ## Testing Webhooks When building an integration, use a request inspection tool to see webhook payloads before writing your handler: 1. **Create a temporary endpoint** using a service like [webhook.site](https://webhook.site) or [RequestBin](https://requestbin.com) 2. **Create a webhook** in RelayBook pointing to that URL 3. **Trigger an event** (create, update, or delete a contact) 4. **Inspect the payload** to understand the data structure 5. **Build your handler** based on the real payload format 6. **Update the webhook URL** to point to your production endpoint > [!success] The Key Idea > Webhooks turn RelayBook into a real-time data source. Instead of polling the API for changes, your systems receive instant notifications when contacts are created, updated, or deleted. Combined with the REST API, this creates a ==two-way data bridge== between RelayBook and your entire tool stack. --- **Next:** See [[Examples & Use Cases]] for real-world integration scenarios with sample code.