Skip to content

Webhook Events

Plirin sends webhook events to your configured endpoints when significant actions occur. This lets you automate workflows without polling the API.

EventTrigger
payment.confirmedAn on-chain transaction is confirmed
payment.expiredA payment link or transaction window expires
payment.revertedA confirmed payment was contradicted by the chain and demoted. Act on this one. See below
payment.dispute_closedA card dispute was decided at Stripe. outcome is won or lost; a lost one settles a chargeback credit note for the money the client kept (credit_note_id, null when the invoice had nothing left to credit, and credit_note.issued and credit_note.settled fire for it). The payload also carries invoice_id, provider_payment_id, dispute_id, amount, currency, rail and livemode
invoice.createdA new invoice is created (an imported invoice carries "source": "imported")
invoice.sentAn invoice is marked sent (email send is attempted; fires even if delivery fails)
invoice.paidAn invoice’s balance reaches zero: on-chain, offline, by card, or a mix (each item in payments[] carries its rail: stablecoin, offline or card)
invoice.overdueAn invoice passes its due date without full payment
invoice.partial_paymentA partial payment is received against an invoice
invoice.payment_recordedAn offline payment is recorded or restored against an invoice
invoice.payment_removedAn offline payment is removed (soft-deleted) from an invoice
invoice.voidedAn invoice is cancelled
invoice.late_fee_appliedAn automatic late fee was charged against an overdue invoice as a new linked invoice (fee_invoice_id, sequence, amount in the payload); the original invoice is unchanged
invoice.late_fee_removedA merchant removed a late fee that had been charged: the fee invoice (fee_invoice_id) is now void and its sequence is never reused (invoice_id, sequence, amount, currency, removed_at and the merchant’s reason, or null, in the payload). Fires only for a fee nobody has paid; a paid fee is refunded or credited instead
estimate.sentAn estimate is sent to a customer (also fires when a revised estimate is sent again; revision in the payload says which)
estimate.acceptedA customer accepts an estimate on its hosted page (a merchant’s “Convert now” fires only estimate.converted). The payload’s status is already converted, because accepting creates the invoice in the same step
estimate.declinedA customer declines an estimate on its hosted page (decline_reason is null when none was given)
estimate.expiredA sent estimate passes its expires_on date, counted on the merchant’s calendar, without a decision
estimate.convertedAn estimate becomes a draft invoice, by customer acceptance or the merchant’s “Convert now” (invoice_id in the payload)
estimate.voidedAn estimate is voided by the merchant
credit_note.createdA credit note is created in draft state
credit_note.issuedA credit note is issued
credit_note.settledA credit note reaches settled state
refund.requestedA refund request is submitted
refund.approvedA refund request is approved
refund.rejectedA refund request is rejected
refund.completedA refund is completed: on-chain, at Stripe for a card refund (including one made in the Stripe dashboard), or recorded (rail and credit_note_id in the payload)
refund.failedA card refund failed at Stripe, at once or after first succeeding, or Plirin found a refund with an unknown outcome was never made while the same money had since been refunded in Stripe (CREDIT_OVER_COMMITTED), or an operator found Stripe never made a refund whose outcome stayed unknown after repeated attempts (STRIPE_NEVER_CONFIRMED); its credit note is void and the balance restored (rail, credit_note_id and Plirin’s failure_code in the payload)
refund.voidedA recorded refund (an offline refund made outside Plirin) was voided as entered by mistake; its credit note is void with it and the balance restored. Undo the refund.completed you booked for it (rail, invoice_id and credit_note_id in the payload)
customer.createdA new customer record is created
subscription.createdA merchant issues a subscription checkout link
subscription.activatedA checkout link’s wallet clears screening and the subscription goes live (trialing or active)
subscription.renewedAn invoice for a subscription’s period is paid (not when it is sent, see invoice.sent for that)
subscription.payment_failedA subscription charge attempt fails
subscription.past_dueA subscription failed a charge and has a retry scheduled
subscription.on_holdA subscription exhausted its retry schedule; act on this one
subscription.canceledA subscription is canceled
subscription.mandate_insufficientA period’s charge exceeded the subscription mandate’s authorized ceiling, or the merchant’s receiving wallet changed since the mandate’s last confirmed collection; either way that period is invoiced instead of collected automatically (see reason in the payload)
subscription.mandate_revokedA subscription mandate can no longer collect, either the customer revoked it on chain or a merchant staff member revoked it; the subscription falls back to sending invoices rather than being cancelled
subscription.mandate_expiringA mandate has two or fewer authorized periods left; re-authorize before it runs out

The one event that can contradict something Plirin already told you.

