Skip to main content

Signature Verification

Every PRESS delivery is signed with HMAC-SHA256. You must verify this on every incoming request.

How the Signature is Computed

signature = HMAC_SHA256(your_signing_key, X-Webhook-Timestamp + "." + raw_request_body)
The result is sent in the X-Webhook-Signature header.

Verification Steps

1

Read the raw body

Read the request body exactly as received. Do not parse or re-serialise before verification — transforming the body will produce a signature mismatch.
2

Read the timestamp

Get the X-Webhook-Timestamp header value.
3

Check replay window

Reject the request if the timestamp is more than 5 minutes from your current time. This prevents replay attacks.
4

Compute the signature

Concatenate timestamp + "." + raw_body and compute HMAC-SHA256 using your signing key.
5

Compare using constant-time comparison

Compare the computed signature against the X-Webhook-Signature header. Use a constant-time comparison function — do not use == as it is vulnerable to timing attacks.
6

Accept or reject

Match → process the event. No match → return 401.

Code Examples

import hmac, hashlib, time

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    message = f"{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(
        secret.encode("utf-8"), message, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Response Codes

Your endpoint must respond with an appropriate HTTP status code within 500 milliseconds.
Your ResponseMeaningWhat PRESS Does
200 / 2xxEvent acceptedDone. No retry.
401 / 403Bad signature or unauthorisedPermanent failure. Not retried.
400Bad requestPermanent failure. Not retried.
3xx (redirects)RedirectTreated as failure. Redirects are not followed.
5xxServer errorTemporary failure. Retried.
Timeout (>500ms)No responseTemporary failure. Retried.

Retry Schedule

PRESS retries failed deliveries (5xx or timeout) with exponential backoff:
AttemptDelay After Previous FailureCumulative Elapsed
1Immediate0
21 minute~1 minute
35 minutes~6 minutes
415 minutes~21 minutes
51 hour~1 hour 21 minutes
66 hours~7 hours 21 minutes
After 6 failed attempts the delivery is marked as permanently failed. Contact ICP support for manual replay.

Idempotency

PRESS uses at-least-once delivery. The same event may arrive more than once (due to retries or network issues). The X-Webhook-Id header (and the id field in the payload) is your idempotency key. It is stable across retries.
1

Store processed IDs

Keep processed X-Webhook-Id values for at least 7 days.
2

Check before processing

If the ID is already in your store, skip business logic.
3

Return 200 regardless

Always return 200 for a valid, signed request — even if it is a duplicate.
X-Webhook-Delivery-Attempt tells you the attempt number. 1 = first delivery, 2+ = retry.
1. Receive POST
2. Read raw body (do NOT parse yet)
3. Verify HMAC signature → invalid? return 401
4. Validate timestamp within 5 minutes → stale? return 401
5. Parse JSON
6. Check X-Webhook-Id → already seen? return 200, skip processing
7. Store event durably (database or queue)
8. Return 200
9. Process business logic async (background worker)
Keep your handler thin. Accept, verify, store, acknowledge. Everything else happens in the background.

Event Ordering

PRESS does not guarantee global ordering of events. If ordering matters for your use case:
  • Compare event_ts values before applying state changes
  • Design idempotent handlers that tolerate out-of-order delivery
  • Use uaekyc_id as the entity key with last-writer-wins logic