Live

Webhooks

Signed webhook delivery, event payloads, verification headers, and replay semantics.

Webhooks

Nimriz webhooks deliver signed lifecycle events to workspace-scoped HTTPS endpoints for plans with webhook access.

Event catalog

Business subscriptions in v1:

  • link.created
  • link.updated
  • link.takedown_updated
  • domain.verification_updated

Operator-only synthetic event:

  • nim.webhook.test

Delivery semantics

  • Delivery is asynchronous and at-least-once.
  • Consumers must tolerate duplicates and out-of-order arrival.
  • Nimriz preserves the same stable event id on manual replay.
  • Test sends use the same signed delivery pipeline as live events, but they are not part of the normal business subscription catalog.

Signed request headers

Every POST includes:

  • X-Nim-Event-Id
  • X-Nim-Event-Type
  • X-Nim-Timestamp
  • X-Nim-Signature
  • X-Nim-Delivery-Attempt
  • X-Nim-Delivery-Reason

The signature input is:

${X-Nim-Timestamp}.${raw_request_body}

The signature format is:

v1=<hex hmac sha256>

Envelope shape

{
  "id": "9f4d5dbd-c8b8-4c89-a080-b4f70fce8f53",
  "type": "link.updated",
  "api_version": "2026-03-30",
  "created_at": "2026-03-30T12:34:56.789Z",
  "organization_id": "org_uuid",
  "workspace_id": "workspace_uuid",
  "data": {
    "link_id": "link_uuid",
    "domain_id": "domain_uuid",
    "event_action": "destination_updated",
    "changed_fields": ["destination"],
    "before": {
      "destination_host": "example.com",
      "destination_url_capped": "https://example.com/path"
    },
    "after": {
      "destination_host": "example.org",
      "destination_url_capped": "https://example.org/new-path"
    }
  }
}

Safe payload posture

  • Nimriz sends safe destination fields such as destination_host and destination_url_capped.
  • Raw query-bearing long_url values are not included in webhook payloads.
  • Signing secrets, passwords, cookies, and other sensitive credentials never appear in emitted webhook bodies.

Verification example

import crypto from 'node:crypto';

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 ?? ''));
}

Example 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"
}

link.takedown_updated

{
  "link_id": "link_uuid",
  "domain_id": "domain_uuid",
  "short_code": "launch24",
  "before_status": "active",
  "after_status": "disabled"
}

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
  }
}

Operator tools

Use the dashboard integrations screen to:

  • create and edit endpoints
  • rotate signing secrets
  • queue a test delivery
  • inspect delivery history
  • replay recent deliveries

Build downstream consumers around the stable event id, not the delivery attempt number.