Webhooks
Webhooks send HTTP POST requests to your server when events occur in your workspace - link created, domain verified, member removed, etc. Use them to build integrations, sync external systems, or trigger workflows.
Plan: Pro, Team, or Agency.
| Plan | Max webhooks |
|---|---|
| Pro | 3 |
| Team | 10 |
| Agency | 25 |
POST /workspaces/:wsId/webhooks
Create a new webhook endpoint.
Role: admin+
curl -X POST https://nobsredir.com/api/workspaces/ws_abc123/webhooks \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Slack Notifications", "url": "https://yourapp.com/webhooks/nobsredir", "events": ["link.created", "link.deleted"] }'Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Human-readable label for the webhook |
url | string | yes | HTTPS endpoint to receive events. Must not be a private/internal address. Max 2048 chars. |
events | string[] | yes | List of event types to subscribe to. See Event types. |
Response 201:
{ "id": "wh_abc123", "name": "Slack Notifications", "url": "https://yourapp.com/webhooks/nobsredir", "events": ["link.created", "link.deleted"], "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "active": true, "created_at": "2025-01-20T14:00:00.000Z"}The secret is only returned once at creation. Store it immediately - you need it to verify signatures.
Errors:
400- Invalid URL (not HTTPS, private IP, too long), invalid event type, or missing fields402- Webhook limit reached for your plan
GET /workspaces/:wsId/webhooks
List all webhooks for the workspace. The secret field is never included.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/webhooks \ -H "X-API-Key: nobs_your_key"Response 200:
{ "webhooks": [ { "id": "wh_abc123", "name": "Slack Notifications", "url": "https://yourapp.com/webhooks/nobsredir", "events": ["link.created", "link.deleted"], "active": true, "created_at": "2025-01-20T14:00:00.000Z", "updated_at": "2025-01-20T14:00:00.000Z", "created_by_email": "admin@example.com" } ]}GET /workspaces/:wsId/webhooks/:webhookId
Get a single webhook. The secret field is never included.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123 \ -H "X-API-Key: nobs_your_key"Response 200: Same shape as the list items above.
Errors: 404 - Webhook not found.
PATCH /workspaces/:wsId/webhooks/:webhookId
Update a webhook’s name, URL, subscribed events, or active status.
Role: admin+
curl -X PATCH https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123 \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "events": ["link.created", "link.updated", "link.deleted"], "active": false }'Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | Updated name |
url | string | no | Updated HTTPS URL (same validation as create) |
events | string[] | no | Updated event subscriptions |
active | boolean | no | Set false to pause delivery without deleting |
Response 200:
{"ok": true}Errors:
400- Invalid URL, invalid event type, or no updates provided404- Webhook not found
DELETE /workspaces/:wsId/webhooks/:webhookId
Delete a webhook. All delivery history is deleted with it.
Role: admin+
curl -X DELETE https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123 \ -H "X-API-Key: nobs_your_key"Response 200:
{"ok": true}Errors: 404 - Webhook not found.
POST /workspaces/:wsId/webhooks/:webhookId/rotate-secret
Generate a new signing secret. The old secret stops working immediately.
Role: admin+
curl -X POST https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123/rotate-secret \ -H "X-API-Key: nobs_your_key"Response 200:
{ "secret": "whsec_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4"}The new secret is only returned once. Update your webhook handler immediately after rotating.
Errors: 404 - Webhook not found.
POST /workspaces/:wsId/webhooks/:webhookId/test
Send a test webhook.test event to the endpoint. Unlike normal deliveries, this runs synchronously and returns the result.
Role: admin+
curl -X POST https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123/test \ -H "X-API-Key: nobs_your_key"Response 200:
{ "status_code": 200, "success": true, "duration_ms": 142, "error": null}If the endpoint is unreachable or returns a non-2xx status, success will be false and error will contain the reason.
Errors: 404 - Webhook not found.
GET /workspaces/:wsId/webhooks/:webhookId/deliveries
List recent delivery attempts for a webhook. Delivery logs are retained for 7 days.
Role: viewer+
curl "https://nobsredir.com/api/workspaces/ws_abc123/webhooks/wh_abc123/deliveries?page=1&limit=20" \ -H "X-API-Key: nobs_your_key"Query params:
| Param | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page (max 100) |
success | boolean | - | Filter by success status (true or false) |
Response 200:
{ "deliveries": [ { "id": 1, "event_type": "link.created", "status_code": 200, "duration_ms": 85, "success": true, "error": null, "created_at": "2025-01-20T14:05:00.000Z" }, { "id": 2, "event_type": "link.deleted", "status_code": 500, "duration_ms": 320, "success": false, "error": "HTTP 500", "created_at": "2025-01-20T14:10:00.000Z" } ], "total": 2, "page": 1, "limit": 20}Errors: 404 - Webhook not found.
Event types
Subscribe to any combination of these events:
| Event | Fires when |
|---|---|
link.created | A link is created (single or bulk) |
link.updated | A link’s target, slug, tags, or other fields are changed |
link.deleted | A link is deleted (single or bulk) |
link.expired | A link with an expires_at date is accessed after expiry (fires once) |
link.broken | A monitored link returns a 4xx/5xx status (Pro+ only, runs on cron) |
link.recovered | A previously broken link starts responding normally again |
domain.added | A custom domain is added to the workspace |
domain.verified | DNS verification succeeds for a custom domain |
domain.removed | A custom domain is deleted |
member.added | A member is added to the workspace |
member.removed | A member is removed from the workspace |
member.role_changed | A member’s role is updated |
webhook.test | The test endpoint is called (always delivered regardless of subscriptions) |
Payload format
Every webhook delivery is an HTTP POST with a JSON body:
{ "id": "evt_a1b2c3d4e5f6g7h8", "type": "link.created", "workspace_id": "ws_abc123", "timestamp": "2025-01-20T14:05:00.000Z", "data": { "id": "lnk_xyz789", "domain": "go.yourco.com", "slug": "demo", "target": "https://example.com/page", "title": "Demo Link", "short_url": "https://go.yourco.com/demo" }}The data field varies by event type. Some examples:
link.updated:
{ "data": { "id": "lnk_xyz789", "changes": { "target": "https://example.com/new-page", "tags": ["campaign-2025"] } }}member.role_changed:
{ "data": { "user_id": "usr_def456", "old_role": "editor", "new_role": "admin" }}link.broken:
{ "data": { "id": "lnk_xyz789", "domain": "go.yourco.com", "slug": "demo", "target": "https://dead-page.example.com", "status_code": 404 }}Request headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | nobsredir-webhooks/1.0 |
X-Webhook-Id | The webhook’s ID |
X-Webhook-Event | The event type (e.g. link.created) |
X-Webhook-Timestamp | ISO 8601 timestamp of the event |
X-Webhook-Signature | HMAC-SHA256 signature for verification |
Verifying signatures
Every delivery is signed with your webhook secret using HMAC-SHA256. Always verify signatures to confirm the request came from noBSredir.
The signature is computed as:
HMAC-SHA256(secret, timestamp + "." + body)And sent in the X-Webhook-Signature header as sha256=<hex>.
Node.js example
import crypto from "crypto";
function verifyWebhookSignature(body, signature, timestamp, secret) { const message = timestamp + "." + body; const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(message) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// In your webhook handler:app.post("/webhooks/nobsredir", (req, res) => { const signature = req.headers["x-webhook-signature"]; const timestamp = req.headers["x-webhook-timestamp"]; const body = req.rawBody; // raw string, not parsed JSON
if (!verifyWebhookSignature(body, signature, timestamp, WEBHOOK_SECRET)) { return res.status(401).send("Invalid signature"); }
const event = JSON.parse(body); console.log("Received:", event.type, event.data); res.status(200).send("OK");});Python example
import hmacimport hashlib
def verify_webhook(body: str, signature: str, timestamp: str, secret: str) -> bool: message = f"{timestamp}.{body}" expected = "sha256=" + hmac.new( secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)Preventing replay attacks
After verifying the signature, add two more checks:
Reject old timestamps. If
X-Webhook-Timestampis more than 5 minutes from the current time, discard the request. This stops an attacker from re-sending a captured payload later.Track seen event IDs. Store the
idfield from each payload (e.g. in Redis or a database) and reject duplicates. IDs are unique per delivery.
// Example checks to add to your handler:const timestamp = req.headers["x-webhook-timestamp"];const age = Date.now() - new Date(timestamp).getTime();if (age > 5 * 60 * 1000) { return res.status(400).send("Timestamp too old");}
const event = JSON.parse(body);if (await alreadySeen(event.id)) { return res.status(200).send("Duplicate");}await markSeen(event.id);Delivery behavior
- Best-effort delivery - each event is sent once. There are no automatic retries.
- 5-second timeout - if your endpoint doesn’t respond within 5 seconds, the delivery is marked as failed.
- 2xx = success - any HTTP 2xx response is treated as successful. Everything else is recorded as a failure.
- Delivery logs - kept for 7 days. Check them via the deliveries endpoint or the dashboard.
- Ordering - events are delivered in approximate order, but not guaranteed. Use the
timestampfield if ordering matters. - Inactive webhooks - set
active: falseto pause delivery. Events that occur while paused are not queued.
URL requirements
Webhook URLs must:
- Use HTTPS (HTTP is not allowed)
- Not point to private or internal addresses (localhost, 10.x.x.x, 192.168.x.x, etc.)
- Be at most 2048 characters long