Skip to main content

Partner API: Webhooks

Receive document, employee and Auto Policy Update events at your endpoint, with signed at-least-once delivery.

Instead of having AirMason email employees and admins, you can receive every event as an HTTP POST to a URL you control and drive your own notifications. Webhooks are configured per organization.

Configure

PUT /organizations/{orgId}/webhook | Access token

{
"url": "https://hooks.acme.com/airmason",
"events": ["*"],
"disableAirMasonEmails": true
}
{
"url": "https://hooks.acme.com/airmason",
"events": ["*"],
"disableAirMasonEmails": true,
"secret": "whsec_7Hq…",
"createdAt": "2026-09-15T14:00:00.000Z",
"updatedAt": "2026-09-15T14:00:00.000Z",
"secretRotatedAt": null,
"previousSecretValidUntil": null
}
  • url must be https and must not point at localhost or a private network address (400 validation_error). Changing the URL also affects deliveries that are already queued, because the URL is read at send time.

  • events is a list of event types, or ["*"] for everything. At most 50 entries; unknown types are rejected; "*" cannot be combined with explicit types; duplicates are collapsed and the list is stored in a canonical order. A ["*"] subscription receives new event types automatically as they are added, while an explicit list needs the new type added to it.

  • disableAirMasonEmails: true suppresses the AirMason-sent employee and admin emails that your subscribed events replace (see Replacing AirMason emails below): you become responsible for notifying people using the event payloads, which include the notification.title and notification.body AirMason would have used.

  • secret is returned once; use it to verify signatures. The response is 201 when a secret was minted (first configuration, or re-creation after a DELETE) and 200 when an existing webhook was updated in place, in which case the body contains no secret.

GET /organizations/{orgId}/webhook | Access token

Returns the current configuration (never the secret), including previousSecretValidUntil, which is null once a rotation window has passed. Returns 404 when no webhook is configured.

DELETE /organizations/{orgId}/webhook | Access token

Disables the webhook and returns 204. Pending deliveries are marked failed with the error webhook_disabled. A later PUT re-creates the webhook with a new secret.

Secret rotation

POST /organizations/{orgId}/webhook/rotate-secret | Access token

{
"secret": "whsec_9Kp…",
"secretRotatedAt": "2026-09-20T10:00:00.000Z",
"previousSecretValidUntil": "2026-09-21T10:00:00.000Z"
}

The previous secret stays valid for 24 hours. During that window every delivery is signed with both secrets: the X-AirMason-Signature header carries two comma-separated entries, v1=<new>,v1=<previous>. Split the header on ,, trim each entry and accept the request when any entry matches. Outside a rotation window the header is the single v1=<hex> shown below.

Delivery

Each event is delivered as a POST with a JSON body and these headers:

POST /airmason HTTP/1.1
Content-Type: application/json
User-Agent: AirMason-Webhooks/1.0
X-AirMason-Event: employee.signed
X-AirMason-Delivery-Id: dlv_01J8…
X-AirMason-Timestamp: 1758029400
X-AirMason-Signature: v1=5f3a…

The signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex-encoded. Reject requests whose signature doesn't match. For 24 hours after a secret rotation the header lists two signatures (see Secret rotation).

Delivery guarantees:

  • At-least-once. We consider an event delivered when your endpoint returns any 2xx within 10 seconds. Anything else is retried: a non-2xx status (including 3xx, since redirects are not followed), a timeout or a connection error. Response bodies over 64 KB are truncated in the delivery log.

  • Retries use exponential backoff over roughly 24 hours: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, then 24 h after the original attempt. After the final attempt the delivery is marked failed and AirMason is alerted.

  • Idempotency. Retries reuse the same X-AirMason-Delivery-Id. De-duplicate on it: you may occasionally receive the same delivery twice.

  • Ordering is not guaranteed across events; use occurredAt in the payload.

  • Granularity. Employee-level events are sent one per employee: publishing a new version to 400 people produces 400 employee.document_published events, and launching a handbook to 400 people produces 400 employee.handbook_assigned events, so each carries that person's own activation or handbook link.

Inspecting deliveries

GET /organizations/{orgId}/webhook/deliveries | Access token

Filters: status (pending | succeeded | failed), limit (1–500, default 25) and offset. Returns { data, pagination: { limit, offset, total } }; each item looks like:

{
"id": "dlv_01J8…",
"eventId": "evt_01J8…",
"eventType": "employee.signed",
"status": "pending",
"attemptCount": 2,
"lastAttemptAt": "2026-01-16T09:15:39.000Z",
"nextAttemptAt": "2026-01-16T09:20:39.000Z",
"lastResponseStatus": null,
"lastError": "connect ETIMEDOUT",
"isRedelivery": false,
"redeliveredFrom": null,
"createdAt": "2026-01-16T09:14:38.000Z",
"attempts": [
{ "at": "2026-01-16T09:14:38.000Z", "responseStatus": 503, "durationMs": 812 },
{ "at": "2026-01-16T09:15:39.000Z", "error": "connect ETIMEDOUT", "durationMs": 10004 }
]
}

