Webhooks
Manage endpoints, verify signed deliveries, handle retries, replay events, and full event schemas.
Prerequisites
- A Nimriz plan with webhook access.
- A secure HTTPS endpoint on your server that can receive JSON POST requests.
- A mechanism in your backend to verify HMAC-SHA256 signatures.
What webhooks deliver
Nimriz webhooks send signed HTTP POST requests to your endpoint whenever specific business events occur in your workspace. Each delivery contains a structured JSON payload describing the event.
Webhooks v1 covers lifecycle events—changes to links and domains. They are not per-click event streams.
Looking for click or conversion event streaming? If you need to forward click-level or conversion data to external platforms, see our Integrations overview, which includes high-volume click webhooks and destination-specific delivery options.
Event catalog
| Event type | When it is emitted |
|---|---|
link.created | A new short link was successfully created in your workspace. |
link.updated | A link was mutated (destination change, slug change, expiration change, password change, routing rule change). |
link.takedown_updated | A moderation or safety workflow changed a link's availability (e.g., active → disabled, or a restore). |
domain.verification_updated | A domain's verification or readiness state changed. |
Managing webhook endpoints
Adding an endpoint
- Go to Settings → Integrations → Webhooks.
- Click Add endpoint.
- Enter your endpoint URL (must be HTTPS).
- Select which event types you want to receive.
- Click Save.
Nimriz generates a unique signing secret for the endpoint. Copy and store it securely-it is shown only once. You need it to verify incoming requests.
Editing an endpoint
Click the endpoint name to open its settings. You can:
- Update the endpoint URL.
- Change which event types are subscribed.
- Enable or disable the endpoint without deleting it.
Disabling and re-enabling
Disabling an endpoint pauses delivery without deleting the endpoint configuration or its delivery history. Nimriz will not attempt delivery to a disabled endpoint. Re-enable it to resume delivery.
Deleting an endpoint
Deleting an endpoint permanently removes it and stops all future deliveries. Delivery history for that endpoint remains visible until it expires.
Rotating the signing secret
If your signing secret is compromised:
- Open the endpoint settings.
- Click Rotate secret.
- A new secret is generated. Copy it immediately.
- Update your backend to use the new secret.
During rotation: The old secret becomes invalid immediately. Update your backend before rotating, or accept a brief gap in signature verification during the switchover.
Event envelope
Every payload uses a consistent envelope structure:
{
"id": "9f4d5dbd-c8b8-4c89-a080-b4f70fce8f53",
"type": "link.updated",
"api_version": "2026-03-30",
"created_at": "2026-03-17T12:34:56.789Z",
"organization_id": "org-uuid",
"workspace_id": "workspace-uuid",
"data": {
"...event-specific payload..."
}
}
| Field | Description |
|---|---|
id | Stable event ID. The same id is used across all delivery attempts for the same event, and on replay. Use this for deduplication. |
type | The event type string (e.g., link.updated). |
api_version | Date-based contract marker. Payload interpretation changes are released under a new api_version. |
created_at | When the event was created-not when it was delivered. |
organization_id | Your org's UUID (when known). |
workspace_id | Your workspace's UUID. |
data | Event-specific payload. Uses stable resource IDs (link_id, domain_id). |
Event-specific payloads
link.created
{
"link_id": "link-uuid",
"domain_id": "domain-uuid",
"domain_name": "go.example.com",
"short_code": "launch24",
"short_url": "https://go.example.com/launch24",
"status": "active",
"redirect_status_code": 302,
"expires_at": null,
"password_protected": false,
"destination_host": "example.com",
"destination_url_capped": "https://example.com/landing",
"source": "dashboard",
"actor": {
"user_id": "user-uuid"
}
}
link.updated
{
"link_id": "link-uuid",
"domain_id": "domain-uuid",
"short_code": "launch24",
"event_action": "destination_updated",
"changed_fields": ["destination"],
"before": {
"destination_host": "example.com",
"destination_url_capped": "https://example.com/landing"
},
"after": {
"destination_host": "example.org",
"destination_url_capped": "https://example.org/new-path"
},
"source": "api",
"actor": {
"user_id": "user-uuid"
}
}
The changed_fields array lists which fields changed. The before and after objects contain safe field snapshots for those changes.
Note: Full raw destination URLs (including query strings) are intentionally omitted from webhook payloads. Only destination_host and a capped version of the URL are included to prevent leaking query-sensitive or signed URL parameters.
link.takedown_updated
{
"link_id": "link-uuid",
"domain_id": "domain-uuid",
"short_code": "launch24",
"before_status": "active",
"after_status": "disabled",
"source": "admin"
}
domain.verification_updated
{
"domain_id": "domain-uuid",
"domain_name": "go.example.com",
"before": {
"is_verified": false,
"verification_status": "pending",
"ready_for_traffic": false
},
"after": {
"is_verified": true,
"verification_status": "verified",
"ready_for_traffic": true
},
"source": "dashboard",
"actor": {
"user_id": "user-uuid"
}
}
Signed request headers
Every POST delivery includes these headers:
| Header | Description |
|---|---|
X-Nim-Event-Id | The stable event ID (same as id in the envelope). |
X-Nim-Event-Type | The event type string. |
X-Nim-Timestamp | Delivery timestamp in Unix seconds. |
X-Nim-Signature | v1=<hex hmac sha256>. |
X-Nim-Delivery-Attempt | Attempt number for this endpoint and event. |
X-Nim-Delivery-Reason | live, replay, or test. |
Verifying signatures
Always verify the signature before processing a webhook payload. Sign the raw request body bytes (not re-serialized JSON) against your endpoint's signing secret.
import crypto from 'node:crypto';
export function verifyNimWebhook({ secret, timestamp, rawBody, signatureHeader }) {
const expected = `v1=${crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader ?? '')
);
}
// Usage (Express example)
app.post('/webhooks/nimriz', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyNimWebhook({
secret: process.env.NIMRIZ_WEBHOOK_SECRET,
timestamp: req.headers['x-nim-timestamp'],
rawBody: req.body.toString('utf-8'), // must be raw bytes, not parsed JSON
signatureHeader: req.headers['x-nim-signature'],
});
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// process event...
res.status(200).send('OK');
});
Critical: Parse the body as raw bytes first, verify the signature, then parse as JSON. If you let your framework parse the body as JSON first, the byte-level content may differ from what Nimriz signed.
Delivery semantics
At-least-once delivery
Nimriz delivers each event at least once. Your endpoint may receive the same event more than once (due to retries or replays). Your processing must be idempotent-handle duplicates by checking the event id.
Retry schedule
If your endpoint does not return a 2xx response, Nimriz retries:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 60 seconds |
| 3 | 120 seconds |
| 4 | 240 seconds |
| 5 | 480 seconds |
| 6 | 900 seconds |
After 6 attempts, the delivery moves to dead-letter state. It will not retry further unless you manually replay it.
Retryable outcomes: Network failures, no response, HTTP 408, 409, 425, 429, and all 5xx responses.
Non-retryable outcomes: Any other non-2xx response (e.g., 400, 401, 403, 404) causes immediate terminal failure for that attempt.
Event ordering
Nimriz does not guarantee global delivery order. Two events for the same link may arrive in any order. Build your receiver to handle out-of-order events-use the created_at timestamp and the before/after field snapshots to reason about state transitions correctly.
Test delivery
Send a test event to your endpoint without triggering a real product event:
- Go to Settings → Integrations → Webhooks.
- Open the endpoint.
- Click Send test event.
A nim.webhook.test event with a synthetic payload is sent to your endpoint. The X-Nim-Delivery-Reason header will be test. This is useful for verifying your endpoint is reachable and your signature verification is working.
Delivery history and replay
Viewing delivery history
Each endpoint has a delivery history view showing recent delivery attempts. For each attempt, you can see:
- Event type and event ID.
- Delivery timestamp.
- HTTP response code from your server.
- Whether it was a live delivery, replay, or test.
- The error message if delivery failed.
Replaying an event
If a delivery failed and you need to reprocess it:
- Find the failed event in the endpoint's delivery history.
- Click Replay.
Nimriz re-delivers the original event envelope (same id, type, data) with a fresh timestamp and signature. The X-Nim-Delivery-Reason header will be replay.
Important: A replay is a duplicate delivery from your server's perspective. Your processing must be idempotent. Deduplication is based on the event id.
Troubleshooting
Signature verification is failing
- Confirm you are reading the raw request body bytes before verifying, not after JSON parsing.
- Confirm you are using the correct signing secret for this endpoint. Each endpoint has its own secret.
- If you recently rotated the secret, ensure your backend is using the new secret.
- Confirm
X-Nim-Timestampis the correct value in your signature input string:${X-Nim-Timestamp}.${rawBody}.
Receiving duplicate events
This is expected. Implement idempotent processing using the event id as the deduplication key. Check whether you have already processed an event with that id before executing any side effects.
Events arriving out of order
Do not rely on arrival order. Use the created_at timestamp in the envelope to determine the correct chronological order. For state-change events (link.updated), use the before and after field snapshots instead of tracking state on your side.
Endpoint is failing and not retrying
Check the delivery history for the specific error. If your endpoint is returning a non-retryable status code (e.g., 400, 404), Nimriz stops retrying immediately. Fix the issue and use Replay to reprocess the failed delivery.
All deliveries are in dead-letter state
Your endpoint was unreachable or consistently failing across all 6 retry attempts. Fix your endpoint, re-enable it if it was disabled automatically, then use the Replay action on the failed events.
The delivery history is missing older events
Delivery history is retained for a limited period. Very old delivery attempts may have expired and be no longer visible.