documentation

HookSync Documentation

Everything you need to integrate HookSync and deliver webhooks reliably — from your first API call to production-grade signature verification.

Quick start 5 min

From zero to your first delivered webhook in under 5 minutes. You need an account and an API key — both are free.

Step 1 — Register and create an API key

Sign up at /register, create an organization, then go to API Keys in the sidebar and generate a key. It will look like sk_abc123… (64 hex characters after the prefix).

Step 2 — Create a webhook endpoint

curl -X POST https://api.hooksync.net/api/v1/organizations/{org_id}/webhooks \
  -H 'Authorization: Bearer sk_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "production",
    "url":  "https://your-server.com/webhooks",
    "events": ["order.*"]
  }'

# Response:
{
  "data": {
    "id":     "wh_...",
    "secret": "whsec_...",   ← store this securely, shown only once
    "events": ["order.*"],
    "is_active": true
  }
}
Save the secret immediately. The whsec_…value is returned only once at creation time and is not stored in plain text. You'll use it to verify every incoming webhook.

Step 3 — Send your first event

curl -X POST https://api.hooksync.net/ingest/{org_id} \
  -H 'Authorization: Bearer sk_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "event_type": "order.placed",
    "payload": {
      "order_id": "ord_123",
      "amount":   9900,
      "currency": "usd"
    }
  }'

# Response:
{
  "data": {
    "event_id":   "evt_...",
    "dispatched": 1          ← number of endpoints that received delivery jobs
  }
}

HookSync queues a delivery job, signs the payload with HMAC-SHA256, and POSTs it to your endpoint within milliseconds. Check the Events tab in the dashboard to see delivery status in real time.

Core concepts

HookSync operates on five primary entities. Understanding how they relate to each other makes the rest of the API obvious.

Organization

The top-level tenant. Every API key, webhook endpoint, event, and consumer belongs to exactly one organization. When you call the Ingest API you target an organization by its ID.

Webhook endpoint

A URL owned by one of your customers (or your own system) that should receive events. Each endpoint has:

Event

An event is what you publish via the Ingest API. It has a event_type string and an arbitrary JSON payload. HookSync stores the event and fans it out to all matching endpoints.

Delivery

One delivery = one attempt to POST an event to one endpoint. If the first attempt fails, HookSync retries automatically (see Retry policy). Each delivery records the HTTP status code, response body (up to 8 KB), and any network error.

Consumer

An optional entity representing one of your end-customers. Linking events and endpoints to a consumer lets you scope the Consumer portal so each customer sees only their own data.

Authentication

API keys (M2M)

For server-to-server calls use a Bearer token in the Authorization header:

Authorization: Bearer sk_0a1b2c3d...   # 64 hex chars

API keys are stored as a SHA-256 hash — HookSync cannot recover the raw value. If a key is compromised, rotate it in Settings → API keys; the old key is invalidated immediately.

Session auth (dashboard)

The web dashboard uses cookie-based session auth (sid=, HttpOnly, Secure, SameSite=Strict). Mutating requests from the dashboard require a CSRF token delivered via the X-CSRF-Token header. You do not need to handle this manually — it is only relevant if you are building a custom frontend on top of the session API.

Creating an API key

POST /api/v1/organizations/{org_id}/api-keys
Authorization: Bearer sk_...
Content-Type: application/json

{
  "name":       "production-ingest",
  "expires_at": "2027-01-01T00:00:00Z"   // optional
}

# Response — raw key returned only once:
{
  "data": {
    "id":         "key_...",
    "name":       "production-ingest",
    "key":        "sk_0a1b2c3d...",   ← store this
    "created_at": "2026-05-28T10:00:00Z"
  }
}

Revoking an API key

DELETE /api/v1/organizations/{org_id}/api-keys/{key_id}
Authorization: Bearer sk_...

# 204 No Content
Plan limits: FREE organizations can have 2 API keys. PRO organizations can have 10.

Send an event

The Ingest endpoint is the only entry point for publishing events. Your backend sends one request to HookSync; HookSync fans it out to all matching endpoints.

POST /ingest/{org_id}
Authorization: Bearer sk_...
Content-Type: application/json
Idempotency-Key: <unique-string>   // optional

{
  "event_type":    "order.placed",
  "payload":       { "order_id": "ord_123", "amount": 9900 },
  "consumer_uid":  "user_456"       // optional — scope to one consumer
}

Request fields