Every other payment event moves a payment forward. This one takes it back: a payment that Plirin confirmed has been demoted because an independent on-chain re-check found that the money never actually moved. The usual cause is a chain reorganisation that displaced the block carrying the transaction.

{
"event": "payment.reverted",
"data": {
"transaction_id": 1234,
"reference": "TX_ABC123",
"previous_status": "confirmed",
"status": "reverted",
"reason": "chain_attestation_failed"
}
}

What to do. Treat it as you would a chargeback on a card rail: the goods may already have shipped. Reverse whatever payment.confirmed triggered — unfulfil the order, revoke the licence, suspend the account — and reconcile with your own records.

It is rare, and deliberately conservative. It fires only when the chain positively contradicts the payment. An unreachable RPC or a temporarily degraded node produces no event at all: those are retried, never treated as evidence that a payment failed. You will not receive this because of an outage.

It is terminal. A reverted payment is never re-confirmed. If the same customer pays again, that is a new payment with a new reference.

You are subscribed by default. An endpoint that does not name a specific event list receives every event, so existing integrations will start receiving payment.reverted without any change on their side. This is intentional: an event that says money you were told had arrived did not arrive is not one to require opting into. If your handler rejects unknown event types, add a case for it.

A client’s card payment is not an on-chain transaction, so it does not send payment.confirmed. Listen for these instead:

What happenedEvent
A card payment settled the invoiceinvoice.paid, with a payments[] entry whose rail is card and whose reference is the Stripe PaymentIntent ID. Read rail rather than type (see Payload Format)
A card payment was refundedrefund.completed (refund.requested, refund.approved and refund.failed on the way), with rail set to card
A client disputed a card payment and the dispute was decidedpayment.dispute_closed

Three things follow from how card payments work. A card payment that is only started or still processing sends nothing: act on invoice.paid. A card payment that arrives after the invoice was voided sends no invoice.paid, because the invoice stays void; it is kept on the invoice with needs_refund_reason (see the SDKs), as is one that takes the invoice past its total. And unlike a stablecoin payment, a card payment can be charged back, so treat a card invoice.paid as money that can still be reversed by a dispute.

A refund goes back the way the money came, so every refund.* payload says which way that was. A card payment can be disputed and charged back; a stablecoin payment cannot, so payment.dispute_closed only ever describes a card payment.

refund.requested, refund.approved, refund.rejected, refund.completed, refund.failed and refund.voided share one payload. refund.failed adds failure_code.

FieldTypeDescription
refund_idintegerPlirin’s refund ID. A recorded refund is voided by this ID
refund_referencestringThe refund’s public reference
amountdecimal stringThe refund amount, in currency
currencystringUppercase ISO-4217 code of the refund
statusstringThe refund’s status when the event fired: pending, approved, rejected, completed, failed or voided
railstringHow the money goes back: stablecoin (on-chain), card (through Stripe) or offline (a refund you paid yourself and recorded, so no money moved through Plirin)
invoice_idstring or nullThe invoice refunded. Null for a refund that is not linked to an invoice
credit_note_idstring or nullThe credit note this refund settles, on every event for the refund including refund.requested, on-chain or card. Null for an overpayment refund (money returned beyond the invoice total, which creates no credit note)
transaction_idinteger or nullThe on-chain payment refunded. Null for card and offline refunds
transaction_hashstring or nullThe on-chain refund transfer. Null until an on-chain refund completes, and always null for card and offline

credit_note.created, credit_note.issued and credit_note.settled carry the same rail (null for a credit note with no refund).

For a refund made in Plirin the events arrive in this order: refund.requested (awaiting approval), refund.approved (for a card refund, Plirin sent it to Stripe; the payload’s status is approved), then refund.completed once Stripe confirms it, or refund.failed. If Stripe refuses a card refund at once, you receive refund.approved and then refund.failed. If Plirin cannot tell whether Stripe made the refund (for example, the request timed out), you receive refund.approved, and refund.completed or refund.failed follows once Plirin confirms the outcome with Stripe. Rarely, when Stripe reports the result in the same moment Plirin records the approval, refund.approved is not sent and refund.completed or refund.failed arrives on its own. Treat refund.approved as informational and act on refund.completed and refund.failed. A recorded refund completes as it is recorded and sends refund.completed alone. A card refund made directly in the Stripe dashboard is picked up from Stripe: it emits refund.completed (or refund.failed) when Stripe settles it, and no refund.requested or refund.approved.

A refund completed. On-chain, the transfer is confirmed. For a card refund, Stripe confirmed it, which includes a refund made in the Stripe dashboard. A recorded refund completes at the moment it is recorded.

{
"event": "refund.completed",
"data": {
"refund_id": 31,
"refund_reference": "REF_3F9A1C7D52B0",
"amount": "40.00",
"currency": "USD",
"status": "completed",
"transaction_id": null,
"transaction_hash": null,
"rail": "card",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"credit_note_id": "7a3f1b20-5c4e-4d8a-9f2b-1e6c0a9d3e87"
}
}

