Introduction

Receive signed workspace events securely in your backend, with a complete event catalog and delivery contract.

What this page covers

  • A log is Hoko's small, immutable activity fact: what happened, which record it affected, who performed it, and when.
  • A webhook delivery is one outbound attempt to one receiver. It may be absent, pending, Unconfirmed, succeeded, or failed without changing whether the original activity was committed. Unconfirmed is the dashboard label for the persisted unknown state.
  • A payload is the same thin message contract for every subscribed event, including lead.created and sale.created.

Webhooks are signed server-to-server notifications from Hoko. When a subscribed workspace action is committed, Hoko sends a best-effort HTTPS POST to each eligible webhook. Use webhooks to synchronize your system or start work in your backend; use Logs to inspect the workspace activity fact that caused the message.

The two concepts are related but not interchangeable:

The Webhooks help guide covers the dashboard flow. This page is the technical reference for access, every selectable event, signatures, payloads, receiver behavior, and limits.

Identifiers

Public webhook payloads use bare 26-character ULIDs. Examples use typed placeholders such as <event_id> and <link_id>; actual payloads contain the corresponding public IDs. Storage identifiers are not part of the public contract.

Access and plan limits

Manage webhooks in Dashboard → Integrations → Webhooks when webhooks are enabled in the deployment. Access requires an active account with owner membership in every active collection of the workspace, with at least one active collection. Blocked or deleted memberships do not qualify. Webhooks are workspace-wide; choosing a collection does not scope a webhook to that collection. There is no public webhook-management API, and an API key cannot authorize dashboard management.

PlanActive webhooksLog history shownWebhook delivery
Free07 daysNot available
Pro030 daysNot available
Business1090 daysAvailable
Enterprise20180 daysAvailable
Elite40365 daysAvailable

Business, Enterprise, and Elite allow five additional storage slots for pending and disabled replacement webhooks, so the total stored configurations are 15, 25, and 45. A downgrade preserves excess configurations but suspends delivery to webhooks above the new active allowance. An upgrade makes eligible active webhooks available again; missed activity is not replayed.

The active webhook ceilings are Business 10, Enterprise 20, and Elite 40.

Create and verify a webhook

  1. Prepare a public HTTPS receiver. Localhost, private, link-local, loopback, ambiguous IP, and redirect destinations are rejected by SSRF controls. 2. In the dashboard, enter a name and receiver URL, choose one or more events from the catalog below, and decide how to set the signing secret. Paste an existing canonical whsec_ secret, use the shuffle control to generate one, or leave the field blank for Hoko to generate one. 3. Create the webhook. It starts as pending_verification. Copy the signing secret immediately and store it in server-only secret storage; it cannot be reopened from the dashboard. 4. Implement signature verification and the exact endpoint.verification challenge response before selecting Verify webhook. 5. A successful verification changes the webhook to active. Only active, plan-eligible webhooks receive subscribed workspace events. 6. Send a test and inspect its delivery. A test is a real outbound request; handle endpoint.test without triggering customer-facing business effects.

Owner actions are limited to 10 verification attempts per hour and 30 combined test-send and resend actions per hour. Rate-limited actions return 429 with Retry-After.

Event catalog

The shared event catalog lists every workspace event available to Webhooks and Logs. Use it to understand each scope and action, then copy the exact event name from the table.

Browse the event catalog

Payloads

Every selectable workspace event follows one payload contract: the same four-field JSON shape and JSON Schema version, 1.0. The version is in the published schema URL, not in the message body. lead.created and sale.created use this same contract; they do not include conversion details or record snapshots.

Code json
{
	"id": "<event_id>",
	"type": "utmTemplate.created",
	"created": "2026-09-23T10:30:00.000Z",
	"data": {
		"id": "<utm_template_id>",
		"workspaceId": "<workspace_id>"
	}
}
FieldMeaning
idStable event ID. It matches the webhook-id request header and is the receiver’s deduplication key.
typeExact event name from the event catalog.
createdTime the event was recorded, in ISO 8601 UTC format.
data.idPublic ID of the affected record.
data.workspaceIdPublic ID of the workspace that owns the event.

Every selectable event schema has the same required fields and no additional payload fields. Use the scope page for an event’s affected record and example. Fetch more information from the relevant API when available. Schema documents are published at /schemas/webhooks/<event-name>/1.0.json.

Webhook test and verification messages

endpoint.test uses the same top-level fields and data contains only the webhook id and workspaceId. endpoint.verification also uses the same top-level fields, with challenge and expiresAt added to data because the receiver must echo the challenge before the webhook can be activated. These are operational messages, not selectable workspace events.

Code json
{
	"id": "<event_id>",
	"type": "endpoint.verification",
	"created": "2026-09-23T10:30:00.000Z",
	"data": {
		"id": "<webhook_id>",
		"workspaceId": "<workspace_id>",
		"challenge": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
		"expiresAt": "2026-09-23T10:35:00.000Z"
	}
}

Verify requests

Download the tested Node standard-library receiver example. It reads HOKO_WEBHOOK_SECRET server-side and does not depend on a third-party package.

Each request includes content-type: application/json and:

HeaderMeaning
webhook-idStable event ID and durable deduplication key
webhook-timestampUnix timestamp in seconds
webhook-signaturev1, followed by a base64 HMAC-SHA256 signature

