Webhook events
Oclavex delivers events to your HTTPS endpoints so your billing, CRM and automation stay in sync without polling. The contract follows two open standards: the payload is a CloudEvents 1.0 envelope, and delivery is signed per Standard Webhooks.
Subscribing
Section titled “Subscribing”Create a subscription from the portal’s Webhooks page. Register an endpoint URL,
the event types you want, an optional filter, and a signing secret (the portal
generates one; one you supply through the API is whsec_ followed by the base64 of 24
to 64 random bytes); the subscription
starts Pending verification, and Verify runs the
registration challenge that moves it to Active.
Test-fire sends a sample event to your endpoint.
To manage subscriptions programmatically, use the integrator API.
Every event type the platform emits is discoverable, unauthenticated, at
GET /api/v1/webhooks/schemas.
Capture subscriptions
Section titled “Capture subscriptions”A subscription can be created as a capture subscription ("targetKind": "Capture")
instead of the default "Http". A capture subscription has no receiver: it carries no
URL and no signing secret, nothing is ever sent, and it is Active the moment it is
created — there is no registration challenge to run, because there is no endpoint to
prove. Every matching event is recorded in the delivery history with the status
captured, so you can read the exact envelope your endpoint would have been sent, and
settle on event types and filters before you build anything.
Because a capture subscription has no receiver, the operations that act on one do not
apply: Verify succeeds without doing anything, and rotating the secret is refused
with 400 subscription.not_applicable (error 4022).
Callback scopes are refused the same way, on create and on update: nothing is sent, so
no delivery could carry a callback token.
Capture is not available on the integrator API — an integration
registers its own receiver.
Captured deliveries are kept for 7 days, or the 1,000 most recent per capture subscription, whichever limit is reached first. That is deliberately much shorter than the delivery history for real deliveries: a capture is a stepping stone, not a record.
A subscription’s kind is fixed when it is created: editing cannot turn a capture subscription into an HTTP one, because the two have different lifecycles (an HTTP subscription needs a receiver URL and a signing secret, and starts Pending verification). The intended flow is:
- Watch with a capture subscription until the event types and filters are right.
- Create an HTTP subscription with the same settings. In the portal, Create an HTTP subscription from this on the capture subscription opens New subscription pre-filled with its name, events and filters; add the receiver URL and a signing secret, and optionally callback scopes.
- Delete the capture subscription. The portal offers to once the HTTP subscription is created; until you do, it keeps capturing the same events.
Patterns and filters
Section titled “Patterns and filters”A subscription lists exact event types from the event catalog
(Entitlement.Activated), or * for every event, including types added later. There
are no family wildcards: a pattern such as Entitlement.*, an unknown type or the
registration challenge is refused with 400 subscription.invalid_event_type
(error 4035), on create and on update. You can narrow further by entity kind
(entitlement, monitored-asset, …), by severity (for overage events), and
by attribute equality. An event is delivered only if it passes every filter you set.
An attribute-equality filter names fields of the event’s data and the value each one
must hold. This subscription receives a reviewed plan-change request only when the
request was approved and asks for the PRO-2024 plan:
{ "eventTypePatterns": ["Entitlement.PlanChangeReviewed"], "filters": { "attributeEquality": { "approved": "true", "requestedPlanCode": "PRO-2024" } }}- Name a field inside a nested object by its path from
data, joined with dots:billing.providermatches theproviderinside an entitlement event’sbilling. The object itself, an array ornullnever matches, and neither does a field the event doesn’t carry. - Write each field name exactly as it appears in the delivered
data, including its case:requestedPlanCode, notRequestedPlanCode. A capture subscription or the test-fire preview shows you thedataan event carries. - Give each value as a JSON string. It is compared as exact, case-sensitive text: a text
field matches its text as it is, a number matches the way the payload writes it, such
as
"1000"or"0.25", and a boolean matches"true"or"false".
A filter that could never match is refused with 400 subscription.invalid_filter
(error 4034), on create and on update: an empty field name, a
null value, or a field name that none of the subscribed event types carries, which the error
names. Under *, any event type’s fields count. Changing a subscription’s event types checks
again the filters it keeps, so a filter can’t be left behind on events that lack its field.
Integrator API
Section titled “Integrator API”An integration manages its own subscriptions under /api/v1/webhooks/integrator/
with a machine credential holding webhooks.integrator, created under
Settings → API credentials → New integration credentials — see
Manage subscriptions programmatically
for the credential exchange. The environment comes from the token, so there is no
tenant header to set or spoof, and every route is scoped to the subscriptions that
credential created: another integration’s subscription returns 404, never 403.
Paths below are relative to /api/v1/webhooks/integrator/.
| Method | Path | Purpose |
|---|---|---|
GET |
subscriptions |
List your subscriptions; paged, optionally filtered by state. |
POST |
subscriptions |
Register an endpoint. Starts Pending verification. |
GET |
subscriptions/{id} |
Read one subscription. |
PATCH |
subscriptions/{id} |
Change the URL, display name, event-type patterns, filters or callback scopes. A new URL puts the subscription back to Pending verification; verifying the new URL makes it Active, even if it was disabled. |
DELETE |
subscriptions/{id} |
Remove the subscription and its pending deliveries. |
POST |
subscriptions/{id}/verify |
Post a one-time check to the URL; any 2xx activates the subscription. |
POST |
subscriptions/{id}/enable · /disable |
Turn deliveries on or off. A subscription still Pending verification can only be verified or deleted, so both return 409 for it. |
POST |
subscriptions/{id}/secret |
Rotate the signing secret, with an overlap window. |
POST |
subscriptions/{id}/test-fire |
Send one sample event. Body {"eventType": "…"} is optional; returns 202. |
GET |
subscriptions/{id}/test-fire/preview?eventType= |
The exact envelope a test-fire would send. Enqueues nothing. |
POST |
subscriptions/{id}/replay |
Re-deliver past events to this subscription; returns 202. |
GET |
deliveries |
Delivery history across your subscriptions. |
GET |
deliveries/{envelopeId} |
One delivery in full, with every attempt. |
Replay takes either an explicit envelopeIds list or a from/to window —
never both and never neither (400 replay.invalid_selection) — optionally narrowed
by eventType and entityId. It only ever re-sends envelopes that went to the
subscription named in the path, each with a fresh envelope id that names the
original. It has its own hourly ceiling of 200 envelopes, so one integration
can’t spend the whole environment’s replay budget; exceeding it returns
429 replay.rate_limited.
Delivery history carries the same fields the portal’s delivery list shows, with
one difference: the stored request body and the receiver’s response excerpt are
always omitted. Your own receiver already holds everything it was sent, and payload
access isn’t granted on this surface. When an envelope reached more than one subscription,
pass ?subscriptionId= to deliveries/{envelopeId} — otherwise it returns
400 delivery.ambiguous_envelope.
Suspend and resume, the dead-letter queue and capture subscriptions are not part of this surface.
Registration challenge
Section titled “Registration challenge”A subscription doesn’t verify itself — you trigger it by calling
POST …/{id}/verify, which makes Oclavex POST a one-time
Webhooks.Registration.Challenge to your URL. Respond 2xx with a JSON body
echoing the challenge — {"challenge": "<data.challenge>"} — and a
webhook-signature response header signed with your shared secret over
{webhook-id}.{webhook-timestamp}.{your response body}. The subscription becomes
Active only once both the echoed body and the signature verify — this proves
you own the endpoint and hold the secret. Subscriptions move through PendingVerification → Active, and can
later be Disabled or Suspended. Giving a subscription a new URL moves it back to PendingVerification:
the new URL has to answer the challenge too. The receiving side, with code, is in
Receiving webhooks.
The envelope
Section titled “The envelope”Every delivery is a CloudEvents 1.0 JSON document:
{ "specversion": "1.0", "id": "01J…", "source": "urn:oclavex:tenant:…", "type": "Entitlement.Activated", "time": "2026-06-16T10:00:00Z", "subject": "…", "datacontenttype": "application/json", "tenantid": "…", "entitykind": "entitlement", "entityid": "…", "eventversion": 1, "data": { }}data is event-specific and non-strict — ignore fields you don’t recognise, so
new ones never break you. Test deliveries (sent from the portal or the test-fire
endpoint) carry "_test": true in data; gate on it so test events never touch
real state.
Entitlement events carry the keys to route them without looking the entitlement
up first: reference, externalReference, customerId, customerExternalReference,
planSetId, and billing with provider, providerSubscriptionReference and
providerCustomerReference — each only when the entitlement has one, and named as the
entitlement names them in the API.
User and organization events keep a CRM or helpdesk in step with your customers: create
or update the record on Created and Updated, remove it on Deleted. Each carries the
id, externalReference (when the record came from a connected system) and origin —
admin (the portal or the admin API), self-service (sign-up or a profile edit),
privacy (an erasure), or the connected system’s scheme, such as sfdc or stripe. A
CRM can skip events whose origin is its own, so it never echoes its own changes back.
User.CreatedandUser.UpdatedcarryuserType(ConsumerorDevice),email,firstName,lastName,roles,organizationIds,isApprovedandisBlocked.Organization.CreatedandOrganization.UpdatedcarryparentId,organizationType,name,emailDomainandcountry. Invoice details stay out; read them from the API.UpdatedaddschangedFields, and a save that changes nothing sends nothing.User.DeletedcarriesuserId,externalReference,originanduserType;Organization.DeletedcarriesorganizationId,externalReferenceandorigin.
User events describe your customers only: devices, members of an organization, and users
whose only role is license consumer. Your own staff never emit them. A user who stops being a
customer is sent User.Deleted, and User.Created if they become one again, so a mirror
that upserts and deletes stays in step.
Seat events
Section titled “Seat events”Entitlement.SeatActivated and Entitlement.SeatReleased tell you who holds your
customers’ seats without polling. Both carry the entitlement’s routing keys and:
sessionId— the session holding the seat, the same id the release endpoint and the admin API use.deviceFingerprintHash— the SHA-256 of the device fingerprint the license is bound to (itsdev_fpclaim), as lowercase hex. Hash a fingerprint you know the same way to match it; the fingerprint itself is never sent. Absent when the activation carried none.productIds— the products the session licenses.activatedAtUtcorreleasedAtUtc— when the seat was taken or released.
{ "type": "Entitlement.SeatActivated", "entitykind": "entitlement", "entityid": "5f0c2a1e-…", "data": { "entitlementId": "5f0c2a1e-…", "planCode": "PRO-2024", "sessionId": "a8d4e913-…", "deviceFingerprintHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "productIds": ["66666666-…"], "activatedAtUtc": "2026-09-30T08:00:00+00:00", "reference": "PO-2026-0042" }}To follow which devices use a license, key them by sessionId: add the session on
SeatActivated and remove it on SeatReleased. When two events about one session
disagree, the later activatedAtUtc or releasedAtUtc wins, whatever order they
arrived in. A seat whose lease runs out without a release sends nothing, so on a license
with leased seats a device can stop holding its seat without a SeatReleased; if it
comes back, you receive SeatActivated again for the same sessionId. When you need
the exact number of seats in use at a given moment, read the entitlement’s sessions
from the admin API. The same seat change sent twice keeps its envelope id, so
deduplicate on the id as for every event.
Device events
Section titled “Device events”Entitlement.DeviceReset, Entitlement.DeviceBlocked and Entitlement.DeviceUnblocked
report what you do to a device on a customer’s license, so a support desk, CRM or audit
trail follows without polling. All three carry the entitlement’s routing keys and:
deviceFingerprintHash— the SHA-256 of the device fingerprint, as lowercase hex, the same as in the seat events. The fingerprint itself and the name you gave the device are never sent. Absent when there is no fingerprint.
Entitlement.DeviceReset — you reset a device. Its seat is freed, its license ends the
next time it contacts Oclavex and, if it activated with an activation code, it stops
counting against that code’s device limit. Activating again starts a new session. Each
session reset sends one event, also when you reset every device on an entitlement. It
also carries:
sessionId— the session that was reset, the same id as in the seat events.productIds— the products that session licensed.resetAtUtc— when you reset the device.transferCount— the number of resets the entitlement has had so far, this one included. When one request resets several devices, each event carries the total after all of them.
Entitlement.DeviceBlocked and Entitlement.DeviceUnblocked — you block a device so that
no activation admits it, or lift the block. Blocking also ends the device’s current
activations on that entitlement, as a reset does; unblocking does not bring them back, and
the device activates again as a new one. Both also carry:
deviceId— the id of the device’s entry in the entitlement’s device list, the id you use to unblock or remove it. It is not a session id.blockedAtUtcorunblockedAtUtc— when you blocked or unblocked the device.
{ "type": "Entitlement.DeviceReset", "entitykind": "entitlement", "entityid": "5f0c2a1e-…", "data": { "entitlementId": "5f0c2a1e-…", "planCode": "PRO-2024", "sessionId": "a8d4e913-…", "deviceFingerprintHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "productIds": ["66666666-…"], "resetAtUtc": "2026-10-04T09:30:00+00:00", "transferCount": 1, "reference": "PO-2026-0042" }}Resetting or blocking a device that held its seat also sends Entitlement.SeatReleased
for its session, and so does releasing a seat yourself, so a receiver that follows seats
stays correct without these events. Blocking a device that is already blocked and
unblocking one that is not blocked send nothing, and neither does adding or removing a
pre-registered device. The same change sent twice keeps its envelope id, so deduplicate
on the id as for every event.
Verifying a delivery
Section titled “Verifying a delivery”Each request carries the Standard Webhooks headers:
| Header | Meaning |
|---|---|
webhook-id |
The envelope id |
webhook-timestamp |
Send time, in integer Unix seconds |
webhook-signature |
v1, + base64 HMAC-SHA256; several, space-separated, during a rotation |
webhook-event-type |
e.g. Entitlement.Activated |
webhook-delivery-attempt |
Retry counter |
webhook-tenant |
The emitting tenant id |
webhook-subscription |
The subscription id the delivery belongs to |
webhook-callback-token |
Optional short-lived JWT (see below) |
Any Standard Webhooks library verifies a delivery as it is: give it the signing secret,
the headers and the raw body (examples).
The signing secret is whsec_ followed by base64, and the HMAC key is the decoded
bytes after the prefix, not the secret’s text. To check by hand, recompute the
signature over {webhook-id}.{webhook-timestamp}.{raw-body} and compare in constant
time:
key = base64_decode( secret without "whsec_" )signature = "v1," + base64( HMAC_SHA256(key, id + "." + timestamp + "." + body) )Reject anything that doesn’t match, or whose timestamp is more than a few minutes from your clock.
Secret rotation
Section titled “Secret rotation”During a rotation there is an overlap window: Oclavex sends both signatures,
space-separated, in webhook-signature. Accept the delivery if either verifies,
so you can rotate keys with no dropped events.
Calling back
Section titled “Calling back”If your subscription requests callback scopes, each delivery includes a short-lived
JWT in webhook-callback-token. Use it as a bearer token to call straight back into
the API with exactly those scopes — no separate OAuth round-trip. The token is minted
fresh for each attempt and is valid for 15 minutes. It is never stored: the delivery
history records only whether a token was attached, its scopes and its expiry, or that
issuing it failed (the delivery still goes out, without the header).
What a callback token can reach is limited to the integration surface: your catalog
(products, features, tags), licenses and their day-to-day operations, license sessions,
usage, quotas, credit balances, metrics and feature ratings. Account administration,
sign-up, privacy requests and connector settings are never offered, and neither are
delete verbs or wildcards.
Delivery and retries
Section titled “Delivery and retries”Delivery is at-least-once — make handling idempotent by deduplicating on webhook-id. Failed
deliveries (5xx, 408, 429, timeout, DNS) retry on this schedule: up to
10 attempts within a 2-hour budget, exponential backoff with full jitter
from a 2 s base capped at 15 min, and a 30 s per-attempt timeout —
size your endpoint’s processing accordingly. Once retries are exhausted the
delivery is dead-lettered — recorded as failed, kept for inspection, and
requeueable from the portal with a fresh budget — and every attempt is audited.
Event catalog
Section titled “Event catalog”An offer is a template, not an entitlement: publishing, changing or deleting one sends no
Entitlement.* event. Each entitlement ordered from it sends its own.
| Event type | Fires when |
|---|---|
Entitlement.Activated |
An entitlement transitions to Active |
Entitlement.TakenOffline |
An active entitlement is taken offline; its licenses stop activating until it is published again. A licence revoked on the way out sends Entitlement.Cancelled instead |
Entitlement.Expiring |
An entitlement is approaching expiry |
Entitlement.TrialExpiring |
A trial is approaching its end |
Entitlement.Renewed |
A subscription period renews |
Entitlement.Cancelled |
An entitlement is canceled |
Entitlement.Paused |
An entitlement is put on hold — by you, or by a billing event such as a failed payment — and stops issuing licenses; the payload’s reason says why |
Entitlement.Resumed |
A hold is lifted and the entitlement issues licenses again |
Entitlement.PlanApplied |
A plan is applied or changed |
Entitlement.PlanChangeRequested |
A customer’s plan-change request awaits approval — the payload’s requestId lets you approve programmatically, e.g. once a payment succeeds |
Entitlement.PlanChangeReviewed |
A plan-change request is decided — approved (including auto-approval) or rejected |
Entitlement.AddOnGranted |
An add-on is granted to an entitlement; the payload carries the add-on’s code and the grant’s start and end |
Entitlement.AddOnRevoked |
An add-on grant is revoked, or the add-on subscription that granted it ends; the payload’s endsAtUtc says when the add-on stops applying, which for a subscription cancelled at the period end is the end of the paid period. A grant that reaches its end date sends Entitlement.AddOnExpired instead |
Entitlement.AddOnExpired |
An add-on grant reaches its end date and stops applying — including one suspended by a failed payment — without being revoked. expiredAtUtc says when; sent once per end date, so a grant whose end moves (a renewal) is announced again when the new one passes. When a recovered payment puts a suspended add-on back, Entitlement.AddOnGranted is sent again. A periodic check sends it, so it can follow the end by a few minutes |
Entitlement.Deleted |
An entitlement is deleted |
Entitlement.SubscriptionAnchored |
A billing subscription that named no entitlement — one sold in your billing provider’s own dashboard, or posted without targetEntitlementId — lands on an entitlement. subscriptionExternalReference is the subscription as you named it and entitlementId the entitlement that carries it, so you never have to look it up. Sent again on each later change of that subscription; treat the pair as idempotent |
Entitlement.SeatActivated |
A device takes a seat on an entitlement: its first activation, online or offline, or a device taking back a seat it no longer held — after releasing it, or after its lease ran out. sessionId names the session and deviceFingerprintHash the device; see Seat events |
Entitlement.SeatReleased |
A seat is freed: a device releases the seat it held — your application releases the session — or you release, reset or block a device that held a seat. A seat whose lease runs out without a release sends nothing, and taking the entitlement offline, revoking or deleting it sends that event instead of one per seat |
Entitlement.DeviceReset |
You reset a device. Its seat is freed and its license ends the next time it contacts Oclavex; each session reset sends one event, also when you reset every device on an entitlement. sessionId names the session and transferCount the entitlement’s total of resets; see Device events |
Entitlement.DeviceBlocked |
You block a device on an entitlement: its current activations end and no activation admits it until you unblock it. deviceId names the device’s entry in the entitlement’s device list; see Device events |
Entitlement.DeviceUnblocked |
You unblock a device on an entitlement. The activations the block ended stay ended, and the device activates again as a new one; see Device events |
Feedback.Submitted |
An end user submits feedback — a feature request, a problem report or a general comment. securityIssue says whether it is labelled a security issue; a labelled item leaves out its title, body and requesterEmail |
Feedback.Reviewed |
You decide on a feedback item — accepted, declined or marked as a duplicate. A security-labelled item leaves out its title and reviewNote |
MeteredUsage.OverageStatusChanged |
A monitored asset crosses a quota tier (Normal / Warning / Critical / OverUsage) |
CreditBalance.StatusChanged |
A prepaid credit balance crosses a threshold — e.g. running low or depleted — so you can prompt a top-up before work is blocked |
CreditBalance.Deposited |
Credits are added to a balance — a paid purchase, or a deposit you make. amount is what was added and currentBalance the balance after it; origin is admin for your own deposit, or the purchase’s scheme, such as stripe, with externalReference naming the purchase. A balance’s opening credits and its scheduled top-ups send nothing |
User.Created |
A customer user appears — you create one, an end user registers, your CRM syncs a contact, a deleted user is restored, or a user becomes a customer (for example by joining an organization). Devices count, and userType lets you filter them out. Your own staff never emit user events |
User.Updated |
A customer user’s details, roles, access (approved, blocked) or organization membership change; changedFields names what changed. Signing in is not a change |
User.Deleted |
A customer user is deleted, removed by your CRM, or erased on a privacy request, or stops being a customer — removed from their last organization, given a staff role, or left without one when their organization is deleted. Identifiers only — no email |
Organization.Created |
An organization appears — you create one, your CRM syncs an account, or a deleted one is restored |
Organization.Updated |
An organization’s details change, its invoice details included; changedFields names what changed |
Organization.Deleted |
An organization is deleted or removed by your CRM. Identifiers only |
Organization.BillingDetailsChanged |
An organization’s invoice details change — billing email, address, legal name or tax registration — whether you edit them or a connected system such as Stripe or Salesforce does. The payload names the changed fields, not their values. The same change also sends Organization.Updated |
DataSubjectRequest.Completed |
A data-subject request finishes; failedCategories names any part that could not be completed. The payload carries no email |
Tenant.Suspended · .Resumed · .Deleted · .Erased |
An environment on the platform is taken offline or brought back online, deleted, or has its data erased for good. Listed and subscribable only in the platform operator’s environment, and delivered only there — never to the environment the event is about |
Webhooks.Registration.Challenge |
Sent once, to verify a new subscription |