Public URLDocs

Webhooks

Signed, retried webhook deliveries for link, click, conversion, and billing events.

Public Url webhooks push real-time events to your own endpoints. Subscribe to link, click, conversion, domain, campaign, report, and billing events — every delivery is signed, retried with exponential backoff, and logged for auditing.


Base URL

https://api.publicurl.in/v1/webhooks

All endpoints require authentication via session cookie or API key. Admin-level endpoints require the owner or admin role.


Event Catalog

Subscribe to individual event types or use "*" to receive all events.

EventDescriptionVolume
link.createdNew short link createdLow
link.updatedLink renamed, redirected, archived, or metadata changedLow
link.deletedLink permanently removedLow
domain.verifiedBranded domain passes DNS verificationLow
domain.failedDomain verification attempt failsLow
click.receivedEvery click for a link in the workspaceHigh
conversion.receivedServer-to-server conversion event landsMedium
campaign.completedCampaign marked completeLow
report.generatedScheduled or on-demand report export finishesLow
billing.subscription.updatedWorkspace subscription changes plan or renewsLow

Endpoints

List endpoints

GET /v1/webhooks/endpoints

Returns all webhook endpoints for the active workspace. The secret field is redacted.

Response 200 OK

{
  "data": [
    {
      "id": "wh_abc123",
      "workspaceId": "wrk_xyz",
      "url": "https://hooks.example.com/events",
      "description": "Production event receiver",
      "events": ["link.created", "link.updated", "click.received"],
      "status": "active",
      "failureCount": 0,
      "lastFailureAt": null,
      "lastSuccessAt": "2026-08-03T12:00:00Z",
      "createdBy": "usr_abc",
      "createdAt": "2026-07-01T00:00:00Z",
      "updatedAt": "2026-08-03T12:00:00Z"
    }
  ]
}

Create endpoint

POST /v1/webhooks/endpoints

Request body

FieldTypeRequiredDescription
urlstringyesHTTPS endpoint that receives events
eventsstring[]yesEvent types to subscribe to (use ["*"] for all)
descriptionstringnoMax 500 characters
statusstringno"active" (default) or "paused"

Response 201 Created

Returns the created endpoint including the auto-generated secret. Store the secret — it is only returned on creation and via the dedicated secret endpoint.

{
  "data": {
    "id": "wh_abc123",
    "workspaceId": "wrk_xyz",
    "url": "https://hooks.example.com/events",
    "events": ["link.created", "click.received"],
    "status": "active",
    "secret": "whsec_5f4dcc3b5aa765d61d8327deb882cf99",
    "createdAt": "2026-08-03T12:00:00Z",
    "updatedAt": "2026-08-03T12:00:00Z"
  }
}

Get endpoint

GET /v1/webhooks/endpoints/:id

Returns a single endpoint. The secret field is redacted.


Get signing secret

GET /v1/webhooks/endpoints/:id/secret

Returns the endpoint's current signing secret. Requires admin role.

Response 200 OK

{
  "data": {
    "id": "wh_abc123",
    "secret": "whsec_5f4dcc3b5aa765d61d8327deb882cf99"
  }
}

Update endpoint

PATCH /v1/webhooks/endpoints/:id

Request body (all fields optional)

FieldTypeDescription
urlstringNew HTTPS endpoint URL
eventsstring[]Replace subscribed event types
descriptionstring or nullUpdate or clear description
statusstring"active" or "paused"

Rotate secret

POST /v1/webhooks/endpoints/:id/secret/rotate

Generates a new signing secret. The old secret is immediately invalidated — update your consumer code before calling this.

Response 200 OK

{
  "data": {
    "id": "wh_abc123",
    "secret": "whsec_newly_generated_secret"
  }
}

Send test event

POST /v1/webhooks/endpoints/:id/test

Sends a test payload to the endpoint. Useful for verifying your consumer is set up correctly.

Request body

FieldTypeRequiredDescription
eventTypestringnoDefaults to link.created

Response 202 Accepted

{
  "data": {
    "id": "whd_test_1722681600000",
    "status": "pending",
    "eventType": "link.created",
    "createdAt": "2026-08-03T12:00:00Z"
  }
}

Delete endpoint

DELETE /v1/webhooks/endpoints/:id

Permanently removes the endpoint and all its delivery logs. Response 204 No Content.


List deliveries

GET /v1/webhooks/deliveries

Query parameters

ParameterTypeDescription
endpointIdstringFilter by endpoint
statusstringpending, processing, completed, failed, dead_letter
limitnumberMax 100, default 50
offsetnumberDefault 0

Response 200 OK

{
  "data": [
    {
      "id": "whd_del_001",
      "workspaceId": "wrk_xyz",
      "endpointId": "wh_abc123",
      "eventType": "link.created",
      "eventId": "evt_001",
      "status": "completed",
      "attempts": 1,
      "maxAttempts": 5,
      "responseStatus": 200,
      "durationMs": 145,
      "createdAt": "2026-08-03T12:00:00Z",
      "completedAt": "2026-08-03T12:00:01Z"
    }
  ],
  "meta": { "total": 42 }
}

Get delivery

GET /v1/webhooks/deliveries/:deliveryId