FieldTypeRequiredDescription
event_typestringyesDot-separated type identifier. Max 128 chars. Examples: order.placed, invoice.paid.
payloadobjectyesArbitrary JSON. Delivered verbatim to endpoints.
consumer_uidstringnoRoutes the event only to endpoints belonging to this consumer.

Idempotency

Add an Idempotency-Key header to make the call safe to retry. If HookSync has already processed a request with the same key within 24 hours it returns the original response without creating a duplicate event.

Idempotency-Key: order_placed_ord_123_20260528

Response

HTTP/1.1 201 Created

{
  "data": {
    "event_id":   "evt_a1b2c3...",
    "dispatched": 3              // endpoints that received delivery jobs
  }
}
A 201 response guarantees the event was persisted and delivery jobs were enqueued. It does not mean delivery succeeded — use the delivery logs to track actual HTTP responses.

Batch ingest

Send up to 100 events in a single HTTP request. Useful for high-throughput pipelines where you want to reduce connection overhead.

POST /ingest/{org_id}/batch
Authorization: Bearer sk_...
Content-Type: application/json

[
  {
    "event_type": "order.placed",
    "payload":    { "order_id": "ord_001" }
  },
  {
    "event_type": "payment.captured",
    "payload":    { "order_id": "ord_001", "amount": 9900 }
  }
]

Response

HTTP/1.1 201 Created

{
  "data": {
    "accepted": 2,
    "event_ids": ["evt_...", "evt_..."]
  }
}

Limits

LimitValue
Max events per batch100
Max payload size per event256 KB
Ingest rate (FREE)10 req/s
Ingest rate (PRO)1 000 req/s

Event types & filtering

Every endpoint subscribes to one or more event type patterns. When an event is ingested, HookSync evaluates each active endpoint against the event type and delivers only to matching ones.

Exact match

"events": ["order.placed"]
# matches: order.placed
# does not match: order.refunded, order.placed.v2

Wildcard suffix

"events": ["order.*"]
# matches: order.placed, order.refunded, order.cancelled
# does not match: payment.captured, order.placed.v2

Catch-all

"events": ["*"]
# matches every event type

Multiple patterns

"events": ["order.*", "payment.captured", "user.created"]
# endpoint receives events matching ANY of the listed patterns
Wildcards only expand one segment. order.* matches order.placed but not order.placed.v2. Use * (catch-all) when you need everything regardless of nesting.

Updating subscriptions

PATCH /api/v1/organizations/{org_id}/webhooks/{webhook_id}
Authorization: Bearer sk_...
Content-Type: application/json

{
  "events": ["order.*", "invoice.paid"]
}

Retry policy

HookSync considers a delivery successful when the endpoint returns any 2xx HTTP status within 30 seconds. Any other status code — or a network error / timeout — is treated as a failure and triggers the retry schedule.

AttemptDelay after previousCumulative elapsed
1 (initial)0
25 minutes5 min
330 minutes35 min
42 hours2 h 35 min
55 hours7 h 35 min
610 hours17 h 35 min

After attempt 6 the delivery is marked Abandoned. The organization owner receives an email notification with the endpoint URL, event type, delivery ID, and number of attempts made.

Manual retry

Once you have fixed the issue on your server you can trigger an immediate retry without waiting for the next scheduled attempt:

POST /api/v1/organizations/{org_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry
Authorization: Bearer sk_...

# 204 No Content — delivery is re-queued immediately

Failure rate alerts

HookSync checks the failure rate for every endpoint every 6 hours. If more than 50% of deliveries in the past 7 days failed, the organization owner receives an email alert so you can investigate before the endpoint is abandoned entirely.

Payload signing

Every delivery request carries an X-Webhook-Signatureheader. The value is an HMAC-SHA256 of the raw request body, computed with the endpoint's unique secret.

X-Webhook-Signature: sha256=3d4f5e6a7b8c...

How it works

Before each delivery attempt HookSync:

  1. Serializes the event payload to JSON
  2. Computes HMAC-SHA256(secret, raw_body)
  3. Hex-encodes the digest and prefixes it with sha256=
  4. Attaches it as X-Webhook-Signature in the request headers

Why this matters

Verifying the signature on your server proves two things:

Always use a timing-safe comparison (e.g. crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) when comparing the expected and received signatures. A naive string comparison leaks timing information and can be exploited.

Endpoint secret rotation

If a secret is compromised, update the endpoint to generate a new one — all future deliveries will use the new secret. Deliveries already queued with the old secret will complete with the old signature.

Verifying signatures

Copy the snippet for your server language. In every case the logic is identical: recompute the HMAC over the raw request body and compare with the header value using a timing-safe function.

import { createHmac, timingSafeEqual } from 'crypto';

function verifyHookSyncSignature(payload: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(payload).digest('hex');
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express / Fastify handler
app.post('/hooks', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-webhook-signature'] as string;
  if (!verifyHookSyncSignature(req.body.toString(), sig, process.env.HOOKSYNC_SECRET!)) {
    return res.status(401).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // process event...
  res.send('ok');
});
Use the raw body. Parse JSON after verification, never before. Parsing and re-serializing the body changes whitespace and key ordering and will make every signature check fail.

Delivery logs

Every delivery attempt is stored permanently with full request and response data. Your customers can self-serve 90% of integration issues without contacting your support team.

What is logged per attempt

FieldDescription
statuspending · delivered · failed · abandoned
last_response_codeHTTP status code returned by the endpoint (e.g. 200, 500)
response_bodyFirst 8 KB of the response body — useful for debugging server-side errors
last_errorNetwork-level error description when the request could not be made (DNS failure, timeout, TLS error)
attemptsNumber of attempts made so far
next_attempt_atTimestamp of the next scheduled retry (null when delivered or abandoned)
delivered_atTimestamp of successful delivery

Listing deliveries

GET /api/v1/organizations/{org_id}/webhooks/{webhook_id}/deliveries?page=1&per_page=50
Authorization: Bearer sk_...

# Response:
{
  "data": [
    {
      "id":                 "del_...",
      "event_type":         "order.placed",
      "status":             "delivered",
      "attempts":           1,
      "last_response_code": 200,
      "response_body":      "ok",
      "delivered_at":       "2026-05-28T10:05:01Z"
    },
    {
      "id":                 "del_...",
      "event_type":         "order.refunded",
      "status":             "failed",
      "attempts":           3,
      "last_response_code": 500,
      "response_body":      "Internal Server Error: database connection refused",
      "next_attempt_at":    "2026-05-28T12:05:01Z"
    }
  ],
  "meta": { "page": 1, "per_page": 50, "total": 124 }
}

Consumer portal — overview

The consumer portal is a ready-made UI you can expose directly to your end-customers. Each customer gets a scoped, token-protected view where they can manage their own webhook endpoints and inspect delivery history — without seeing data from other customers and without any access to your organization-level controls.

What customers can do in the portal

What customers cannot do

Generate a portal token

Before redirecting a customer to the portal, your backend generates a short-lived access token scoped to that customer. The token is tied to a consumer_uid — a stable identifier from your own system (user ID, account ID, etc.).

POST /api/v1/portal/token
Authorization: Bearer sk_...
Content-Type: application/json

{
  "org_id":          "{org_id}",
  "consumer_uid":    "user_456",
  "expires_in_secs": 3600          // default: 3600 (1 hour), max: 86400
}

# Response:
{
  "data": {
    "token":      "pt_a1b2c3d4...",
    "expires_at": "2026-05-28T11:00:00Z",
    "portal_url": "https://api.hooksync.net/portal/v1?token=pt_a1b2c3d4..."
  }
}

Security properties

Generate a fresh token on every page visit rather than caching it. Tokens are cheap to generate and expire quickly.

Embed in your product

There are two integration patterns: redirect and iframe. Choose based on your UX requirements.

Option A — redirect (recommended)

The simplest integration. When a customer clicks “Manage webhooks”, your backend generates a token and redirects the browser to the portal URL:

# Server-side (any language)
token = hooksync.portal.generate_token(
  consumer_uid:    current_user.id,
  expires_in_secs: 3600
)

redirect_to token.portal_url

Option B — iframe

Embed the portal directly inside your dashboard with an <iframe>. Pass the token as a query parameter. The portal respects your CSS custom properties for basic white-labelling.

<iframe
  src="https://api.hooksync.net/portal/v1?token=pt_..."
  style="width: 100%; min-height: 600px; border: none;"
  title="Webhook settings"
/>

White-labelling

The portal reads CSS custom properties from the parent page when embedded as an iframe (same-origin) or from the query string parameters accent, bg, and font(cross-origin). This lets you match the portal to your product's design system without rebuilding it.

OpenAPI spec

The complete API schema is available as an OpenAPI 3.1 document. Import it into Postman, Insomnia, or any API client to get auto-generated request builders and response type hints.

ResourceURL
OpenAPI JSONGET /api-docs/openapi.json
Interactive Swagger UIGET /api-docs
# Download the spec
curl https://api.hooksync.net/api-docs/openapi.json -o hooksync-openapi.json

# Import into Postman
# File → Import → select hooksync-openapi.json

SDK examples

Send events and manage resources from your backend using idiomatic code in your language of choice.

import { HookSyncClient } from '@hooksync/sdk';

const hooksync = new HookSyncClient({
  apiKey:  process.env.HOOKSYNC_API_KEY!,
  orgId:   process.env.HOOKSYNC_ORG_ID!,
  baseUrl: 'https://api.hooksync.net',
});

// Send a single event
const result = await hooksync.send({
  event_type: 'order.placed',
  payload:    { order_id: 'ord_123', amount: 9900 },
  consumer_uid: 'user_456',   // optional — route to one consumer
});
console.log(result.event_id);  // evt_...

// Send up to 100 events in one request
const batch = await hooksync.sendBatch([
  { event_type: 'order.placed',    payload: { order_id: 'ord_001' } },
  { event_type: 'payment.captured', payload: { order_id: 'ord_001', amount: 9900 } },
]);
console.log(batch.accepted);  // 2

// Upsert a consumer (idempotent)
await hooksync.consumers.upsert({
  uid:      'user_456',
  name:     'Jane Doe',
  metadata: { plan: 'pro', account_id: 'acc_789' },
});

// List all consumers
const { items } = await hooksync.events.list({ page: 1, per_page: 20 });

// List endpoints
const endpoints = await hooksync.endpoints.list();

Endpoints reference

Authentication

MethodPathDescription
POST/api/v1/auth/registerCreate account
POST/api/v1/auth/loginPassword login — sets sid cookie
POST/api/v1/auth/logoutInvalidate session
POST/api/v1/auth/mfa/verifyComplete MFA login challenge
POST/api/v1/auth/password-reset/requestSend password-reset email
POST/api/v1/auth/password-reset/confirmApply new password with reset token
POST/api/v1/auth/verify-emailVerify email address with token

Organizations & members

MethodPathDescription
GET/api/v1/organizationsList organizations for the authenticated user
POST/api/v1/organizationsCreate organization
GET/api/v1/organizations/{org_id}Get organization details and plan
DELETE/api/v1/organizations/{org_id}Delete organization (cascades)
GET/api/v1/organizations/{org_id}/membersList members with roles
POST/api/v1/organizations/{org_id}/members/inviteInvite member by email
DELETE/api/v1/organizations/{org_id}/members/{member_id}Remove member

API keys

MethodPathDescription
GET/api/v1/organizations/{org_id}/api-keysList API keys
POST/api/v1/organizations/{org_id}/api-keysCreate API key — returns raw key once
DELETE/api/v1/organizations/{org_id}/api-keys/{key_id}Revoke API key

Ingest

MethodPathDescription
POST/ingest/{org_id}Publish a single event
POST/ingest/{org_id}/batchPublish up to 100 events

Webhook endpoints

MethodPathDescription
GET/api/v1/organizations/{org_id}/webhooksList endpoints
POST/api/v1/organizations/{org_id}/webhooksCreate endpoint
PATCH/api/v1/organizations/{org_id}/webhooks/{webhook_id}Update endpoint (name, url, events, active, ip_allowlist)
DELETE/api/v1/organizations/{org_id}/webhooks/{webhook_id}Delete endpoint

Events & deliveries

MethodPathDescription
GET/api/v1/organizations/{org_id}/eventsList events (newest first)
GET/api/v1/organizations/{org_id}/events/{event_id}Get event with delivery list
GET/api/v1/organizations/{org_id}/webhooks/{webhook_id}/deliveriesList deliveries for an endpoint
POST/api/v1/organizations/{org_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retryRetry a failed delivery immediately

Consumer portal

MethodPathDescription
POST/api/v1/portal/tokenGenerate scoped portal access token
GET/portal/v1/endpointsConsumer: list own endpoints
POST/portal/v1/endpointsConsumer: create endpoint
PATCH/portal/v1/endpoints/{webhook_id}Consumer: update own endpoint
DELETE/portal/v1/endpoints/{webhook_id}Consumer: delete own endpoint
GET/portal/v1/eventsConsumer: list own events
Explore the interactive API reference
Live Swagger UI with every endpoint, request schema, and example response.
open API docs →