Webhooks
What we send when something happens in a workspace, how to check it came from us, and what happens when your endpoint does not answer.
A webhook sends an HTTPS POST to a URL of yours when something happens in a
workspace: a link is created, edited or deleted, a custom domain is verified, a member joins.
Use it to post new links to a chat, keep a spreadsheet or a CRM in step, or start your own
processing without polling the API.
Endpoints are added in the app, under Webhooks on the workspace's Developers page, by the owner or an admin. Each endpoint picks the events it wants and gets its own signing secret. The help article walks through it.
Plans
Endpoints per workspace, read live from our plan catalog:
| Plan | Webhook endpoints |
|---|---|
| Free | None |
| Pro | None |
| Team | 10 |
| Business | 25 |
After a downgrade, endpoints stay listed and can be edited or removed, but nothing is sent to them until the plan includes webhooks again.
The request
Urlicer-Event: the event type, the same astypein the body.Urlicer-Delivery: the event id, the same asidin the body. It stays the same on every retry and every resend, so use it to ignore an event you have already handled.Urlicer-Signature: see verifying a request.
Every body has the same envelope, with the event in data:
workspace.id is the key in the workspace's app URLs. createdAt is
when the event happened, not when this attempt was made.
Events
link.created
A link was created in the app or through the API. link has the fields
the API returns for a link, without clicks, plus
id, our number for the link. Times are unix seconds.
link.updated
A link was edited. link is its state after the edit, and changed
names the fields of link that differ from before. It can be empty, when the edit
only touched settings that are not in the link object, such as targeting rules.
link.deleted
link is the link as it was just before it was deleted.
links.bulk_created
A bulk create, in the app or through the bulk endpoint. The links come
in events of up to 100, each with batch saying which part of the whole it is, so
a 250-link import is three events. A bulk create does not also send one
link.created per link.
domain.verified
A custom domain passed its DNS ownership check.
member.joined
Someone became a member: via is invitation when they accepted one,
added when an admin added an existing account.
webhook.test
Sent only by the Test button, to that one endpoint, whatever events it takes.
data holds a message. A test is tried once and never retried.
More event types will be added. An endpoint set to all events receives new ones as they arrive, so ignore types you do not handle rather than rejecting them.
Verifying a request
Urlicer-Signature looks like t=1790769600,v1=3b1f0c…. t
is the unix time the attempt was sent. v1 is the hex HMAC-SHA256 of
t, a dot and the raw request body, keyed with the endpoint's secret. Compute
it yourself and compare in constant time. Check that t is recent, five minutes
is a good limit, so a captured request cannot be replayed later. Sign the body exactly as
received: parsing the JSON and encoding it again changes the bytes.
# $SIGNATURE is the Urlicer-Signature header, body.json the raw request body.
t=$(printf '%s' "$SIGNATURE" | sed -E 's/^t=([0-9]+),v1=.*/\1/')
v1=$(printf '%s' "$SIGNATURE" | sed -E 's/.*,v1=([0-9a-f]+)$/\1/')
expected=$( { printf '%s.' "$t"; cat body.json; } \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.*= //')
[ "$expected" = "$v1" ] && echo valid || echo invalid
The secret is shown in the app, where it can also be replaced. A replacement takes effect from the next attempt, retries included, so update your receiver as you replace it.
Responding
- Any
2xxwithin 10 seconds counts as delivered. Anything else is a failed attempt: another status, a timeout, a refused connection or a TLS error. - Redirects are not followed, so a
3xxis a failure too. Give us the final URL. - We read at most the first 1 KB of your answer, and keep it in the delivery log.
- Answer first and process afterwards. A slow receiver risks the 10-second limit, and a timed-out attempt is sent again even if you did handle it.
Retries
A failed attempt is tried again after 1 minute, then 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: seven attempts over about 21 hours. After the seventh it is marked failed, and can still be resent by hand from the delivery log.
Events are sent in parallel and retried on their own schedules, so they can arrive out of
order. When order matters, compare createdAt.
An endpoint is switched off when 20 events in a row have failed every attempt, or when it is still failing and nothing has been delivered for three days. The workspace owner gets an email. While it is off, nothing is sent to it and events that happen are not kept for later. Turning it back on in the settings starts the count again.
Endpoint requirements
- HTTPS on port 443, with a valid certificate.
- A public address: a host that resolves to a private or reserved network address is refused when you save it, and checked again before every attempt.
- No user name or password in the URL. Put a token in the path or query if you want one on top of the signature.
The delivery log in the settings keeps every attempt for 30 days.