Events
The shared webhook envelope and the payload of all five Insy event types — membership, payment and digital product purchase events.
Insy sends five event types, and every active webhook of an eligible user receives all of them. Each delivery uses the same envelope; only the contents of data differ per type. Branch on event_type and ignore anything you do not handle.
| Event type | Fires when |
|---|---|
membership.created |
A membership is created for a user in a community |
membership.updated |
An existing membership’s active state flips |
payment.success |
A payment completes |
payment.failed |
A payment does not complete |
digital.product.purchase |
A digital product is purchased |
The envelope
Every event has the same four top-level fields.
{
"event_type": "membership.created",
"event_id": "insy_event_1756742400000_k3f9d2a1x8b",
"data": {
"membershipId": "4f9a1c2e-7b3d-4e58-9c01-6d2f8a5b3e17",
"metadata": { "userProfile": {} }
},
"timestamp": "1756742400000"
}
| Field | Type | Description |
|---|---|---|
event_type |
string | One of the five event types. Also sent as the X-Insy-Event-Type header. |
event_id |
string | The event identifier, in the same insy_event_<epochMillis>_<random> form as the X-Insy-Event-Id header. Use it as your deduplication key. |
data |
object | The event payload. Always contains metadata, plus the type-specific fields documented below. |
timestamp |
string | Epoch milliseconds, as a JSON string. |
data.metadata.userProfile
Every payload carries data.metadata.userProfile, the Insy profile of the user the event is about. The examples below abbreviate it as {}. Read the properties you need defensively and ignore the rest; the object is additive.
The data fields listed in the tables below are in addition to metadata.
membership.created
Fires when a membership record is created for a user in a community. Delivered to the webhooks of that community’s owner, admin and moderator members.
Creation is not activation: a membership backed by a not-yet-active Stripe subscription is created with isActive: false, so membership.created can arrive for a member without access. Grant access only when isActive is true, not on the event itself.
| Field | Type | Description |
|---|---|---|
membershipId |
string | Identifier of the membership. Stable across later updates, so it is the right key to store. |
isActive |
boolean | Whether the membership is active now. |
wasActive |
boolean | Whether it was active before this change. |
role |
string | The role the user holds in the community. The values are owner, admin, moderator and member. |
validFrom |
string | Start of the membership window, ISO-8601. |
validUntil |
string | End of the membership window, ISO-8601. Do not assume it is always populated — a lifetime membership has no end date. |
lastPaymentMethod |
string | The payment method most recently used for this membership. |
{
"event_type": "membership.created",
"event_id": "insy_event_1756742400000_k3f9d2a1x8b",
"data": {
"membershipId": "4f9a1c2e-7b3d-4e58-9c01-6d2f8a5b3e17",
"isActive": true,
"wasActive": false,
"role": "member",
"validFrom": "2025-09-01T16:00:00.000Z",
"validUntil": "2025-10-01T16:00:00.000Z",
"lastPaymentMethod": "stripe",
"metadata": { "userProfile": {} }
},
"timestamp": "1756742400000"
}
membership.updated
Fires when a membership’s active state flips — an activation or a lapse. The payload has the same shape as membership.created.
A role change, or a renewal that only moves validUntil while the membership stays active, does not produce a webhook. Poll or re-read the membership if you need those.
| Field | Type | Description |
|---|---|---|
membershipId |
string | Identifier of the membership being updated. |
isActive |
boolean | The state after the change. |
wasActive |
boolean | The state before the change. |
role |
string | The role the user holds in the community. |
validFrom |
string | Start of the membership window, ISO-8601. |
validUntil |
string | End of the membership window, ISO-8601. |
lastPaymentMethod |
string | The payment method most recently used for this membership. |
The pair wasActive and isActive tells you which direction the membership moved:
wasActive |
isActive |
Meaning |
|---|---|---|
false |
true |
Activation — grant access. |
true |
false |
Lapse — revoke access. |
wasActive is always the negation of isActive — every membership event is a transition. There is no “nothing changed” delivery.
Deliveries are unordered and can repeat, so do not toggle a flag on receipt — compare against the state you hold and treat isActive and validUntil from the payload as authoritative.
{
"event_type": "membership.updated",
"event_id": "insy_event_1756831245880_9qz4m7t2v1c",
"data": {
"membershipId": "4f9a1c2e-7b3d-4e58-9c01-6d2f8a5b3e17",
"isActive": false,
"wasActive": true,
"role": "member",
"validFrom": "2025-09-01T16:00:00.000Z",
"validUntil": "2025-09-02T16:00:00.000Z",
"lastPaymentMethod": "stripe",
"metadata": { "userProfile": {} }
},
"timestamp": "1756831245880"
}
payment.success
Fires when a payment completes. Delivered to the webhooks of the community’s owner, admin and moderator members.
| Field | Type | Description |
|---|---|---|
paymentId |
string | Identifier of the payment. Use it, together with the event id, to keep your bookkeeping idempotent. |
membershipId |
string | The membership this payment belongs to. |
paymentStatus |
string | Status of the payment. For payment.success this is always completed — the event type is derived from it. |
paymentMethod |
string | How the payment was made. |
paymentType |
string | What kind of payment it is. The values are subscription, one_time and refund. |
amountCents |
integer | Amount in minor units of currency. 4900 with "currency": "pln" is 49.00 PLN. |
currency |
string | Currency code for amountCents. Case is not normalised — Stripe-settled payments send the lowercase ISO code (pln, usd), crypto settlements may send uppercase. Compare case-insensitively. |
periodStart |
string, optional | Start of the paid period, ISO-8601. |
periodEnd |
string, optional | End of the paid period, ISO-8601. |
durationMonths |
integer, optional | Length of the paid period in months. |
{
"event_type": "payment.success",
"event_id": "insy_event_1756742463127_7hd4p9s2n6r",
"data": {
"paymentId": "b81d5f60-3c2a-4d19-95e7-0af6c3241b8d",
"membershipId": "4f9a1c2e-7b3d-4e58-9c01-6d2f8a5b3e17",
"paymentStatus": "completed",
"paymentMethod": "stripe",
"paymentType": "subscription",
"amountCents": 4900,
"currency": "pln",
"periodStart": "2025-09-01T16:00:00.000Z",
"periodEnd": "2025-10-01T16:00:00.000Z",
"metadata": { "userProfile": {} }
},
"timestamp": "1756742463127"
}
payment.failed
Fires when a payment does not complete. In practice this means one thing: a subscription renewal invoice Stripe could not charge, typically a declined card. Same field names as payment.success, but fewer of them — see the note below.
| Field | Type | Description |
|---|---|---|
paymentId |
string | Identifier of the failed payment. |
membershipId |
string | The membership the payment was for. |
paymentStatus |
string | Status of the payment. For payment.failed this is always failed. |
paymentMethod |
string | How the payment was attempted. Today this is always stripe, since only a Stripe renewal charge produces the event. |
paymentType |
string | What kind of payment it was. Today this is always subscription, for the same reason. |
amountCents |
integer | Amount in minor units of currency. |
currency |
string | Currency code for amountCents. Case is not normalised — see the note under payment.success. |
A failed payment is not, by itself, a loss of access. The membership state is carried by membership.updated: expect a lapse only when a later event reports wasActive: true with isActive: false.
{
"event_type": "payment.failed",
"event_id": "insy_event_1756831188441_c5w8j1r4y0k",
"data": {
"paymentId": "e02b7a94-6d15-42c8-b3f1-58ac0d97e264",
"membershipId": "4f9a1c2e-7b3d-4e58-9c01-6d2f8a5b3e17",
"paymentStatus": "failed",
"paymentMethod": "stripe",
"paymentType": "subscription",
"amountCents": 4900,
"currency": "pln",
"metadata": { "userProfile": {} }
},
"timestamp": "1756831188441"
}
digital.product.purchase
Fires when a digital product is purchased. Unlike the other four, this event is delivered to the webhooks of the product’s creator, not to community moderators.
| Field | Type | Description |
|---|---|---|
membershipId |
string | The membership created for the purchase. |
productVariantId |
string | The specific variant of the product that was bought. |
paymentId |
string | The payment behind the purchase. |
amountCents |
integer | Amount in minor units of currency. |
currency |
string | Currency code for amountCents. This event is always Stripe-settled, so the code is lowercase (pln, usd). |
validFrom |
string | Start of the access window, ISO-8601. |
validUntil |
string | End of the access window, ISO-8601. |
{
"event_type": "digital.product.purchase",
"event_id": "insy_event_1756744812004_m2v6b3t9q7d",
"data": {
"membershipId": "a17c8e42-9f05-4b6d-8e31-27c9a4d05f6b",
"productVariantId": "5d3f0b71-8a24-4c96-a0e5-1b7f62d84c39",
"paymentId": "c94e2d18-7b60-4f53-8a2c-3e15d097ab42",
"amountCents": 12900,
"currency": "pln",
"validFrom": "2025-09-01T16:40:12.004Z",
"validUntil": "2026-09-01T16:40:12.004Z",
"metadata": { "userProfile": {} }
},
"timestamp": "1756744812004"
}
Routing the five types
A single handler receives everything, so the dispatch is a switch with a default branch that does nothing but still acknowledges.
function handle(event) {
switch (event.event_type) {
case "membership.created":
// A created membership is not always active — grant only when it is.
return event.data.isActive ? grantAccess(event.data) : undefined;
case "membership.updated":
return event.data.isActive
? grantAccess(event.data)
: revokeAccess(event.data);
case "payment.success":
return recordPayment(event.data);
case "payment.failed":
return flagDunning(event.data);
case "digital.product.purchase":
return fulfilProduct(event.data);
default:
// Unknown type: acknowledge and move on.
return undefined;
}
}
Signature verification comes first — see Verifying signatures.