Skip to content
noBSredir

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.

PlanMax webhooks
Pro3
Team10
Agency25

POST /workspaces/:wsId/webhooks

Create a new webhook endpoint.

Role: admin+

Terminal window
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:

FieldTypeRequiredDescription
namestringyesHuman-readable label for the webhook
urlstringyesHTTPS endpoint to receive events. Must not be a private/internal address. Max 2048 chars.
eventsstring[]yesList 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 fields
  • 402 - Webhook limit reached for your plan

GET /workspaces/:wsId/webhooks

List all webhooks for the workspace. The secret field is never included.

Role: viewer+

Terminal window
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+

Terminal window
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+

Terminal window
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:

FieldTypeRequiredDescription
namestringnoUpdated name
urlstringnoUpdated HTTPS URL (same validation as create)
eventsstring[]noUpdated event subscriptions
activebooleannoSet false to pause delivery without deleting

Response 200:

{"ok": true}

Errors:

  • 400 - Invalid URL, invalid event type, or no updates provided
  • 404 - Webhook not found

DELETE /workspaces/:wsId/webhooks/:webhookId

Delete a webhook. All delivery history is deleted with it.

Role: admin+

Terminal window
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+

Terminal window
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+

Terminal window
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+

Terminal window
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:

ParamTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max 100)
successboolean-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:

EventFires when
link.createdA link is created (single or bulk)
link.updatedA link’s target, slug, tags, or other fields are changed
link.deletedA link is deleted (single or bulk)
link.expiredA link with an expires_at date is accessed after expiry (fires once)
link.brokenA monitored link returns a 4xx/5xx status (Pro+ only, runs on cron)
link.recoveredA previously broken link starts responding normally again
domain.addedA custom domain is added to the workspace
domain.verifiedDNS verification succeeds for a custom domain
domain.removedA custom domain is deleted
member.addedA member is added to the workspace
member.removedA member is removed from the workspace
member.role_changedA member’s role is updated
webhook.testThe 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:

HeaderDescription
Content-Typeapplication/json
User-Agentnobsredir-webhooks/1.0
X-Webhook-IdThe webhook’s ID
X-Webhook-EventThe event type (e.g. link.created)
X-Webhook-TimestampISO 8601 timestamp of the event
X-Webhook-SignatureHMAC-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 hmac
import 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:

  1. Reject old timestamps. If X-Webhook-Timestamp is more than 5 minutes from the current time, discard the request. This stops an attacker from re-sending a captured payload later.

  2. Track seen event IDs. Store the id field 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 timestamp field if ordering matters.
  • Inactive webhooks - set active: false to 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