Webhook events

Each webhook event KeyGrant sends: when it is sent, and what its data holds (see The request).

EventSent when
license.issued A key was created.
license.revoked A key was revoked, or its subscription canceled.
license.reinstated A revoked key works again.
license.upgraded A key got an upgrade.
license.upgrade_reverted A refund or a lost dispute took an upgrade back.
license.flagged A purchase is flagged for you to look at.
license.expiring A subscription or timed key expires within 14 days.
license.picked_up An app restored a key by key pickup.
trial.ending A trial key has 3 days or fewer before it expires.
device.activated A device took a seat on a key.
device.released A seat was released.
device.limit_reached A device's activation was refused because every seat was taken.
device.rekeyed A seat moved from a host name to a machine ID.
device.shared A seat was seen on two or more machines.
meter.used_up A meter has no uses left.
meter.overage A device back from offline spent past a meter.
sale.held An upgrade or a timed model's sale changed no key and is held.

Common fields

These mean the same in every event that carries them. Times are in Unix milliseconds.

key

The license key, as issued.

product

The product's API ID.

email

The key's email address, or null when the key has none.

days

The number of whole days left, rounded up. At least 1.

License events

license.issued

Sent when

Issued in the dashboard or over the API, imported, or sold through a Stripe or Paddle checkout.

Data

Common fields: key, product, email, and:

model

The key's type: perpetual, subscription, trial or timed.

modelId

The key's license model, or null.

source

dashboard, api, import or checkout.

expiresAt

When the key expires, or null.

term

A timed key still waiting for its first activation: its { months, days }. Otherwise null.

sale

Only on a key that is one of a sale of several keys: { id, index, size }, so each key's event says which sale it is part of. There is no event for the sale itself.

file

A key sent as a file (a key file or a license file) adds file: { id, kind, filename, mediaType }, never the file itself; fetch it with POST /v1/licenses/:key/file.

Notes

  • Not sent when a checkout upgrades a key the customer already holds (license.upgraded is).

license.revoked

Sent when

A key was revoked, or its Stripe or Paddle subscription was canceled:

  • By hand or over the API.
  • By a full refund or a lost dispute (see Refunds and disputes).
  • Its Stripe or Paddle subscription was canceled.
  • Up to a day after a subscription key's grace period ends or a trial key expires.

Data

Common fields: key, product, and:

email

The key's email address, or null. Absent on the event sent after a grace period or a trial's expiry.

reason

manual, refund, chargeback or subscription_canceled. For a trial key that simply expired, reason is manual.

effectiveAt

Only on a cancellation: when the key will stop working, its expiry plus its grace period.

graceSpent

true, only on the event sent after a grace period or a trial's expiry.

Notes

  • A canceled subscription therefore sends it twice: once when it is canceled, and again when the key stops working.

license.reinstated

Sent when

In any of these cases:

  • A subscription key that was canceled, or revoked after its grace period ran out, because Stripe or Paddle reports its subscription as paid. A key revoked by hand, by a refund or by a dispute is never reinstated this way.
  • A refund that revoked a key fails or is canceled after Stripe took it (see A refund that fails); a failed refund never brings back a key revoked by hand or by a dispute.
  • A chargeback Paddle reverses puts back the keys it revoked (see Selling through Paddle).
  • You put a revoked key back by hand.

Data

Common fields: key, product, email, and:

expiresAt

The key's expiry.

withdrawnFileIds

A key sent as a license file that a refund or a dispute had revoked adds withdrawnFileIds: its own file ids, which leave that flag as the key comes back and which no update of your app should refuse now.

license.upgraded

Sent when

A checkout upgraded a key the customer already holds, or an upgrade was put on a key afterwards:

Data

Common fields: key, product, email, and:

upgrade

What changed: versions, entitlements, devices, uses or term.

session

The Checkout Session ID, or null.

versions

As the key now stands: minMajor, maxMajor (null for "and later") and upgradeUntil.

entitlements, deviceLimit, expiresAt

As the key now stands.

meters

The key's meters as the license object shows them, {} for none.

term

A timed key still waiting for its first activation: its { months, days }. Otherwise null.

license.upgrade_reverted

Sent when