Your receiver must:

  1. Read the exact raw request bytes before parsing JSON. 2. Reject bodies over 64 KiB and timestamps outside ±300 seconds. 3. Compute HMAC-SHA256 over webhook-id + "." + webhook-timestamp + "." + exactRawBody using the canonical 32-byte whsec_ key. 4. Compare equal-length signatures with timingSafeEqual. 5. Parse JSON only after verification and require its id to equal webhook-id. 6. For endpoint.verification, return exactly {"challenge":"<received challenge>"} as JSON. 7. Atomically record a durable unique receipt keyed by webhook-id while persisting or queueing your work. 8. Return 2xx quickly. A duplicate ID must also return 2xx without repeating its side effect.

An in-memory Set is not production deduplication. Use a database unique constraint or another durable atomic store. Verification must happen before any receipt, persistence, queue, or business side effect. Never parse and reserialize a body before verifying it; whitespace changes invalidate the signature.

Delivery contract and limits

  • pending: the delivery row exists, but no attempt has started.
  • Unconfirmed (unknown): Hoko claimed the attempt but saved no terminal result. Hoko cannot prove whether the receiver received it, so this state is never reported as success or failure.
  • succeeded: Hoko recorded a successful sender result, not proof of downstream business completion.
  • failed: Hoko recorded a failure category such as timeout, DNS, TLS, network, unsafe destination, invalid challenge, or non-2xx response.

Hoko writes the log and delivery row in the same transaction as the originating mutation, then awaits one best-effort outbound attempt from the committed request. Receiver failure does not roll back an accepted Hoko mutation. A rolled-back mutation sends no webhook.

Delivery is immediate and request-bound, not a durable background queue. Business-plus workspaces can use webhooks, subject to their webhook allowance. Hoko supports fan-out to up to 40 active webhooks and keeps at most 1,000 delivery candidates in request memory. Large bulk operations can leave additional rows pending when the three-second receiver budget expires. Hoko makes one automatic attempt for each delivery, with no automatic retries or ordering guarantee. Owners can send an eligible delivery again from the log row's More options menu.

The Webhooks dashboard side sheet displays the receiver URL and the 10 most recent attempts for the selected webhook. See all deliveries opens the dedicated per-webhook delivery history page from either the row's More options menu or the side sheet. That page uses paginated loading and supports sorting and filtering, so older attempts are loaded only when requested.

Delivery states have these fixed meanings:

The recorded categories are success, unsafe_destination, dns_failure, invalid_message, oversized_request, timeout, tls_failure, network_failure, non_2xx, invalid_challenge, response_too_large, and pinning_unavailable. Only success maps to succeeded; every other recorded category maps to failed. An Unconfirmed attempt has no category because Hoko saved no final result.

Requests are limited to 64 KiB and three seconds of receiver time. Database work adds time to the originating request. There is no monthly webhook quota or overage billing; attempts are separate from tracked clicks and conversion usage. Hoko keeps delivery history for the workspace lifetime, including after a webhook is deleted.

Resend a delivery

Open Logs, find the event, and choose More options → Resend on a pending, Unconfirmed, or failed delivery. A pending delivery reuses attempt 1. An Unconfirmed delivery can be resent after a five-minute cooldown. A failed delivery creates the next attempt number. Hoko records every result alongside the original result. A successful delivery cannot be resent from the dashboard.

The resend uses the original event ID and a new signature timestamp. A targeted webhook test resends its original endpoint.test body. A log-backed broadcast keeps its original event type and body. Receivers must treat webhook-id as a durable idempotency key because an earlier attempt may have reached the receiver before Hoko recorded its result. Resending requires an active, plan-eligible webhook.

Lifecycle, replacement, and troubleshooting

The webhook moves through pending_verification, active, disabled, or deleted. Disabling stops new fan-out and cannot recall an already-sent request. To change the receiver URL, first disable the webhook and save the new URL. It returns to pending_verification while Hoko keeps the webhook ID, delivery history, and signing secret. Verify the new destination to activate it. To change the webhook name, subscribed events, or signing secret, create and verify a replacement, switch traffic, then disable and delete the old webhook. If both are active, the receiver may see the same event through both.

Select Delete webhook when you no longer need the configuration. Deletion hides the webhook from management, preserves its name and delivery history, and erases signing material. It does not remove ordinary workspace log facts. Deleted webhooks cannot deliver or be resent.

For a missing event, check the webhook is active, the event is selected, the action happened in this workspace, the plan still allows the webhook, and the delivery result. Old activity is not sent retroactively. For a failed delivery, fix the receiver and use More options → Resend. For an Unconfirmed delivery, assume the receiver may have applied the event, use the durable event-ID receipt to avoid duplicate processing, and resend only if needed.

Security and data boundaries

During local development, expose the receiver through a public HTTPS tunnel. Do not disable signature verification to make a tunnel work.

Keep HOKO_WEBHOOK_SECRET and every whsec_ signing secret in server-only secret storage. Never put a signing secret in browser code, a client bundle, localStorage, analytics, client logs, or a public repository.

Payloads intentionally exclude record snapshots, customer name, email, phone, avatar, conversion metadata, API keys, signing secrets, destination credentials, full response bodies, raw IP addresses, exact location, visitor or session identifiers, the full referrer, destination query strings, and other database-row fields. Treat the included identifiers as sensitive business data. Do not log raw bodies, signature headers, secrets, URL query values, or challenge values.

The canonical JSON and JavaScript examples are locale-neutral. Additional-language examples are not currently published.