A card refund failed at Stripe. It can fail at once (Stripe refused it) or after first succeeding. The refund’s credit note is void and the invoice’s balance is restored, so undo anything you booked for it. failure_code is Plirin’s code, never Stripe’s text:

failure_codeMeaning
CARD_LOST_OR_STOLEN, CARD_EXPIRED_OR_CANCELEDThe card can no longer receive the money
REFUND_DECLINEDThe card issuer declined it
CHARGE_DISPUTEDThe payment is under dispute
INSUFFICIENT_FUNDSThe Stripe balance could not cover it
ALREADY_REFUNDEDThe payment had already been refunded in Stripe
REFUND_EXCEEDS_REFUNDABLEStripe holds less of the payment than the refund
CANCELED_BY_MERCHANT, REFUND_CANCELEDThe refund was canceled in Stripe
CREDIT_OVER_COMMITTEDA refund whose outcome was unknown was never made, because the same money had since been refunded in Stripe. Nothing more was sent
STRIPE_REFUSED, REFUND_FAILEDStripe refused it, or could not complete it, for another reason
{
"event": "refund.failed",
"data": {
"refund_id": 31,
"refund_reference": "REF_3F9A1C7D52B0",
"amount": "40.00",
"currency": "USD",
"status": "failed",
"transaction_id": null,
"transaction_hash": null,
"rail": "card",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"credit_note_id": "7a3f1b20-5c4e-4d8a-9f2b-1e6c0a9d3e87",
"failure_code": "CARD_EXPIRED_OR_CANCELED"
}
}

A refund request was rejected by a manager or the owner. Nothing was sent, the invoice stays paid, and the refund’s credit note is void. The payload is the standard refund payload with status set to rejected.

{
"event": "refund.rejected",
"data": {
"refund_id": 32,
"refund_reference": "REF_B07E44D91A3C",
"amount": "250.00",
"currency": "USD",
"status": "rejected",
"transaction_id": null,
"transaction_hash": null,
"rail": "card",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"credit_note_id": "9c0d27e4-3b1a-4f68-8a2e-5d7b61f40a12"
}
}

A recorded refund was voided: you recorded that you had paid a customer back outside Plirin, and it was entered by mistake. Its credit note is void with it and the invoice’s balance is restored. Undo whatever you booked for the refund.completed of the same refund_id. Only a recorded refund (rail is offline) can be voided: a card refund that went to Stripe, and an on-chain transfer, cannot be taken back.

{
"event": "refund.voided",
"data": {
"refund_id": 33,
"refund_reference": "REF_5D21E8A0C7F4",
"amount": "45.00",
"currency": "USD",
"status": "voided",
"transaction_id": null,
"transaction_hash": null,
"rail": "offline",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"credit_note_id": "4e8a91c3-6d2f-4b05-b7a9-0c1f3e5d8a26"
}
}

A customer’s dispute of a card payment was decided at Stripe. It fires once per dispute, however many times Stripe reports the result.

FieldTypeDescription
outcomestringwon or lost
invoice_idstring or nullThe invoice the card payment was for
provider_payment_idstringThe Stripe PaymentIntent ID of the card payment
dispute_idstringThe Stripe dispute ID
amountdecimal stringThe disputed amount, in currency
currencystringUppercase ISO-4217 code
railstringAlways card today
livemodebooleanWhether the payment was live or a test-mode payment
credit_note_idstring or nullFor lost, the chargeback credit note that settles the money the customer kept. Null for won, and null for a lost dispute when the invoice had nothing left to credit

A won dispute changes nothing on the invoice: it stayed paid while the dispute was open. A lost dispute creates a settled chargeback credit note for the disputed amount, so the invoice stays paid and the books record that the money went back to the customer. credit_note.issued and credit_note.settled fire for that note, with rail null, because a chargeback is not a refund. There is no webhook for a dispute opening: Plirin emails the merchant when it opens, with the deadline to respond in Stripe.

{
"event": "payment.dispute_closed",
"data": {
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"provider_payment_id": "pi_3Px9aQ2eZvKYlo2C0f1d4a7b",
"dispute_id": "dp_1Px9bR2eZvKYlo2CpL5m8n0q",
"outcome": "lost",
"amount": "120.00",
"currency": "USD",
"rail": "card",
"livemode": true,
"credit_note_id": "6b2d3f90-1a4c-4e78-9d05-8f7e2c1b4a93"
}
}

Configure webhook endpoints in Settings > Webhooks or via the API. See the Set Up Webhooks guide for a step-by-step walkthrough.