A full refund or a lost dispute took such an upgrade back (see Refunds and disputes). The key stays active.

Data

as license.upgraded

The same fields as license.upgraded, the key as it stands after.

reason

refund or chargeback.

uses

Only when a top-up is taken back, which takes back only the uses not yet spent: by meter code, each { added, takenBack, kept }, kept being what was already spent.

license.flagged

Sent when

KeyGrant flags a purchase for you to look at (see Repeat refunds and duplicate purchases). The kind says why:

  • repeat-refund: a checkout created a key whose buyer holds an earlier key of the same product, for the same major version, that a refund or a dispute took back.
  • duplicate: a checkout created a key whose buyer holds an earlier key of the same product, for the same major version, bought under the same license model in the 7 days before.
  • refunded-file: a refund, a lost dispute, or a revoke whose reason is refund or chargeback, revokes keys sent as license files, which keep working offline.

Data

Common fields: product, email, and:

id

The flag's ID.

key

The purchase's first key.

kind

repeat-refund, duplicate or refunded-file.

earlierKeys

The keys it was flagged against, newest first.

sale

{ id, size } for a sale of several keys, or null. For a refunded-file flag, sale.size is the number of keys that refund revoked.

flaggedAt

When it was flagged. Sent again, its flaggedAt is when it was flagged again.

fileIds

Only on a refunded-file flag: the ids to list in your app's revokedLicenseFiles in its next update.

Notes

  • For repeat-refund and duplicate, a key not yet bound to a major version counts as any. The key was created as usual, and license.issued was sent first. One per kind.
  • One flag per purchase: while it still names a file, a key of it refunded again, or another key of the sale refunded later, adds its ids, and the flag is sent again under the same id, its fileIds every id it names now.
  • A flag whose keys were all put back is gone, so a later refund usually sends a new flag with a new id.

A license.flagged event's data:

Data
{  "id": "flg_4f1c9a0e2b7d4c6f8a1b3e5d7c9f0a2b",  "key": "SLUICE-7K2M-Q9XD-4HTR-WB6N",  "product": "sluice",  "kind": "repeat-refund",  "earlierKeys": ["SLUICE-3HVW-7RNC-LD2B-M6PQ"],  "sale": null,  "email": "[email protected]",  "flaggedAt": 1791504000000}

license.expiring

Sent when

A subscription key that is not canceled, or a timed key, has 14 days or fewer before it expires.

Data

Common fields: key, product, email, days, and:

model

subscription or timed.

expiresAt

When the key expires.

Notes

  • Sent once per key for each expiry date.

license.picked_up

Sent when

An app restored a customer's keys by key pickup: the customer typed the code KeyGrant emailed to the address they bought with, and KeyGrant handed that address's keys of the product to the app.

Data

Common fields: key, product, and:

email

The address the customer gave.

keys

How many keys the same pickup handed out.

Notes

  • One event for each key handed out.
  • The app then activates one of them as usual, which sends device.activated.

trial.ending

Sent when

A trial key has 3 days or fewer before it expires.

Data

Common fields: key, product, email, days, and:

expiresAt

When the key expires.

Notes

  • Sent once per key for each expiry date.
  • Not sent for device trials, which have no key.

Device events

device.activated

Sent when

A device took a seat on a key.

Data

Common fields: key, product, and:

activationId

The seat it took.

fingerprint

The device's fingerprint.

Notes

  • Not sent again when a device that already holds a seat activates again.

device.released

Sent when

In any of these cases:

  • By the app, in the dashboard or over the API.
  • By your customer in the customer portal.
  • By KeyGrant, for a device that went past its license's days without checking in (see Free unused seats).

Data

Common fields: key, product, and:

activationId, fingerprint

The seat, and its device's fingerprint.

reason

Only on a seat KeyGrant freed: inactive.

lastSeen

Only on a seat KeyGrant freed: the device's last check-in.

Notes

  • The events for seats KeyGrant freed go out within minutes of the nightly pass that frees the seats.

A device.released event's data when KeyGrant freed the seat:

Data
{  "key": "SLUICE-7K2M-Q9XD-4HTR-WB6N",  "product": "sluice",  "activationId": "act_3f9c2a71d04b4e8a9c6d1e2f",  "fingerprint": "5e1f0ca7d00d4a37b9c2",  "reason": "inactive",  "lastSeen": 1788912000000}

