Skip to content
Oclavex is in private preview, available to selected users. Public signup is blocked. Request access at [email protected].

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.

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.

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:

  1. Watch with a capture subscription until the event types and filters are right.
  2. 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.
  3. Delete the capture subscription. The portal offers to once the HTTP subscription is created; until you do, it keeps capturing the same events.

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.provider matches the provider inside an entitlement event’s billing. The object itself, an array or null never 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, not RequestedPlanCode. A capture subscription or the test-fire preview shows you the data an 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.

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.

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.

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.Created and User.Updated carry userType (Consumer or Device), email, firstName, lastName, roles, organizationIds, isApproved and isBlocked.
  • Organization.Created and Organization.Updated carry parentId, organizationType, name, emailDomain and country. Invoice details stay out; read them from the API.
  • Updated adds changedFields, and a save that changes nothing sends nothing.
  • User.Deleted carries userId, externalReference, origin and userType; Organization.Deleted carries organizationId, externalReference and origin.

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.

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 (its dev_fp claim), 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.
  • activatedAtUtc or releasedAtUtc — 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.

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.
  • blockedAtUtc or unblockedAtUtc — 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.

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.

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.

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 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.

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