Skip to main content

Verify before parsing

Your public HTTPS endpoint receives the three headers below:
Use the endpoint signing secret to compute HMAC-SHA256 over:
Skinloop-Timestamp is the current Unix timestamp in seconds, encoded as a decimal string. Compare the lowercase hexadecimal digest with Skinloop-Signature: v1=... using a constant-time comparison:
Use the timestamp to enforce a freshness window configured by your receiver. Reject malformed headers, stale timestamps, unknown secrets, and altered bodies. Read the request body as raw bytes first; never parse and reserialize it before verification. Store the event ID only after authentication. After verification, require Skinloop-Event-Id to equal the body id. See the complete webhook requests for all headers and body fields.

Complete Node and Express receiver

Install Express and your PostgreSQL client, then register this route before any global express.json() middleware. The route uses express.raw() because parsing and reserializing JSON changes the signed bytes.
Create the corresponding event table:
recordEventAndEnqueue represents your local transactional status update and durable fulfillment-job insert from Safe fulfillment. Implement it with your own order schema. It must use the supplied database client so event storage and job creation commit together.

Timestamp policy

The receiver must reject a timestamp more than five minutes in the past or future relative to its own UTC clock. This limits replay exposure while allowing ordinary network delay and clock skew.
  • Synchronize production hosts with a reliable time service.
  • Perform the timestamp check before HMAC work and JSON parsing.
  • Do not use createdAt for signature freshness; use Skinloop-Timestamp.
  • Do not treat the freshness check as deduplication. An attacker can replay a valid request within five minutes, so the event-ID unique constraint remains required.

Endpoint secrets

Each webhook endpoint has its own signing secret beginning with whsec_. Skinloop displays the full secret only when the endpoint is created or its secret is rotated. Store it as a server-side secret:
Never put either value in browser code, source control, logs, support messages, or the event database. A secret belongs only to the endpoint for which it was issued; do not reuse it between staging and production.

Rotate without dropping events

The direct Rotate secret action replaces an endpoint’s secret immediately and returns the new value once. Because the new value cannot be deployed before it is created, use two endpoint records for a planned zero-downtime rotation:
  1. Create a second webhook endpoint with the same HTTPS URL and event subscriptions. Securely capture its new secret.
  2. Deploy the new secret as CURRENT and the old endpoint’s secret as PREVIOUS.
  3. Trigger a webhook test for the new endpoint and confirm it verifies.
  4. Revoke the old endpoint in Skinloop.
  5. Keep the old secret as PREVIOUS for one hour to cover requests already in flight.
  6. Remove PREVIOUS and test the remaining endpoint again.
While both endpoints are active, the same event can be delivered through each endpoint. The event-ID unique constraint makes that overlap safe. For an emergency response to suspected exposure, use Rotate secret immediately, update CURRENT as quickly as possible, and remove the compromised secret rather than keeping an overlap. A short rejection window is safer than accepting a known-compromised secret. The receiver checks both configured secrets during planned overlap. Do not keep old secrets indefinitely.

Response contract

Return 2xx only after durable storage and any required fulfillment job commit. Do not return 2xx first and continue important work only in process memory. Next: test retries and accepted duplicates in Webhook retries and deduplication.