Blog
Webhooks for link events: signatures, retries and the endpoint you need
The eight events gc.mw sends, the exact signature scheme, the nine-attempt retry schedule, and the receiver code that verifies a delivery correctly in a dozen lines.
By Wasim Idrishi · Updated · 5 min read
A webhook is the only part of an integration where a mistake is silent. A wrong signature check does not throw; it just accepts everything, including the request someone crafted after reading your docs.
So start with the signature.
The signature scheme
Every delivery carries three headers:
X-GC-Signature: t=1757030400,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-GC-Event: link.created
X-GC-Delivery: 66d9f0c1a2b3c4d5e6f70819
v1 is HMAC-SHA256 (RFC 2104) over the string formed by the
timestamp, a full stop, and the raw request body, keyed with your webhook secret. The timestamp is inside
the signed string, which is what makes a captured request unreplayable once your tolerance window passes.
The verification, in the order that matters:
Read the raw body before anything parses it
Signature is computed over bytes. If your framework parses JSON and you re-serialise it, key order or whitespace will differ and every signature will fail. Capture the raw body first.
Parse the header into t and its v1 values
The format allows more than one v1. Accept the delivery if any of them matches, so your verifier does not depend on there being exactly one.
Reject a stale timestamp
Five minutes either side is the tolerance we implement and recommend. Do this before the comparison, so a replayed request never reaches the expensive part.
Compare in constant time
Not with string equality. Use your language's timing-safe comparison; ours is crypto.timingSafeEqual.
Return 2xx quickly, then do the work
Anything outside 200 to 299 is a failure and will be retried. Acknowledge first, process asynchronously.
The eight events
| Event | Fires when |
|---|---|
link.created | A link is created, from any source |
link.updated | A link changes; the payload lists which fields |
link.deleted | A link is deleted |
link.claimed | An anonymous link is claimed into a workspace |
link.expired | A link passes its expiry |
link.blocked | A link is blocked by safety review, with a reason |
domain.active | A custom domain finishes verification |
report.created | An abuse report is filed against one of your links |
There is no click event, and there will not be one. A link that gets a hundred thousand clicks in an hour would send a hundred thousand requests at your server, and your server would be the thing that breaks. Read traffic from the analytics endpoint on a schedule.
Every body is the same envelope: an id matching the X-GC-Delivery header, a type, a created_at, a
workspace_id, and a data object carrying the event payload. The envelope is byte-identical across
retries — only the signature timestamp changes — so deduplicating on id is safe and is what you should do.
The retry schedule
Nine attempts: the first, then retries after 30 seconds, 2 minutes, 10 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours. Just over a day and a half of patience in total.
That shape assumes the common failures. A deploy that takes a minute is covered by the first two retries. A database outage of an hour is covered. A DNS mistake nobody noticed until the morning is covered by the tail. After the last attempt the delivery is marked failed and stays in the delivery log with its status code and the first few kilobytes of your response body, which is usually enough to see what your server said.
What we refuse to deliver to
Webhook URLs are checked harder than link destinations, because a webhook is a request we make on your behalf from inside our network.
The URL must be https. It cannot carry credentials. The hostname cannot be a local name. Every address it
resolves to must be public — a hostname answering with even one private address is refused, which closes the
split-horizon trick. And the address that passed the check is the address the delivery connects to, so DNS
cannot change between the check and the connection.
Each delivery is bounded at eight seconds end to end, and at most a few kilobytes of your response is read and logged.
Link Checker
Open the full toolSecrets and rotation
The secret is generated with 256 bits of entropy, prefixed whsec_ so it is recognisable in your config,
and shown once. At rest it is encrypted with AES-256-GCM; the interface will only ever show you the last
four characters.
Rotation is immediate: the new secret replaces the old one on the webhook, and the next delivery is signed with the new one. There is no overlap window, so the order matters — put the new secret into your receiver's configuration and deploy before you rotate, not after.
The header format allows several v1 values in one signature, and your verifier should loop over all of
them rather than reading the first. Today we always send exactly one, so a verifier that checks only the
first will work; it will also be the thing that breaks quietly if that ever changes. Looping costs one line.
Ten webhooks per workspace. Management is session-only for a workspace admin, and webhooks are a Business feature. Everything they report on is an ordinary branded link, so a webhook fires the same way whether the link was made in the dashboard, through the API, or in a bulk import.
Questions people ask
Do webhooks fire on every click?
No, and that is deliberate. Events are lifecycle changes to your links and domains, not traffic. A click webhook on a link that goes viral is a denial of service against your own endpoint; read clicks from the analytics endpoint or the CSV export instead.
How do I verify a delivery is really from you?
Compute HMAC-SHA256 over the timestamp, a full stop, and the exact raw request body, using your webhook secret as the key. Compare it to the v1 value in the X-GC-Signature header with a constant-time comparison, and reject anything whose timestamp is more than five minutes from now.
How many times will a failed delivery be retried?
Nine attempts in total: the first, then eight retries after 30 seconds, 2 minutes, 10 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours. After the last one the delivery is marked failed and stays in the log for you to inspect.
Can I use an http endpoint or a local address?
No. Webhook URLs must be https, must not contain credentials, and every address the hostname resolves to must be public. A hostname that resolves to even one private address is refused, which is what stops a webhook being used to probe an internal network.
Can an API key manage webhooks?
No. Webhook creation, rotation and deletion are session-only operations for a workspace admin. A leaked API key cannot repoint your deliveries at someone else's server.
Sources
- RFC 2104 — HMAC: Keyed-Hashing for Message Authentication — checked 5 September 2026
Keep going
Try it with your own link
Paste a long link and get a 10-character gc.mw link with a QR code. No account needed for 30 days; free forever with one.