Webhook events
The events Transakt sends to your endpoints, and their payloads.
Webhook endpoints belong to an API client and receive the event types they subscribe to. subscription.* and seat.* events arrive only for products the API client may read; customer.created only for API clients with access to all your products. Every message is signed: see Verifying webhooks.
Envelope
{
"id": "msg_0193a8f2c4b17d3e9f0a1b2c3d4e5f60",
"type": "subscription.plan_changed",
"timestamp": "2026-10-01T08:15:02.123Z",
"data": {}
}| Member | Meaning |
|---|---|
id | The message id, equal to the webhook-id header; the same on retries and replays. |
type | The event type (below). |
timestamp | When the change was committed (UTC), not when this attempt was sent. |
data | The event data: identifiers, your product, plan and edition slugs, the customer's tenant id, the subscription and an entitlement snapshot. |
Payloads carry identifiers only: no names, email addresses, keys, tokens or payment data. Re-read anything else through the product API. Bodies are UTF-8 JSON of at most 64 KiB, with the conventions of the API (camelCase, PascalCase enumeration values, UTC timestamps, nullable members always present).
Event types and data
The reference below is generated from the webhooks map of the product API document (GET /openapi/product.json), so it always matches what the platform sends: one section per event type with the Standard Webhooks headers, the body schema and an example. Receivers can be generated from the same document as your entitlement client.
An event is written in the same transaction as the change, so every change produces its event whichever way it was made, and a change that is not applied (a duplicate notification, for example) produces none. Moving a plan to another edition, or reordering editions, sends no event. The entitlement member of subscription and seat events is the tenant's entitlement for the product as of the change, exactly as the entitlement API returns it.
| Type | Sent when |
|---|---|
subscription.activated | A subscription becomes Active for the first time, or again after it ended: Marketplace activation, key redemption, online purchase, manual activation or resumption. |
subscription.plan_changed | The plan changed: a lifecycle operation, a billing change, a scheduled change applied at the end of the period, or a reconciliation with Microsoft Marketplace. previousPlan and previousEditionSlug carry the plan before the change (previousEditionSlug equals editionSlug for a lateral change). |
subscription.quantity_changed | The quantity (licences) changed. previousQuantity carries the quantity before the change. |
subscription.suspended | The subscription was suspended: by Microsoft, or after failed payments and the grace period. |
subscription.reinstated | A suspended subscription became Active again. |
subscription.renewed | The validity was extended for a new term: a Marketplace or online renewal, a key redeemed on an existing subscription, a manual extension. |
subscription.cancelled | The subscription ended as Cancelled: a cancellation, a Marketplace unsubscribe, the end of a period with renewal off, or non-payment. |
subscription.expired | A non-Marketplace subscription passed its end date. |
seat.assigned | A seat was given to a user. |
seat.revoked | A seat was taken back, including when a subscription ends. |
customer.created | A new customer organisation was created in your workspace. Delivered only to endpoints of API clients with access to all your products. |
webhook.test | You pressed Send test event in the Admin Portal; sent to that endpoint only. |
A subscription becomes Active for the first time, or again after it ended: Marketplace activation, key redemption, online purchase, manual activation or resumption.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.activatedThe plan changed: a lifecycle operation, a billing change, a scheduled change applied at the end of the period, or a reconciliation with Microsoft Marketplace. previousPlan and previousEditionSlug carry the plan before the change (previousEditionSlug equals editionSlug for a lateral change).
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.plan_changedThe quantity (licences) changed. previousQuantity carries the quantity before the change.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.quantity_changedThe subscription was suspended: by Microsoft, or after failed payments and the grace period.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.suspendedA suspended subscription became Active again.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.reinstatedThe validity was extended for a new term: a Marketplace or online renewal, a key redeemed on an existing subscription, a manual extension.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.renewedThe subscription ended as Cancelled: a cancellation, a Marketplace unsubscribe, the end of a period with renewal off, or non-payment.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.cancelledA non-Marketplace subscription passed its end date.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"subscription.activated""subscription.plan_changed""subscription.quantity_changed""subscription.suspended""subscription.reinstated""subscription.renewed""subscription.cancelled""subscription.expired"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of every subscription.* event.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/subscription.expiredA seat was given to a user.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"seat.assigned""seat.revoked"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of seat.assigned and seat.revoked.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/seat.assignedA seat was taken back, including when a subscription ends.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*stringThe event type.
"seat.assigned""seat.revoked"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of seat.assigned and seat.revoked.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/seat.revokedA new customer organisation was created in your workspace. Delivered only to endpoints of API clients with access to all your products.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*"customer.created"The event type.
"customer.created"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of customer.created.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/customer.createdYou pressed Send test event in the Admin Portal; sent to that endpoint only.
webhook-id*stringThe message id: msg_ followed by 32 hexadecimal characters. The same on every attempt and every replay; deduplicate by it.
webhook-timestamp*stringUnix time in seconds at this attempt (each attempt is signed afresh). Reject a timestamp more than 5 minutes from your clock.
webhook-signature*stringv1, followed by the base64 HMAC-SHA-256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the decoded secret (the base64 after whsec_). Two space-separated signatures during a secret rotation.
The stored body bytes, UTF-8 JSON of at most 64 KiB, signed as {webhook-id}.{webhook-timestamp}.{body}.
application/json- body
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
id*stringThe message id, equal to the webhook-id header.
^msg_[0-9a-f]{32}$type*"webhook.test"The event type.
"webhook.test"timestamp*stringWhen the change was committed (UTC), not when this attempt was sent.
date-timedata*The data of webhook.test.
Acknowledged. Any 2xx within 10 seconds counts as delivered; answer before doing the work.
Example Requests
The webhook body (6.8.2): the message id, the event type, when the change was committed, and the event data.
/webhook.testOrdering and versions
Delivery is at least once and unordered. Each subscription event carries subscription.version, which increases with every committed change of the subscription; entitlement.version carries the same number for the tenant and product. Keep the highest version you have applied per subscription and ignore lower ones, or re-read the entitlement.
Versioning
Event payloads change only additively: new members, new enumeration values and new event types (delivered only to endpoints that subscribe to them). Ignore unknown members and treat unknown accessLevel or licenceOutcome values as the most restrictive. A breaking change publishes a new event type (for example subscription.plan_changed.v2) next to the old one, which keeps being delivered for at least 12 months.
Prove control of an Entra application POST
Activates a pending Entra application credential of an API client. Call it from the application with a client-credentials token for the Transakt API and the one-time challenge shown in the Admin Portal (valid 24 hours). The token must be issued in the tenant you recorded for the application. Errors: [`auth.credentials_invalid`](/errors/auth.credentials_invalid/), [`api_client.application_verification_failed`](/errors/api_client.application_verification_failed/), [`auth.forbidden`](/errors/auth.forbidden/), [`request.rate_limited`](/errors/request.rate_limited/).
Errors
Every error code the API can return, with its status and meaning.