Webhooks
Receive real-time notifications when events happen in your SkipUp workspace.
Webhooks let you subscribe to events in your SkipUp workspace. When an event occurs, SkipUp sends an HTTP POST request to your configured URL with a JSON payload describing what happened.
Webhook endpoint object
Section titled “Webhook endpoint object”| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the webhook endpoint. |
url | string | The URL that receives webhook deliveries. |
description | string | null | A human-readable description of the endpoint. |
enabled | boolean | Whether the endpoint is currently active. |
events | string[] | List of event types this endpoint subscribes to. |
disabled_at | string | null | ISO 8601 timestamp of when the endpoint was disabled. |
disabled_reason | string | null | Reason the endpoint was disabled (e.g., too_many_failures). |
created_at | string | ISO 8601 timestamp of when the endpoint was created. |
updated_at | string | ISO 8601 timestamp of the last update. |
Webhook delivery object
Section titled “Webhook delivery object”| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the delivery attempt. |
event_type | string | The event type that triggered this delivery. |
status | string | Delivery status: pending, success, or failed. |
payload | object | The JSON payload that was sent. |
response_code | integer | null | HTTP response code from your server. |
attempts | integer | Number of delivery attempts made. |
delivered_at | string | null | ISO 8601 timestamp of successful delivery. |
last_attempted_at | string | null | ISO 8601 timestamp of the most recent attempt. |
created_at | string | ISO 8601 timestamp of when the delivery was created. |
List webhook endpoints
Section titled “List webhook endpoints”GET /api/v1/webhook_endpointsReturns a paginated list of webhook endpoints in your workspace, sorted by creation date (newest first).
Scope required: webhooks.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
limit | integer | Number of results per page. Default 25, max 100. |
cursor | string | Cursor for fetching the next page. |
Request
Section titled “Request”curl https://api.skipup.ai/api/v1/webhook_endpoints \ -H "Authorization: Bearer $SKIPUP_API_KEY"Response
Section titled “Response”{ "data": [ { "id": "we_01HQ...", "url": "https://example.com/webhooks/skipup", "description": "Production webhook handler", "enabled": true, "events": ["meeting_request.booked", "meeting_request.cancelled"], "disabled_at": null, "disabled_reason": null, "created_at": "2025-01-10T12:00:00Z", "updated_at": "2025-01-10T12:00:00Z" } ], "meta": { "limit": 25, "has_more": false }}Get a webhook endpoint
Section titled “Get a webhook endpoint”GET /api/v1/webhook_endpoints/:idRetrieves a single webhook endpoint by ID.
Scope required: webhooks.read
Request
Section titled “Request”curl https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ... \ -H "Authorization: Bearer $SKIPUP_API_KEY"Response
Section titled “Response”{ "data": { "id": "we_01HQ...", "url": "https://example.com/webhooks/skipup", "description": "Production webhook handler", "enabled": true, "events": ["meeting_request.booked", "meeting_request.cancelled"], "disabled_at": null, "disabled_reason": null, "created_at": "2025-01-10T12:00:00Z", "updated_at": "2025-01-10T12:00:00Z" }}Create a webhook endpoint
Section titled “Create a webhook endpoint”POST /api/v1/webhook_endpointsCreates a new webhook endpoint. The endpoint starts receiving deliveries immediately.
Scope required: webhooks.write
Request body
Section titled “Request body”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The HTTPS URL to receive webhook deliveries. |
events | string[] | Yes | Event types to subscribe to. See Event types. |
description | string | No | A human-readable description. |
headers | object[] | No | Custom HTTP headers to include with each delivery. Each object has key and value fields. |
Request
Section titled “Request”curl -X POST https://api.skipup.ai/api/v1/webhook_endpoints \ -H "Authorization: Bearer $SKIPUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/skipup", "events": ["meeting_request.booked", "meeting_request.cancelled"], "description": "Production webhook handler", "headers": [ {"key": "X-Custom-Auth", "value": "my-secret-token"} ] }'Response 201 Created
Section titled “Response 201 Created”{ "data": { "id": "we_01HQ...", "url": "https://example.com/webhooks/skipup", "description": "Production webhook handler", "enabled": true, "events": ["meeting_request.booked", "meeting_request.cancelled"], "disabled_at": null, "disabled_reason": null, "created_at": "2025-01-10T12:00:00Z", "updated_at": "2025-01-10T12:00:00Z" }}Update a webhook endpoint
Section titled “Update a webhook endpoint”PATCH /api/v1/webhook_endpoints/:idUpdates an existing webhook endpoint. You can modify any combination of fields.
Scope required: webhooks.write
Request body
Section titled “Request body”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | No | Updated delivery URL. |
events | string[] | No | Updated list of event types to subscribe to. |
description | string | No | Updated description. |
enabled | boolean | No | Set to false to pause deliveries, true to resume. |
Request
Section titled “Request”curl -X PATCH https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ... \ -H "Authorization: Bearer $SKIPUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": ["meeting_request.booked", "meeting_request.cancelled", "meeting_request.created"], "enabled": true }'Response
Section titled “Response”{ "data": { "id": "we_01HQ...", "url": "https://example.com/webhooks/skipup", "description": "Production webhook handler", "enabled": true, "events": ["meeting_request.booked", "meeting_request.cancelled", "meeting_request.created"], "disabled_at": null, "disabled_reason": null, "created_at": "2025-01-10T12:00:00Z", "updated_at": "2025-01-20T09:00:00Z" }}Delete a webhook endpoint
Section titled “Delete a webhook endpoint”DELETE /api/v1/webhook_endpoints/:idPermanently deletes a webhook endpoint. Pending deliveries for this endpoint will not be sent.
Scope required: webhooks.write
Request
Section titled “Request”curl -X DELETE https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ... \ -H "Authorization: Bearer $SKIPUP_API_KEY"Response 204 No Content
Section titled “Response 204 No Content”No response body.
Test a webhook endpoint
Section titled “Test a webhook endpoint”POST /api/v1/webhook_endpoints/:id/testSends a test delivery to the endpoint. Use this to verify your webhook handler is working correctly.
Scope required: webhooks.write
Request
Section titled “Request”curl -X POST https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ.../test \ -H "Authorization: Bearer $SKIPUP_API_KEY"Response
Section titled “Response”{ "data": { "delivery_id": "wd_01HQ...", "status": "queued" }}The test delivery is queued and sent asynchronously. The payload contains:
{ "event": "test", "timestamp": "2025-01-20T14:30:00Z", "message": "Test webhook delivery"}List deliveries
Section titled “List deliveries”GET /api/v1/webhook_endpoints/:webhook_endpoint_id/deliveriesReturns a paginated list of webhook deliveries for a specific endpoint, sorted by creation date (newest first).
Scope required: webhooks.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
status | string | Filter by delivery status: pending, success, or failed. |
event_type | string | Filter by event type (e.g., meeting_request.booked). |
limit | integer | Number of results per page. Default 25, max 100. |
cursor | string | Cursor for fetching the next page. |
Request
Section titled “Request”curl "https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ.../deliveries?status=failed" \ -H "Authorization: Bearer $SKIPUP_API_KEY"Response
Section titled “Response”{ "data": [ { "id": "wd_01HQ...", "event_type": "meeting_request.booked", "status": "failed", "payload": { "event": "meeting_request.booked", "timestamp": "2025-01-20T14:30:00Z", "id": "mr_01HQ...", "status": "booked", "booked_at": "2025-01-20T14:30:00Z" }, "response_code": 500, "attempts": 8, "delivered_at": null, "last_attempted_at": "2025-01-20T16:00:00Z", "created_at": "2025-01-20T14:30:00Z" } ], "meta": { "limit": 25, "has_more": false }}Event types
Section titled “Event types”Subscribe to these events when creating or updating a webhook endpoint.
meeting_request.created
Section titled “meeting_request.created”Fired when a new meeting request is created.
{ "event": "meeting_request.created", "timestamp": "2025-01-20T14:30:00Z", "id": "mr_01HQ...", "status": "active", "created_at": "2025-01-20T14:30:00Z"}meeting_request.booked
Section titled “meeting_request.booked”Fired when a meeting request has been successfully scheduled and calendar invites sent.
{ "event": "meeting_request.booked", "timestamp": "2025-01-20T16:00:00Z", "id": "mr_01HQ...", "calendar_event_id": "evt_abc123", "status": "booked", "booked_at": "2025-01-20T16:00:00Z"}meeting_request.cancelled
Section titled “meeting_request.cancelled”Fired when a meeting request is cancelled.
{ "event": "meeting_request.cancelled", "timestamp": "2025-01-21T10:00:00Z", "id": "mr_01HQ...", "status": "cancelled", "cancelled_at": "2025-01-21T10:00:00Z", "was_booked": false}The was_booked field indicates whether the meeting had been previously booked before it was cancelled.
meeting_request.paused
Section titled “meeting_request.paused”Fired when a meeting request is paused.
{ "event": "meeting_request.paused", "timestamp": "2025-01-22T09:00:00Z", "id": "mr_01HQ...", "status": "paused", "paused_at": "2025-01-22T09:00:00Z"}meeting_request.resumed
Section titled “meeting_request.resumed”Fired when a paused meeting request is resumed back to active scheduling.
{ "event": "meeting_request.resumed", "timestamp": "2025-01-23T11:00:00Z", "id": "mr_01HQ...", "status": "active", "resumed_at": "2025-01-23T11:00:00Z"}meeting_request.organizer_changed
Section titled “meeting_request.organizer_changed”Fired when the organizer of a meeting request is changed.
{ "event": "meeting_request.organizer_changed", "timestamp": "2025-01-21T11:00:00Z", "id": "mr_01HQ...",}meeting_request.follow_up_sent
Section titled “meeting_request.follow_up_sent”Fired when SkipUp sends a follow-up email to participants who haven’t responded.
member.deactivated
Section titled “member.deactivated”Fired when a workspace member is deactivated.
Sent when you use the test endpoint. Useful for verifying your webhook handler is reachable and working.
{ "event": "test", "timestamp": "2025-01-20T14:30:00Z", "message": "Test webhook delivery"}Signature verification
Section titled “Signature verification”Every webhook delivery includes a signature in the X-Webhook-Signature header so you can verify that the request came from SkipUp and hasn’t been tampered with.
Headers sent with each delivery
Section titled “Headers sent with each delivery”| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the payload. |
X-Webhook-Timestamp | Unix timestamp (seconds) of when the delivery was sent. |
X-Webhook-Event | The event type (e.g., meeting_request.booked). |
Content-Type | Always application/json. |
Signature format
Section titled “Signature format”The X-Webhook-Signature header uses this format:
t=1706000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— The same timestamp asX-Webhook-Timestamp.v1— The HMAC-SHA256 hex digest of the signed payload.
How to verify
Section titled “How to verify”- Extract the timestamp (
t) and signature (v1) from theX-Webhook-Signatureheader. - Build the signed payload by concatenating the timestamp, a period (
.), and the raw request body:{timestamp}.{body} - Compute the expected signature using HMAC-SHA256 with your webhook endpoint’s signing secret.
- Compare your computed signature to the
v1value. Use a constant-time comparison to prevent timing attacks.
Example (Ruby)
Section titled “Example (Ruby)”def verify_webhook(request, secret) signature_header = request.headers["X-Webhook-Signature"] timestamp = signature_header[/t=(\d+)/, 1] received_sig = signature_header[/v1=(\w+)/, 1]
signed_payload = "#{timestamp}.#{request.body.read}" expected_sig = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
ActiveSupport::SecurityUtils.secure_compare(expected_sig, received_sig)endExample (Node.js)
Section titled “Example (Node.js)”const crypto = require("crypto");
function verifyWebhook(headers, body, secret) { const signature = headers["x-webhook-signature"]; const [tPart, vPart] = signature.split(","); const timestamp = tPart.replace("t=", ""); const receivedSig = vPart.replace("v1=", "");
const signedPayload = `${timestamp}.${body}`; const expectedSig = crypto .createHmac("sha256", secret) .update(signedPayload) .digest("hex");
return crypto.timingSafeEqual( Buffer.from(expectedSig), Buffer.from(receivedSig) );}Example (Python)
Section titled “Example (Python)”import hmacimport hashlib
def verify_webhook(headers, body, secret): signature = headers["X-Webhook-Signature"] parts = dict(p.split("=", 1) for p in signature.split(",")) timestamp = parts["t"] received_sig = parts["v1"]
signed_payload = f"{timestamp}.{body}" expected_sig = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest()
return hmac.compare_digest(expected_sig, received_sig)Retry behavior
Section titled “Retry behavior”SkipUp retries failed deliveries up to 8 times using polynomial backoff. The approximate retry schedule is:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | ~3 seconds |
| 3 | ~18 seconds |
| 4 | ~83 seconds |
| 5 | ~4 minutes |
| 6 | ~10 minutes |
| 7 | ~22 minutes |
| 8 | ~40 minutes |
A delivery is considered successful when your server responds with a 2xx status code. Any other status code (or a connection error) is treated as a failure.
Deliveries time out after 10 seconds. Make sure your webhook handler responds quickly. If you need to do heavy processing, acknowledge the webhook immediately and process the payload asynchronously.
Automatic disabling
Section titled “Automatic disabling”If a webhook endpoint accumulates 10 or more failed deliveries within a 24-hour period, SkipUp automatically disables it. When this happens:
- The
enabledfield is set tofalse. - The
disabled_attimestamp is set. - The
disabled_reasonis set totoo_many_failures.
To re-enable the endpoint, fix the issue with your server and then update the endpoint:
curl -X PATCH https://api.skipup.ai/api/v1/webhook_endpoints/we_01HQ... \ -H "Authorization: Bearer $SKIPUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"enabled": true}'