Step-by-step guidance
Who can do this: Viewing webhooks requires integrations.view; creating, editing, deleting, and manually delivering also require integrations.manage. Module, subscription, and integrations entitlement rules apply.
Before you begin
- An HTTPS receiver controlled by your organization, using its final URL rather than a redirect.
- A server-side secret store, raw-body signature verification, durable duplicate detection, and monitoring. Read the current limitations before relying on delivery.
Steps
- Select the correct organization and open Integrations → Webhooks at /administration/integrations.
- Choose New webhook, enter a descriptive Name and HTTPS endpoint, and select at least one supported event.
- Select Active when your receiver is ready, then Save webhook. Copy the revealed signing secret into your receiver’s secret store.
- Use a controlled, harmless real event in a test workspace to check automatic delivery. Saving does not test connectivity, and the current UI has no dedicated send-test button.
Expected result
Your receiver can verify and accept subscribed event notifications, subject to delivery limits and the supported event producers.
Next action
Implement signature checks and idempotency, monitor delivery failures, and reconcile critical data independently of notifications.
Endpoint access and setup
These are outgoing customer notifications, separate from incoming provider synchronization and billing-provider callbacks. An API key does not authorize webhook management. Saving or manually delivering checks the integrations entitlement, in addition to normal organization access.
Use a receiver you own and authorize to receive event data. Do not put credentials in its URL or use loopback or private-infrastructure URLs. HTTPS is required, but saving does not certify ownership, reachability, network safety, or redirect behavior.
Supported event subscriptions
Use these exact event names. Wildcards are not supported. Completing a work item again after reopening it can produce a new business event, not a duplicate of the earlier event.
| Event name | When it is emitted automatically |
|---|---|
ProjectCreated | A project is created. |
ProjectUpdated | Not currently emitted for normal project edits. |
WorkItemCompleted | A work item’s status changes to Done. |
ResourceAllocated | A resource allocation is created. |
TimesheetApproved | A timesheet’s status changes to Approved. |
RiskCreated | A project risk is created. |
ChangeRequestApproved | A change request’s status changes to Approved. |
Delivery request headers
Allocyn sends POST with Content-Type: application/json; charset=utf-8. Header names are case-insensitive. The signature authenticates the timestamp and payload, not the event and delivery-ID headers separately; use the verified body’s type for trusted dispatch.
| Header | Meaning |
|---|---|
X-Allocyn-Signature | sha256= followed by 64 lowercase hexadecimal HMAC characters. |
X-Allocyn-Timestamp | Decimal Unix time in seconds for this attempt. |
X-Allocyn-Event | Event name; verify the body before dispatching work. |
X-Allocyn-Delivery-Id | Delivery identifier retained across retries; it is not independently signed. |
Automatic event payload
This fictional payload is formatted for readability. The id identifies the business event, not the delivery; occurredAtUtc is the event time, not the retry time. The envelope does not include an organization ID, actor, or schema version. Map the endpoint and secret to your organization in your receiving system.
The data object contains event-specific details, not a complete entity snapshot. Changes can be empty and oldValue/newValue can be strings or nulls, even for numeric-looking values. Validate what your workflow needs and tolerate additional fields. Follow-up public API reads support only its four documented collections.
{
"id": "11111111-1111-7111-8111-111111111111",
"type": "WorkItemCompleted",
"occurredAtUtc": "2026-09-07T10:30:00Z",
"entityType": "WorkItem",
"entityId": "22222222-2222-7222-8222-222222222222",
"data": {
"changes": {
"Status": { "oldValue": "InProgress", "newValue": "Done" }
}
}
}Verify the signature before parsing JSON
Keep the exact received body bytes before parsing JSON. Do not pretty-print, reserialize, normalize whitespace, or change escaping. Use the literal signing-secret text as the UTF-8 key: although it looks like Base64, do not decode it, trim it, or substitute an API key.
The Node.js example verifies signatures only. Your receiver must enforce a body-size limit, reject duplicate or ambiguous headers, obtain the secret securely, and pass the raw Buffer. Its five-minute timestamp window is a recommended receiver policy, not a sender guarantee; check the attempt timestamp rather than the older event time.
Reject failed verification. After success, parse and validate the body and accepted event type, then persist or enqueue it durably before acknowledging. Successful verification alone does not prevent replay or validate application data.
message = UTF8(timestampText + ".") followed by the raw payload bytes
key = UTF8(signingSecretText)
digest = HMAC-SHA256(key, message)
signatureHeader = "sha256=" + lowercaseHex(digest)import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyAllocynWebhook(options) {
if (!options || typeof options !== 'object') return false;
const {
rawBody, signature, timestamp, secret,
nowSeconds = Math.floor(Date.now() / 1000),
toleranceSeconds = 300,
} = options;
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret.length) return false;
if (typeof signature !== 'string' || signature.length !== 71 ||
!/^sha256=[0-9a-f]{64}$/.test(signature)) return false;
if (typeof timestamp !== 'string' || !timestamp.length || timestamp.length > 16) return false;
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || seconds < 0 || String(seconds) !== timestamp ||
!Number.isSafeInteger(nowSeconds) || nowSeconds < 0 ||
!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0 ||
Math.abs(nowSeconds - seconds) > toleranceSeconds) return false;
const expected = createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(Buffer.from(timestamp + '.', 'utf8'))
.update(rawBody)
.digest();
const supplied = Buffer.from(signature.slice('sha256='.length), 'hex');
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}Duplicates and acknowledgements
Return 2xx after durable acceptance, ideally well before the 15-second sender timeout, and process longer work asynchronously. Return failure if the event could not be safely accepted. For an already accepted duplicate, return success without repeating its side effects.
For automatic events, enforce durable, atomic uniqueness on your configured endpoint plus the verified body id. Do not rely on the unsigned delivery-ID header as your only replay check. Manual sends may have no body event ID and need their own authenticated schema and idempotency key. There is no exactly-once or strict-order guarantee.
Retries and recovery
Retry timings are eligibility delays, not exact schedules. Failures across several deliveries can disable an endpoint before one delivery reaches five attempts. Retries retain the delivery ID and original body details but receive a fresh attempt timestamp and signature.
Correct receiver, connectivity, certificate, or secret problems, then edit the webhook and enable Active again to reset consecutive failures. Eligible unfinished deliveries can resume; re-enabling does not reset deliveries already at five attempts, and no exhausted-delivery replay/reset operation exists.
| Behavior | Current implementation |
|---|---|
| Success | Any HTTP 2xx response. |
| Failure | Non-2xx responses; network errors and timeouts are recorded as 503. |
| HTTP timeout | 15 seconds. |
| Maximum attempts | Five total: one initial attempt and up to four retries. |
| Retry delays | Eligible after 1, 2, 4, then 8 minutes from the preceding attempt; workers poll approximately once a minute. |
| Automatic disabling | Five consecutive endpoint failures; any success resets the count. |
Change, pause, or delete an endpoint
Edit the webhook and select Replace signing secret to generate a new secret; copy it when revealed. Saving without replacement retains the secret. There is no old-secret grace period: later attempts, including queued retries, use the current URL and secret. Coordinate changes with your receiver; an in-flight request may still use previous values.
Pausing does not clear the backlog. Delete removes the endpoint’s subscriptions and stored delivery history, not just its active state. Keep required troubleshooting information securely before deletion, or pause to retain history. Deletion cannot withdraw received events or guarantee cancellation of an in-flight send.
Webhook management reference
Use normal authenticated organization access, not an API key. Prefix these routes with /api/organization/integration-operations and any deployment base path. GET requires integrations.view; other operations also require integrations.manage. Use the latest opaque Base64 version for edits and deletion; reload after 409 conflicts. Invalid settings return 400, and access failures can return 401 or 403.
Endpoint summaries include id, name, url, enabled, consecutiveFailures, events, deliveries, and version. Delivery summaries include id, businessEventId, eventType, attemptCount, statusCode, succeeded, errorSummary, and attemptedAtUtc. They show the latest attempt, not a row per attempt. Listings include up to 200 recent deliveries across the organization; the UI shows a recent-delivery count.
Manual delivery requires an active endpoint subscribed to that event. payloadJson must be a string containing valid JSON; it is sent directly without the automatic envelope and has no businessEventId. It does not change a business record. Inspect succeeded in the returned summary even when the management request returns 200, and read the serialization limitation below before using it.
| Method and route | Request | Response |
|---|---|---|
GET /webhooks | — | 200: WebhookEndpointSummary[] |
POST /webhooks | {name, url, enabled, rotateSecret, events, version} | 200: {endpoint, signingSecret} |
PUT /webhooks/{id} | {name, url, enabled, rotateSecret, events, version} | 200: {endpoint, signingSecret} |
POST /webhooks/{id}/deliver | {eventType, payloadJson} | 200: WebhookDeliverySummary |
DELETE /webhooks/{id} | {version} | 204 |
{
"name": "Delivery receiver",
"url": "https://example.com/hooks/allocyn",
"enabled": false,
"rotateSecret": false,
"events": ["WorkItemCompleted", "TimesheetApproved"],
"version": null
}Current webhook limitations
For support, provide the endpoint name, UTC attempt time, delivery ID, event type, and status or error summary. Do not share signing secrets or sensitive payloads.
- ProjectUpdated is selectable, but normal project edits emit different internal event names that are not supported webhook subscriptions. Do not rely on it for project-edit notifications.
- For manual JSON, the sender signs the supplied string and then parses and reserializes it for transmission. Whitespace, non-ASCII text, and escaping can change the bytes. Reject raw-body signature mismatches and report them; do not normalize incoming JSON or disable verification. Reliable arbitrary manual payloads require a sender-side correction.
- Automatic publication scans the previous seven days, no earlier than endpoint creation. It reads the earliest 1,000 events before filtering supported subscriptions. This is not a complete history export or guaranteed backlog; high-volume organizations can have events outside that batch.
- Each retry pass considers the oldest 50 unfinished failed deliveries before checking due times and enabled endpoints. Backlogs can delay eligible work. Downtime, exhausted attempts, volume limits, or pauses can leave gaps: keep independent reconciliation and alerts for critical work.
Common problems
- ProjectUpdated is selectable but is not currently emitted automatically for normal project edits.
- Manually supplied JSON can encounter a signature/body serialization mismatch. Reject mismatches; never bypass verification to make them pass.
- Deleting an endpoint also removes its stored delivery history. Pause it instead if that history must be retained.