device.limit_reached

Sent when

A device's activation was refused because every seat was taken.

Data

Common fields: key, product, and:

deviceLimit

The key's device limit.

device.rekeyed

Sent when

A seat activated under its device's host name moved to the device's machine ID.

Data

Common fields: key, product, and:

activationId

The seat.

from, to

The two fingerprints' hashes, never the fingerprints.

Notes

  • Sent once per seat.

device.shared

Sent when

A seat was seen on two or more machines within 30 days (see Device management).

Data

Common fields: key, product, and:

activationId

The seat.

machines

How many machines it was seen on, up to 20.

refused

How many of those machines were never accepted.

Notes

  • Sent at once the first time, then at most once an hour per seat when the count has grown.

Meter events

For a key whose license model counts uses (see Metered uses). They are sent in the background and never fail a spend or a check-in.

meter.used_up

Sent when

A spend, a report of what a device spent offline, or a spend from your backend took its last use.

Data

Common fields: key, product, email, and:

meter

The meter's code.

total, used

The meter's uses with its extra, and what was used.

periodEnd

The end of the billing period, or null.

activationId

The device that spent them, or null for a spend from your backend.

source

app, report or api.

Notes

  • Sent once each time a meter goes from having uses to having none, so a top-up, an add, a reset or an edit that gives it uses again lets it be sent again.
  • This is the moment to offer a top-up. KeyGrant sends the customer no such nudge.

meter.overage

Sent when

Its report of spends took a meter past what the key holds. The device spent from its offline allowance while KeyGrant could not be reached, and was never refused for it.

Data

Common fields: key, product, email, and:

meter

The meter's code.

total, used

The meter's uses with its extra, and what was used.

overage

How far over its uses the meter now stands, all of it, not what this report added.

reported

The uses this report counted toward the meter.

allowance

The device's offline allowance.

activationId

The device that reported.

periodEnd

The end of the billing period, or null.

Notes

  • Sent once for each report that grew the overage, which is at most one a check-in.

Sale events

sale.held

Sent when

A paid upgrade sale, or a timed model's purchase, changed no key: KeyGrant held it, and it waits for you to attach it to a key or refund it. Each kind's causes are linked from Held sales.

Data

Common fields: product, and:

id

The held sale's ID.

modelId

The license model sold.

session, paymentIntent

The Checkout Session ID and the Stripe payment; for a Paddle sale, the transaction ID (txn_…) in both.

email, customer

The buyer's address and their customer at Stripe or Paddle.

reason

Why it was held:

  • no-key: no key it can change was found.
  • several-keys: more than one key could take it, and the checkout named none.
  • already-upgraded: the key already holds what it sells.
  • not-eligible: the key found or named cannot take it.
  • quantity: how many it bought does not fit the key's seats.
  • basket: it was bought with other items, so its line cannot be told.
  • lines-unread: Stripe or Paddle would not give the checkout's lines.
  • over-cap: the devices it adds would take the key past 1,000.
  • ended: a timed model's date had passed.
kind

What it sells: entitlements, versions, devices, uses or term.

upgradeFrom, upgradeTo

An edition upgrade's entitlements: the set a key must hold, and the set that replaces it.

quantity

How many it bought at once.

quantityRead

false when the checkout did not say how many it bought: quantity, and devices, are then one unit's worth.

keySeats

The seats of the key it found, as the reason quantity compares them.

amountCents, currency

What it charged, in minor units, and its currency.

versions

A version upgrade's sold versions (minMajor, maxMajor, null for "and later", upgradeUntil), or null.

devices

What a device upgrade adds in all, or null.

uses

What a top-up adds in all, by meter code, or null.

term

What a timed sale adds, { months }, { days } or { endsAt }, with keys when one purchase sells several, or null.

Notes

  • No key exists for it, so it carries none.

What sends no event

  • An ordinary subscription renewal.
  • A failed renewal.
  • An extension.
  • A change of versions made by hand or over the API.
  • A change of entitlements made by hand or over the API.
  • A change of meters made by hand or over the API.
  • A device trial.