POST /organizations/{orgId}/webhook/deliveries/{deliveryId}/redeliver | Access token

Replays one delivery and returns 202 with the new delivery: same evt_ id, new dlv_ id and a fresh retry chain. Returns 400 while the original delivery is still pending and 404 if the webhook has been disabled.

Payload shape

{
"id": "evt_01J8…",
"type": "employee.signed",
"occurredAt": "2026-01-16T09:14:37.000Z",
"organization": { "id": "1f2a…", "externalId": "acme-eu" },
"notification": {
"title": "Sam Okafor signed Employee Handbook 2026",
"body": "Sam Okafor (sam.okafor@acme.com) signed version 1.1 of Employee Handbook 2026 on 16 Jan 2026."
},
"data": { … }
}

notification.title and notification.body are the plain-text subject and body of the email AirMason would have sent for this event, so you can use them or replace them with your own copy. For employee.handbook_assigned and employee.document_published they are the actual launch, access or publish notice as configured by the organization's admin: the event is the email, minus the sending. data varies by event type.

Objects shared across events (all ids are strings and match the REST resources):

  • handbook — { id, name, slug, status, isLaunched }, where status is draft, published or archived.

  • version — { id, number, label, publishedAt }. number is the raw stored value (110) and label the display form ("1.1").

  • employee — { id, employeeId, email, firstName, lastName, name, source, sourceId }.

Event types

Document (handbook) events

Type

Sent when

Key data fields

document.created

A handbook is created

handbook, source — builder, template, duplicate, upload or addendum

document.launched

A handbook is launched (made available to employees for the first time). A launch also produces one employee.handbook_assigned per assigned employee with reason: launch; it does not produce document.published.

handbook, url

document.published

A version is published. Because a launch is not a publish, a partner subscribing to document.published alone misses the first version.

handbook, version, publishedAt, signatureRequired, acknowledgementRequired, publishType — manual, apu_auto (published automatically by Auto Policy Update), or auto_republish (linked content re-published)

Employee events (one per employee)

Type

Sent when

Key data fields

employee.created

An employee is created (via API, import or HRIS)

employee, source — manual, import, hris, scim or partner_api, activationLink (single-use, valid 7 days; null whenever the employee needs no activation, which includes every employee created by CSV import), activationLinkExpiresAt (authoritative: always use it rather than assuming a fixed window)

employee.handbook_assigned

An employee is given access to a handbook (directly, via group, or on launch)

employee, handbook, version, url, signatureRequired, reason — launch, access_added or auto_welcome, activationLink (when the employee still has to activate)

employee.document_published

A version is published with employee notification on: one event per employee who would have received the "new version" email, deduplicated per version and employee so you see each notice once

employee, handbook, version, url (that employee's handbook link), signatureRequired, acknowledgementRequired, activationLink (when the employee still has to activate)

employee.signed

The employee signs

employee, handbook, version, signedAt, signature { id, signedOn, comment }

employee.acknowledged

The employee acknowledges

employee, handbook, version, acknowledgedAt

employee.signature_reminder

A signature/acknowledgement reminder is due (per the handbook's reminder schedule)

employee, handbook, version, url, daysOutstanding, reminderNumber (reminders already delivered to that employee for that version via the webhook), reminder { subject, isScheduled }

Auto Policy Update events (per handbook)

Type

Sent when

Key data fields

apu.policy_added

A new policy applicable to the organization's purchased regions becomes available for a handbook. Today this is emitted by the daily policy-update run (the morning after the policy is published), not at the moment of publishing.

handbook, policy, summary, why (present when an applicability explanation exists)

apu.policy_updated

Auto Policy Update applies an update to a handbook

handbook, policy, summary, why, changes[] — each { pageId, pageTitle, op, scope }, published (boolean), publishedVersion (set if auto-published)

policy is { id, title, updateType, effectiveDate, summary, regions[] }, where updateType is NEW or UPDATE. regions[] uses the GET /regions shape { code, name, country: { code, name } }, with FEDERAL for federal policies. summary is the policy's plain-text description and is repeated at the top level of data.

Replacing AirMason emails

With disableAirMasonEmails: true, an email is suppressed only when all three conditions hold: the webhook is enabled, the organization is active, and the webhook is subscribed to the event that replaces that email. A webhook subscribed to employee.signed alone still gets welcome and reminder emails sent by AirMason.

AirMason email

Replaced by

Employee welcome / activation

employee.created

Employee launch, access and auto-welcome notices

employee.handbook_assigned

Employee publish notice ("new version")

employee.document_published (subscribing to document.published alone suppresses nothing)

Employee signed confirmation

employee.signed

Employee signature reminder

employee.signature_reminder

Admin daily "new policy updates" email

apu.policy_added

Admin Auto Policy Update auto-apply digest

apu.policy_updated

An admin who belongs to several organizations still receives admin emails for the organizations that are not suppressed. Suppressed sends are recorded in AirMason with the status Suppressed, so support can see what was withheld.


Partner API documentation:

Did this answer your question?