Skip to main content
A webhook URL is a public, unauthenticated endpoint. Anyone who learns it can send it a forged request. Signed webhooks let your receiver confirm that a delivery actually came from Salesforge and was not altered in transit.
Only webhooks created through the public API are signed today. Webhooks created in the app UI, Zapier, or Make are not signed yet.

Prerequisites

  • WORKSPACE_ID
  • SALESFORGE_API_KEY
  • A publicly reachable URL that answers 2xx to receive deliveries
For the shared authentication pattern, see Authentication.

How it works

Each webhook gets its own secret, generated once and returned only in the create response. Every delivery is signed with HMAC-SHA256 over the event ID, the send timestamp, and the raw request body.
1

Create a signed webhook

POST /workspaces/{workspaceID}/integrations/webhooks
type is the event that triggers this webhook. Omit sequenceIds to receive the event from every sequence, or set it to scope the webhook to specific sequences.The response includes signingSecret:
signingSecret is returned exactly once. Store it immediately — it is never included in later responses. If you lose it, delete the webhook and create a new one; there is no rotation or retrieval endpoint.
2

Read what arrives with each delivery

Example delivery used throughout this guide:
Treat the body as opaque bytes until the signature check passes — do not parse it first.
3

Verify the signature

  1. Split the signature header on ,, then each part on its first =, to read t and v1.
  2. Reject the delivery if t is more than 300 seconds from your current time.
  3. Rebuild the signed content by joining the event ID, the t value exactly as received, and the raw request body with literal . characters: event_id + "." + t + "." + raw_body.
  4. Compute HMAC-SHA256 of that content using the full secret — including the whsec_ prefix — as the key, and compare the result to v1 with a constant-time comparison.
Reject any delivery with no signature header. A missing header means the request either did not come from Salesforge, or came from a webhook created through a path that does not sign yet.
Always compare signatures with a constant-time function — hmac.compare_digest, crypto.timingSafeEqual, subtle.ConstantTimeCompare, or MessageDigest.isEqual. A regular == leaks timing information an attacker can use to guess the signature byte by byte.
Read the raw body before any JSON-parsing middleware touches it, or the signature will never match:
4

Test your implementation

Run this known-good vector through your verifier before pointing it at live traffic:
With the timestamp check disabled (the vector is old), it must return true. Flip one byte of the secret, event ID, or body, and it must return false. If a mutation still passes, your verifier is reading something other than what it signs.

Common mistakes