Subscribe to webhooks and troubleshoot delivery

Set up a Platform API webhook receiver, verify signatures, test delivery, and handle retries, pauses, and subscription removal.

Updated

Platform API webhooks send workspace events to an HTTPS endpoint that your integration controls. They are separate from incoming automation triggers, automation connections, and Live Chat's customer-facing realtime connection.

Use https://api.protodesk.io/v1 as the API base. The public OpenAPI contract describes the available requests and response fields. This guide is for developers building a server-side integration.

Prepare access and a receiver

You need a workspace API key with webhooks:manage. This scope manages subscriptions and reads delivery history; Help Center Read articles and Submit proposals permissions are not sufficient. See Create and manage API keys.

Check the key's workspace with GET /v1/me before creating anything. Keep the key on your server, outside browser code, URLs, and logs. Test and Live key labels do not isolate production data.

Choose a receiver authorized to handle that workspace's event data. Payloads can contain customer or conversation information: do not point production subscriptions at public request bins or an unapproved third party.

The receiver must have a publicly reachable HTTPS URL without embedded credentials. Localhost and private-network destinations are rejected by the sender. Configure the final URL directly; delivery does not follow redirects.

Create a subscription

Send POST /v1/webhooks with Bearer authentication, JSON content, and a unique Idempotency-Key. For example, the following request body creates a disabled subscription so you can prepare and test the receiver before enabling ordinary deliveries:

{
  "url": "https://your-service.example/webhooks/protodesk",
  "events": ["conversation.created", "message.created"],
  "status": "disabled"
}

Replace the example URL with your approved endpoint. Save the returned subscription id and secret securely. The signing secret is included in the creation response; normal list and update responses do not reveal it. Do not publish that response or include it in application logs.

status supports active and disabled; omitting it defaults to active. Subscribe only to the events you need. The accepted event names are:

  • Customers: customer.created, customer.updated.
  • Conversations: conversation.created, conversation.updated, conversation.assigned, conversation.resolved, conversation.read.
  • Messages: message.created, message.delivered, message.failed.
  • Attachments: attachment.created.
  • Assignments: assignment.offered, assignment.accepted, assignment.declined, assignment.revoked, assignment.expired.
  • AI runs: ai.run.completed.

Accepted event names do not imply that every channel or product action produces every event. Verify the specific workflow your integration depends on. webhook.test is a synthetic test event, not a subscription event name.

Verify signatures before processing

Protodesk sends JSON by HTTP POST with these headers:

  • Protodesk-Event-Id
  • Protodesk-Event-Type
  • Protodesk-Timestamp
  • Protodesk-Signature

The JSON envelope contains id, type, apiVersion, createdAt, and data. Preserve the exact raw request-body bytes before parsing JSON.

To verify a delivery, use the complete signing-secret string as the HMAC key. Calculate HMAC-SHA256 over the timestamp header, a literal period, and the raw body:

signed bytes = UTF8(Protodesk-Timestamp) + UTF8(".") + raw body
expected header = "v1=" + lowercase_hex(HMAC_SHA256(secret, signed bytes))

Compare the expected and received signatures using a constant-time comparison. The timestamp is RFC 3339 text, not a Unix-seconds number. Verify that it is parseable and within your receiver's chosen short clock-skew/replay window; keep your server clock synchronized. That window is a receiver security decision, not an API guarantee. Reject invalid signatures before processing the payload.

Re-encoding parsed JSON changes the bytes and can break verification. An event ID or type header alone is not authentication. Use the signed body's event ID to deduplicate work after verification.

Test, then enable

Send POST /v1/webhooks/{webhookId}/test only when you intend to send a real HTTP request to the configured destination. This works even while the subscription is disabled and does not enable it.

The API returns a test result with eventId, delivered, responseStatus, and responseSnippet. HTTP 200 from the test API is not proof of delivery: inspect delivered. The synthetic test is synchronous and does not create an ordinary delivery-history entry or a background retry series.

Once verification and handling work, enable the subscription with PATCH /v1/webhooks/{webhookId} and body {"status":"active"}. Test the relevant product workflow separately; a synthetic test establishes receiver connectivity, not complete event coverage.

Handle retries and inspect delivery history

Durably accept verified work, then return a 2xx response promptly and process it asynchronously. The sender's request timeout is 10 seconds. A timeout, network error, redirect, or other non-2xx response is a failed attempt.

Normal deliveries have up to six attempts. Retry delays after the first five failures are approximately 30 seconds, 2 minutes, 10 minutes, 1 hour, and 6 hours, plus worker scheduling time. After 20 consecutive delivery series exhaust their attempts, the subscription is automatically disabled. A successful delivery resets that failure count.

Handle duplicates and out-of-order arrivals. A timeout does not prove your receiver did no work, and retries can overlap newer events. Do not treat webhook delivery as exactly-once processing.

Use GET /v1/webhooks/{webhookId}/deliveries to inspect event IDs, attempt counts, status, the latest response status/snippet, and completion time. Each row summarizes one event's delivery series, not a full log of every response from every attempt. Results are paginated with limit, cursor, hasMore, and nextCursor.

Check DNS/TLS, signature handling, response time, redirects, and your receiver logs. Keep response bodies free of secrets: a short snippet is retained for diagnostics. Preserve event and request IDs when asking support for help, without sharing keys or unnecessary customer payloads.

Pause, recover, or remove

List subscriptions using GET /v1/webhooks. Pause ordinary delivery with PATCH /v1/webhooks/{webhookId} and {"status":"disabled"}. Requests already in flight cannot be recalled. Explicit test requests can still send while disabled.

After repairing the receiver, test it and deliberately re-enable the subscription. Already-pending deliveries may resume; exhausted failures are not automatically replayed by re-enabling. Events missed while disabled are not guaranteed to be backfilled. Reconcile affected resources through supported read APIs. There is no dedicated public manual-replay endpoint in the current contract.

To replace a lost or compromised signing secret, create and test a replacement subscription, then disable/remove the old one. There is no in-place secret-rotation request in this contract. Plan for duplicate events during overlap.

DELETE /v1/webhooks/{webhookId} removes the subscription and its delivery history. Save necessary non-secret diagnostic references first. Revoking the API key used to create a subscription does not itself stop the subscription; manage the subscription separately.

Related articles