Security
Verify webhook signatures and handle retries
Every webhook from iwoca includes a cryptographic signature so you can verify it's genuine before processing. Signatures use a Hash-based Message Authentication Code (HMAC), which proves both that the payload is unchanged and that it came from someone holding your secret token.
Signature verification
Each webhook includes two headers:
| Header | Purpose |
|---|---|
X-IW-Signature | HMAC-SHA256 signature of the request |
X-IW-Timestamp | Unix timestamp when the webhook was sent |
The signature is computed as:
HMAC-SHA256(secret_token, timestamp + "." + request_body)
The result is base64-encoded and prefixed with sha256=, so the header looks like sha256=YoSHTwbVZnuLY8kPusTeHoqcyQ9g1IeVHKpIFLJjp4k=.
A small number of older integrations are still on SHA-1, where the signature is sha1= + base64 of HMAC-SHA1(secret_token, request_body) – over the body alone, with no timestamp. GET /webhooks/configuration/ returns an encryption_method of sha256 or sha1 so you can check which applies to you.
Everything below assumes sha256; if you are on sha1, talk to your Partnership Manager about moving across.
Code examples
import base64import hmacimport hashlibimport timedef verify_webhook(request_body: bytes, signature: str, timestamp: str, secret: str) -> bool:# Reject old webhooks (replay protection)if abs(time.time() - int(timestamp)) > 300:return Falsedigest = hmac.new(secret.encode(),f"{timestamp}.".encode() + request_body,hashlib.sha256).digest()# The header is base64-encoded and prefixed with "sha256=" —# not a hex digest.expected = "sha256=" + base64.b64encode(digest).decode()return hmac.compare_digest(expected, signature)
Replay protection
Use X-IW-Timestamp to reject stale webhooks.
We recommend rejecting any webhook where the timestamp is more than five minutes old.
This prevents replay attacks where a captured webhook is resent later.
Retry behaviour
A delivery counts as successful when your endpoint responds with a status below 400. Anything else – a 4xx, a 5xx, a connection error, or a timeout – is a failed attempt and is retried.
By default iwoca retries up to 10 times after the first attempt, so 11 attempts in all, spread over roughly 16 hours:
| Attempt | Sent after the previous attempt |
|---|---|
| 1 | – (immediately after the event) |
| 2 | 10 seconds |
| 3 | 1 minute |
| 4 | 2 minutes |
| 5 | 5 minutes |
| 6 | 15 minutes |
| 7 | 30 minutes |
| 8 | 1 hour |
| 9 | 2 hours |
| 10 | 4 hours |
| 11 | 8 hours |
A few seconds of random jitter is added to each gap, so treat the times as approximate. Each attempt waits up to 10 seconds for your response before being treated as a timeout.
These are the default timings. The schedule is configurable per partner. A custom one changes both the intervals and the number of attempts, so confirm yours with your Partnership Manager if your integration depends on either.
What each retry looks like
X-IW-Event-IDstays the same across every attempt of the same event – use it as your idempotency key.X-IW-TimestampandX-IW-Signatureare regenerated on every attempt, because the timestamp is part of the signed payload.
Because each attempt carries a fresh timestamp, your signature verification must use the timestamp from the request in hand – never a cached value from an earlier attempt.
Point your subscription at the final URL, not a redirect.
Redirects are followed, but a 301, 302 or 303 is followed as a bodyless GET.
Your handler still receives the X-IW-* headers, but the event payload is gone.
The redirect target's 2xx makes the delivery look successful to iwoca, so it is never retried.
307 and 308 do replay the full POST.
If the redirect crosses to another host, the signature headers are forwarded to that host as well.
Register the exact HTTPS URL you want to receive events on.
After the last attempt
If all attempts fail, iwoca raises an internal alert and the delivery stops. Your subscription stays active – it is not disabled automatically, and events after that point are still delivered normally.
Nothing is re-sent to you automatically.
If your endpoint has been down, reconcile by calling the relevant GET endpoints — your Partnership Manager can arrange a manual redelivery if you need one.
A manual redelivery arrives as a separate event with its own X-IW-Event-ID, so it will not match the ID of the original attempt.
If you need to detect that specific case, compare the payload fields as well as the event ID.
Test webhooks sent from the Developer Portal are attempted once and never retried, so you will not see retry behaviour while debugging there.
Idempotency
The same event may be delivered more than once.
Your webhook handler should be idempotent – processing the same event twice should not cause issues.
Use the X-IW-Event-ID header (a unique ID per webhook) as your idempotency key; fall back to the combination of event_type, timestamp, and the data fields if needed.
IP allowlisting
If you need IP-based filtering, contact your Partnership Manager for iwoca's webhook IP addresses.