Webhooks
Send every click, app open, install and conversion to your backend as it happens, signed and retried.
Webhooks let Lander POST events to your server: a click on a short link, an app open from a link, an install, or a conversion. Every request is signed, retried with backoff and listed in the dashboard so you can see exactly what was sent.
Open Settings (the gear at the bottom of the navigation rail) and choose Webhooks.
Growth and Scale
Webhooks need the Growth plan or higher. On Starter the page says so instead of listing endpoints. If you move down from Growth, your endpoints are kept but nothing is sent until you move back up (events from the meantime are not sent later), and you can still edit, turn off or delete them. See Features by plan.
Who can manage webhooks
Owners, admins and developers can add, edit and test endpoints and see deliveries. Marketers see a note asking them to contact one of those roles.
Endpoints belong to one project and one mode
Each endpoint receives events from the project and mode (test or live) you were in when you added it. The page lists only the endpoints for the current mode, so switch between Test and Live in the header to manage the other set. Each endpoint gets its own signing secret.
An endpoint receives events that happen after you add it. Past events are not replayed.
Add an endpoint
Open the dialog
Select Add endpoint.
Fill in the details
- URL: where Lander should POST, for example
https://api.example.com/webhooks/lander. It must usehttps. Plainhttponly works forlocalhost,127.0.0.1and[::1], so you can test locally. - Description (optional): a note for your team, up to 200 characters, for example "Attribution service".
- Events: pick at least one. All four are selected by default.
| Event | Sent when |
|---|---|
| Click | Someone tapped a link (bots excluded). |
| Open | The app opened from a link. |
| Install | First launch after an install or reinstall. |
| Conversion | A custom event from Lander.trackEvent. |
Send a test and copy the secret
Select Add endpoint to save. Then use Send test on the endpoint's row to check that your server answers, and open Signing secret from the row's menu to copy the secret into your server's configuration.
Manage endpoints
Each row shows the URL, the subscribed events and a status:
| Status | Meaning |
|---|---|
| Active | Working. Shows when the last delivery succeeded, or "No deliveries yet". |
| Failing | The most recent attempts failed. Shows how many failed in a row and when the streak started. |
| Off | Turned off by someone on your team, or automatically after a sustained failure (the reason is shown). |
Next to the status, Send test sends a test event right away. The menu (More actions) has:
- Signing secret: reveal, copy or rotate the secret.
- Edit: change the URL, description or events.
- Turn off / Turn on: pause or resume the endpoint.
- Delete: the endpoint stops receiving events and its delivery history is removed.
Recent deliveries
Below your endpoints, Recent deliveries lists the last 50 deliveries across the mode's endpoints, with Event, Status, Endpoint, Attempts and When. Select Refresh to load new ones.
| Status | Meaning |
|---|---|
| Queued | Waiting for its first attempt. |
| Sending | Being sent right now. |
| Delivered | Your endpoint answered with a 2xx. |
| Retrying | An attempt failed. Shows when the next try is due. |
| Failed | Every automatic retry failed. |
Select a row to see the full delivery: status, attempts, duration, the last attempt, any error, the Request body Lander sent and the Response your server returned. Retry sends a failed delivery again immediately, and Resend does the same for a delivered one.
Delivery history is kept for 30 days.
Events and payloads
type | Sent when | Source |
|---|---|---|
click | Someone opened a short link. Bot clicks are never sent. | Edge redirector |
open | The app was opened from a link. | SDK |
install | First launch after an install or reinstall. | SDK |
conversion | A custom event (Lander.trackEvent). | SDK |
test | Someone pressed Send test in the dashboard. | Dashboard |
Events go out within about a minute. Order is not guaranteed, and the same event can arrive more than once (after a timeout, for example), so deduplicate on the webhook-id header.
Request
POST /webhooks/lander HTTP/1.1
Content-Type: application/json
User-Agent: Lander-Webhooks/1.0 (+https://golander.link/docs/dashboard/webhooks)
webhook-id: 7163a571-9dfc-4bc7-9fb1-536d1c095436
webhook-timestamp: 1792069000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
lander-event: install| Header | Value |
|---|---|
webhook-id | The delivery ID. It stays the same across retries, so use it to skip duplicates. |
webhook-timestamp | Unix seconds when this attempt was signed. |
webhook-signature | v1, followed by the base64 HMAC SHA-256 signature. See Verify signatures. |
lander-event | The event type, the same as type in the body. |
Envelope
Every body has the same shape. id is the event itself (the click or app event), createdAt is when Lander recorded it, and data depends on type. Timestamps are ISO 8601 in UTC.
{
"id": "cc380bf8-52b4-521f-8d18-f596831c1baa",
"type": "install",
"createdAt": "2026-10-15T12:55:57.258Z",
"projectId": "33333333-3333-3333-3333-333333333333",
"env": "test",
"data": {}
}{
"linkId": "84168627-0d2c-4dbe-9883-d894300ee2f8",
"url": "https://acme.golander.link/summer-sale",
"domain": "acme.golander.link",
"slug": "summer-sale",
"campaign": "summer_2026",
"channel": "instagram",
"platform": "ios",
"country": "DE",
"referrerHost": "instagram.com",
"clickedAt": "2026-10-15T12:48:06.555Z"
}platform is ios, android or desktop. country and referrerHost can be null. Lander never stores or sends the IP address, the full user agent or the full referrer.
Verify signatures
Requests follow the Standard Webhooks spec, so any of its libraries work. Always verify against the raw request body, before you parse it.
- Read the
webhook-id,webhook-timestampandwebhook-signatureheaders. - Reject the request if the timestamp is more than 5 minutes away from your clock. This protects you from replayed requests.
- Take your secret, drop the
whsec_prefix and base64 decode the rest. That is the HMAC key. - Compute
base64(HMAC_SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")). webhook-signatureis a space separated list ofv1,<signature>entries. Accept the request if anyv1entry matches, using a constant time comparison.
import express from 'express'
import { Webhook } from 'standardwebhooks'
const app = express()
const wh = new Webhook(process.env.LANDER_WEBHOOK_SECRET!) // whsec_...
app.post('/webhooks/lander', express.raw({ type: 'application/json' }), (req, res) => {
let event: { id: string; type: string; data: unknown }
try {
// Checks the signature and the timestamp (5 minute tolerance).
event = wh.verify(req.body, req.headers as Record<string, string>) as typeof event
} catch {
return res.sendStatus(401)
}
// Skip duplicates with req.headers['webhook-id'], then handle event.type and event.data.
res.sendStatus(204)
})Rotate the secret
Open Signing secret from the endpoint's menu, select Rotate secret, then Rotate now. The new secret is shown right away and the old one stops verifying immediately, so deploy the new secret to your server straight after rotating.
Keep the signing secret on your server. Never ship it in your Flutter app.
Responses, retries and automatic turn off
- Answer with any
2xxwithin 10 seconds. Do slow work after you respond. - Anything else is a failed attempt: another status code, a timeout, a connection error, or a redirect.
- Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. That is seven attempts over roughly 21 hours, after which the delivery is marked Failed.
- An endpoint is turned off only after a sustained failure: at least 20 failed attempts in a row across all its deliveries, with the first of them more than 24 hours ago. Any successful attempt ends the streak, so a short outage (a deploy, a restart) never turns an endpoint off. Every delivery that failed during the outage still gets all of its retries.
- While an endpoint is off, new events are not queued for it. Deliveries that were already waiting resume when you select Turn on, which also resets the failure count.
Try it locally
Plain http URLs work for localhost, so you can point an endpoint at a server on your machine. Add http://localhost:8809/webhooks/lander (or any port), press Send test, then tap one of your test links and watch the delivery appear in Recent deliveries within a minute.