Returns full delivery details including response body (truncated to 4000 chars), response headers, and the original event payload.


Retry delivery

POST /v1/webhooks/deliveries/:deliveryId/retry

Re-queues a failed or dead-letter delivery for another attempt. Requires admin role.

Response 202 Accepted


List event catalog

GET /v1/webhooks/events

Returns all supported event types with labels and descriptions.

Response 200 OK

{
  "data": {
    "events": [
      {
        "type": "link.created",
        "label": "Link created",
        "description": "New short link created in the workspace"
      }
    ]
  }
}

Manual dispatch

POST /v1/webhooks/dispatch

Manually dispatches an event to all active endpoints subscribed to the given type. Useful for testing or re-processing.

Request body

FieldTypeRequiredDescription
typestringyesEvent type from the catalog
payloadobjectnoCustom data sent to subscribers

Response 202 Accepted

{
  "data": { "dispatched": 3 }
}

Delivery Format

Every delivery is an HTTP POST to your endpoint URL with the following structure:

Headers

HeaderValue
Content-Typeapplication/json
User-AgentPublicUrl-Webhooks/1.0
x-public-url-eventEvent type (e.g. link.created)
x-public-url-signaturet=<unix_timestamp>,v1=<hmac_hex>
x-public-url-timestampUnix timestamp (seconds)
x-public-url-event-idUpstream event identifier

Body

{
  "id": "whd_del_001",
  "type": "link.created",
  "workspaceId": "wrk_xyz",
  "endpointId": "wh_abc123",
  "attempt": 1,
  "createdAt": "2026-08-03T12:00:00.000Z",
  "data": {
    "id": "lnk_abc",
    "slug": "my-link",
    "destinationUrl": "https://example.com",
    "workspaceId": "wrk_xyz"
  }
}

The data field contains the original event payload. The shape varies by event type.


Signature Verification

Every delivery includes an x-public-url-signature header. Verify it to confirm the payload came from Public Url and hasn't been tampered with.

Algorithm

  1. Extract the timestamp (t) and signature (v1) from the header:
    t=1722681600,v1=5f4dcc3b5aa765d61d8327deb882cf99
  2. Build the signed payload string: t.<request_body> (the literal t. followed by the raw JSON body).
  3. Compute HMAC-SHA256 of that string using your endpoint's secret.
  4. Compare the computed signature with v1 using constant-time comparison.
  5. (Optional) Reject payloads where t is more than 5 minutes old.

Node.js example

import { createHmac, timingSafeEqual } from "node:crypto"

function verifyWebhookSignature(
  body: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds = 300
): boolean {
  const parts = signatureHeader.split(",")
  const ts = parts.find((p) => p.startsWith("t="))?.slice(2)
  const sig = parts.find((p) => p.startsWith("v1="))?.slice(3)
  if (!ts || !sig) return false

  const now = Math.floor(Date.now() / 1000)
  if (now - Number(ts) > toleranceSeconds) return false

  const payload = `t.${ts}.${body}`
  const computed = createHmac("sha256", secret).update(payload).digest("hex")

  const a = Buffer.from(computed)
  const b = Buffer.from(sig)
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}

Python example

import hmac
import hashlib
import time

def verify_webhook_signature(body, signature_header, secret, tolerance=300):
    parts = dict(p.split("=") for p in signature_header.split(","))
    ts = parts.get("t")
    sig = parts.get("v1")
    if not ts or not sig:
        return False
    if time.time() - int(ts) > tolerance:
        return False
    payload = f"t.{ts}.{body}".encode()
    computed = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, sig)

Retry Schedule

Failed deliveries are retried up to 5 times with exponential backoff:

AttemptDelay
130 seconds
22 minutes
310 minutes
430 minutes
51 hour

After the 5th failure the delivery moves to dead_letter status. You can manually retry dead-letter deliveries via the retry endpoint.

A delivery is considered successful when your endpoint responds with any HTTP 2xx status code. Redirects are not followed (maxRedirects: 0).


Best Practices

  1. Respond quickly — your endpoint should return a 2xx within 10 seconds. Use a queue for long-running work.
  2. Verify signatures — always verify the x-public-url-signature header before processing.
  3. Idempotent processing — the id field in the body is unique per delivery. Use it to deduplicate.
  4. Handle retries — the same event may be delivered more than once. Design your consumer to be idempotent.
  5. Monitor delivery logs — check the deliveries endpoint for failures and response times.
  6. Rotate secrets periodically — use the secret rotation endpoint to rotate secrets without downtime.
  7. Use "*" sparingly — subscribing to all events is convenient but click.received is high-volume. Prefer specific event types.

Rate Limits

Webhook delivery is subject to the workspace's plan limits for webhook_endpoints (number of endpoints) and api_requests_monthly (API calls). Delivery retries count against the monthly API request limit.


Error Codes

CodeMeaning
PLAN_CAPABILITY_REQUIREDWebhooks are not included in your plan
PLAN_LIMIT_REACHEDWebhook endpoint limit reached for your plan
VALIDATION_ERRORInvalid request payload
NOT_FOUNDEndpoint or delivery not found
FORBIDDENInsufficient role or workspace mismatch

On this page