Webhooks
Connect a public HTTPS receiver to the events your organisation needs. Ovee signs each notification; your receiver verifies it and uses the related GET API to retrieve current information.
Set up your first subscription
Section titled “Set up your first subscription”- Create an API access key as an organisation administrator.
- Deploy a public HTTPS receiver. Private network addresses, loopback destinations, redirects, URL credentials, and fragments are not supported.
- In Organisation Settings → APIs → Webhooks, create a subscription with your receiver URL and selected event types. You can also use the subscription API below.
- Copy the signing secret once and save it in your receiver’s secret store. It is different from your Ovee API key.
- Select Send test, verify the signature, and check delivery history. Tests make one attempt. Use a real subscribed event when testing retries.
The following reference describes the current delivery contract. See the API reference for request and response schemas.
Manage API access keys and webhook subscriptions under Organisation Settings → APIs, at /org-settings/apis. The previous /org-settings/apiAccessKeys URL redirects there.
Authentication and subscription APIs
Section titled “Authentication and subscription APIs”Use an existing Ovee API Access Key:
Authorization: Bearer <ovee-api-access-key>The key’s bound organisation determines the subscription’s organisation. The caller must still be an admin of that organisation. The UI uses the same endpoints with the signed-in admin’s authentication. Subscriptions belong to the organisation, not to a particular access key.
All endpoints below start with /api/webhooks:
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /capabilities | Delivery/signing configuration status |
| GET | /event-types | Available event names, descriptions and related API templates |
| GET | /subscriptions | List this organisation’s subscriptions |
| POST | /subscriptions | Create a subscription; returns the signing secret once |
| GET | /subscriptions/{id} | Get subscription settings; never returns secrets |
| PATCH | /subscriptions/{id} | Edit name, URL, events, filters or enabled state |
| DELETE | /subscriptions/{id} | Unsubscribe and cancel queued deliveries |
| POST | /subscriptions/{id}/rotate-secret | Generate a new signing secret, returned once |
| POST | /subscriptions/{id}/test | Queue a signed test, including for paused subscriptions |
| GET | /subscriptions/{id}/deliveries?limit=50&before={cursor} | Delivery history; maximum page size 100 |
| GET | /subscriptions/{id}/deliveries/{deliveryId} | Notification payload and attempt history |
| POST | /subscriptions/{id}/deliveries/{deliveryId}/replay | Queue a failed delivery again, retaining the event ID |
POST /api/webhooks/subscriptionsContent-Type: application/jsonAuthorization: Bearer <ovee-api-access-key>{ "name": "Business integration", "url": "https://integration.example.com/ovee-events", "eventTypes": [ "partner.compliance.submitted", "partner.compliance.approved", "partner.compliance.status_changed", "pool.created", "person.association.created", "person.association.type_changed", "job.created", "job.state_changed" ], "associationTypes": ["EMPLOYEE", "TRUSTED"], "enabled": true}associationTypes is optional. Omit it or use [] for all association types. It affects association events only. Supported values are EMPLOYEE, TRUSTED, LINKED, PENDING, and BLACKLISTED. Creation/type changes match the new type; removal matches the previous type.
PATCH replaces any supplied event/filter arrays and preserves omitted fields. At least one supported event is required. PATCH { "enabled": false } pauses the endpoint. PATCH { "enabled": true } resumes it and resets the consecutive failure count. Paused endpoints do not collect new events, while previously queued deliveries remain queued. URL edits apply to queued deliveries as well as future events.
Subscription and delivery IDs from other organisations return 404. No subscription, listing or history response exposes stored signing secrets. Deleting a subscription retains history for the retention period, but prevents replay. An already in-flight request may finish after unsubscribe.
Event catalogue
Section titled “Event catalogue”| Group | Events | Related GET API |
|---|---|---|
| Partner organisations | partner.created, partner.updated, partner.removed | /api/org/{orgId}/partners/{partnerOrgId}/preview |
| Partner onboarding | partner.onboarding.requested, partner.onboarding.accepted, partner.onboarding.declined, partner.onboarding.expired | /api/org/{orgId}/partners/onboarding-requests/{requestId} |
| Compliance completion | partner.compliance.completion_changed | /api/org-compliance/org/{partnerOrgId}?partnershipId={partnershipId} |
| Compliance packs | partner.compliance.submitted, partner.compliance.approved, partner.compliance.conditionally_approved, partner.compliance.rejected | /api/org-compliance/org/{partnerOrgId}?partnershipId={partnershipId} |
| Compliance evidence/status | partner.compliance.evidence_updated, partner.compliance.status_changed | Same compliance API |
| Organisation-owned pools | pool.created, pool.updated, pool.deleted | /api/pool/{poolId} |
| Pool membership | pool.person_added, pool.person_removed | /api/pool/{poolId} |
| Person associations | person.association.created, person.association.type_changed, person.association.removed | /api/org/person/{personId} |
| Onboarding | person.onboarding.started, person.onboarding.completed, person.offboarding.completed | /api/org/person/{personId} or /api/org/person/{personId}/onboarding-records |
| Jobs | job.created, job.updated, job.state_changed, job.request_status_changed, job.deleted | /api/job/{jobId} |
| Job applications/candidates | job.application_submitted, job.candidate_state_changed | /api/job/{jobId}/candidates |
Partner compliance notifications go only to the organisation requesting the evidence. Where both organisations request evidence, each receives notifications for its own direction of the relationship. Evidence notifications cover applicable text, bank-detail and attachment changes; they never contain the evidence itself.
Approval and overall compliance are separate: an approved pack can subsequently become expired. Status is recalculated after relevant changes and checked hourly for date-driven expiry, even when nobody opens Ovee. This uses stored evidence and register results and does not initiate external register lookups.
Association creation means the first of the supported association types was granted. Moving Pending → Trusted → Employee produces type-change notifications. Automatically identified candidates do not count as applications; entering INTERESTED does, including reapplication.
Jobs are scoped to organisation-owned records. Private user/team-owned jobs and private/team pools are excluded. Operational state and recruitment requestStatus have distinct events. updated represents relevant persisted configuration changes, not analytics refreshes or unchanged saves. Only committed changes are notified; intermediate saves within the same transaction are combined. There is no historical backfill when a subscription is enabled. Deletion notifications have identifiers and links, but the referenced GET may subsequently return 404.
Potential future additions include workstream role provisioning, association approval requests/decisions, sharing invitations/acceptance, qualification verification, and upcoming document expiry notifications. These are not currently selectable events.
Notification payload
Section titled “Notification payload”{ "id": "event-uuid", "type": "partner.compliance.submitted", "schemaVersion": 1, "occurredAt": "2026-10-30T01:00:00Z", "orgId": "requesting-org-id", "resource": { "type": "partner", "id": "partner-org-id" }, "data": { "partnerOrgId": "partner-org-id", "partnershipId": "partnership-id", "previousState": "OPEN", "newState": "SUBMITTED_FOR_APPROVAL" }, "links": [ { "rel": "resource", "method": "GET", "href": "/api/org-compliance/org/partner-org-id?partnershipId=partnership-id" } ]}API links contain the actual IDs and query parameters. Resolve href against the same Ovee base URL used by your integration, and fetch it with an API access key for orgId. The GET returns current information, which may have changed since the event occurred. Webhooks do not grant extra API permissions. Bank account details, documents, profile contents, free-text notes and decision reasons are not sent in event payloads.
Signing and receiver behaviour
Section titled “Signing and receiver behaviour”Requests include:
Content-Type: application/jsonX-Ovee-Event-Id: <stable-event-id>X-Ovee-Event-Type: pool.createdX-Ovee-Delivery-Id: <delivery-id>X-Ovee-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>The signing secret is separate from the Ovee API key. Treat the entire returned whsec_... string as the UTF-8 HMAC key. Compute HMAC-SHA256 over the UTF-8 bytes of timestamp + "." + rawRequestBody, and compare hex signatures in constant time. Validate a five-minute timestamp tolerance. Verify the raw bytes before parsing or reformatting JSON.
During signing-secret rotation, the previous secret remains valid for 24 hours and the header contains two v1 signatures. Accept a match against any v1 value using a currently trusted signing secret. Every retry has a fresh signing timestamp; the event ID and event payload remain stable.
The receiver should verify, durably enqueue/deduplicate by event ID, and promptly return any 2xx response. Fetch full records in its own background processing. Delivery is at least once; ordering is not guaranteed. A worker crash after the receiver accepts a request can cause a duplicate. Replay retains the event ID but uses a new delivery ID, so receivers that intentionally reprocess a replay must account for their own deduplication policy.
Asynchronous delivery and retries
Section titled “Asynchronous delivery and retries”The business action only writes an event and delivery entries in its MongoDB transaction. It does not call the remote webhook. A separate bounded worker sends HTTP after commit; receiver latency, timeouts and retries cannot hold up the triggering action. Rollbacks cannot produce deliverable notifications.
Each subscription has at most one active sender lease. Worker leases recover after process failure. Failed requests retry at event-relative offsets of 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours, and 24 hours. Requests have a ten-second overall deadline, including bounded DNS resolution and connection/TLS establishment. A valid Retry-After can delay the next attempt within the 24-hour window.
Five consecutive deliveries exhausting retries pause the endpoint. A successful delivery resets this count. Resume it in APIs settings or through PATCH, then replay failed deliveries as needed. Expired queued delivery windows are recorded as failures without issuing a stale HTTP request. Test deliveries make one attempt and do not affect automatic-pause counters.
Delivery statuses are PENDING, SUCCEEDED, FAILED, and CANCELLED. Retain failed delivery details for diagnosis and replay. Terminal delivery/event history expires after 30 days; pending entries are not expired by retention cleanup.
For the complete partner workflow, see the partner compliance How-to Guide. Onboarding events notify the requesting customer; acceptance is an action for the invited organisation administrator. Completion notifications carry identifiers, completion state and the last recorded completion timestamp. Bank details, document contents and signed storage URLs are never included. Fetch current authorised state after each event; ordering is not guaranteed.
Signature verification example (Node.js)
Section titled “Signature verification example (Node.js)”This helper verifies the signature on the unmodified Buffer received from the HTTP request. Capture raw bytes before JSON middleware. Provide your current signing secret and, during rotation, the previous secret from your secret store.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyOveeSignature(rawBody, header, secrets, now = Date.now()) { if (!Buffer.isBuffer(rawBody) || typeof header !== 'string') return false; const parts = header.split(',').map(part => part.trim()); const timestamps = parts.filter(part => part.startsWith('t=')); if (timestamps.length !== 1) return false; const timestamp = timestamps[0].slice(2); if (!/^\d+$/.test(timestamp)) return false; const seconds = Number(timestamp); if (!Number.isSafeInteger(seconds) || Math.abs(now / 1000 - seconds) > 300) return false; const signatures = parts.filter(part => /^v1=[a-fA-F0-9]{64}$/.test(part)).map(part => Buffer.from(part.slice(3), 'hex')); return secrets.some(secret => { const expected = createHmac('sha256', secret) .update(timestamp + '.', 'utf8') .update(rawBody) .digest(); return signatures.some(signature => timingSafeEqual(signature, expected)); });}After successful verification, parse the JSON, durably enqueue the notification, and deduplicate by its id. Only acknowledge with 2xx after durable acceptance. A duplicate should receive a successful acknowledgement without repeating business effects.
Diagnose delivery problems
Section titled “Diagnose delivery problems”| Symptom | Next step |
|---|---|
| No notification | Check selected event types, optional association filters, organisation ownership, and whether the subscription is enabled. There is no historical backfill. |
| Signature mismatch | Verify the original bytes; use the full whsec_... string as the UTF-8 key, check clock drift, and accept any matching v1 signature during rotation. |
| Test succeeds but real events fail | Inspect delivery history, response statuses, and receiver processing. Tests do not exercise retries. |
| Subscription automatically paused | Fix the receiver, resume the subscription, then replay failed deliveries as needed. |
| Linked GET fails | Use the Ovee application origin, not the receiver origin, with an API key for the event’s organisation. Check resource permissions and whether the record was deleted. |
Do not forward the Ovee API key to a webhook receiver URL. Resolve event resource links against your configured Ovee application origin.