Payments and payment methods
A payment is money taken from a guest: a card charge, a BACS or SEPA Direct Debit collection, or any other method. A payment method (a mandate) is a card or Direct Debit saved on a booking so future payments can be taken. Lavanda sends a webhook when either one changes, and the API tells you what changed and why.
Which webhook, when
Section titled “Which webhook, when”All events are in the webhook catalogue.
| Event | When it fires | Payload |
|---|---|---|
v1.payments.payment_method.created |
A saved payment method (card, BACS or SEPA mandate) is set up for a booking | payment_method.id, booking.id, workspace.id, timestamp |
v1.payments.payment_method.updated |
Its status changes: accepted, refused or revoked by the bank (BACS), cancelled, removed, card replaced by the bank, or guest switched banks (BACS) | same as above |
v2.payments.payment.created |
A payment is created, including a BACS collection from a mandate | payment.id, workspace.id |
v2.payments.payment.updated |
A payment’s status changes: succeeded, failed, cancelled, disputed, dispute won or lost. Also when the BACS expected debit date is set | payment.id, workspace.id |
v2.payments.payment.deleted |
A payment is removed | payment.id, workspace.id |
payment_method.idis the mandate ID.- Webhooks carry IDs only. Query the API for the details: the API response is your source of truth.
- Webhooks can arrive out of order. Order
payment_methoddeliveries bytimestamp. - Your endpoint must be subscribed to
v1.payments.payment_method.createdand.updatedto receive mandate updates. An endpoint subscribed only topayment.*events does not receive them.
Payment lifecycle
Section titled “Payment lifecycle”Typical flow: Due → Success, or Due → Failed
- Due — Created and in progress (for BACS, submitted to the bank and waiting to clear).
- Success — The money was collected.
- Failed — The payment didn’t go through.
failureDetails.failureCodesays why. - Canceled — Stopped before it completed.
cancellationReasonsays why. - Disputed — The guest’s bank has raised a dispute (a chargeback or, for Direct Debit, an indemnity claim).
- Dispute reversed — The dispute was lost and the payment reversed
(
reversalReason: CHARGEBACK). A won dispute returns the payment to Success. - Refunded and Expired — see Refunds and reversals. A refund that is later reversed by a refund reversal note shows as Failed.
| Field | Meaning |
|---|---|
status |
DUE, SUCCESS, FAILED, CANCELED, DISPUTED, DISPUTE_REVERSED, REFUNDED, EXPIRED |
initiatorType |
CIT (guest-initiated) or MIT (merchant-initiated, for example a collection from a mandate) |
failureDetails.failureCode |
Why it failed, for example CARD_DECLINED, INSUFFICIENT_FUNDS, DEBIT_NOT_AUTHORIZED, ACCOUNT_CLOSED |
cancellationReason |
For example REQUESTED_BY_CUSTOMER, ABANDONED, EXPIRED |
reversalReason |
CHARGEBACK when a lost dispute reversed the payment |
dispute |
reason (for example FRAUDULENT, DEBIT_NOT_AUTHORIZED) and status (for example NEEDS_RESPONSE, WON, LOST) |
expectedDebitDate |
BACS only: the date the money is expected to leave the guest’s account |
paymentMandate |
BACS only: the mandate the payment was collected from |
Querying a payment
Section titled “Querying a payment”Payments are looked up by workspace, so workspaceId is required. Pass workspace.id and
payment.id from the webhook.
query Payment($filters: PaymentChargeFilters!) { payments { charges(first: 1, filters: $filters) { edges { node { id status initiatorType failureDetails { failureCode } cancellationReason reversalReason dispute { reason status } sourceDetails { ... on PaymentSourceGatewayDetails { paymentMethodDetails { paymentMethodType ... on PaymentBacsDebitDetails { expectedDebitDate paymentMandate { id } } } } } } } } }}{ "filters": { "workspaceId": { "eq": "1234567890" }, "id": { "in": ["4c564450-4159-815b-81a4-000000190919"] } }}Payment method lifecycle
Section titled “Payment method lifecycle”Flow: Pending → Active → Cancelled
- Pending — Being set up: waiting for the guest to authorise it, or for the bank to confirm.
- Active — Ready to use for taking payments.
- Cancelled — No longer usable. Terminal. If the payment method was removed at the payment
provider,
statusReasonisPAYMENT_METHOD_DETACHED. If the guest completed the setup but the card could not be saved for future payments,statusReasonisCARD_DECLINED_AT_SETUP,CARD_EXPIRED_AT_SETUP,CARD_SECURITY_CODE_INCORRECT_AT_SETUPorCARD_NUMBER_INCORRECT_AT_SETUP(UNKNOWNfor any other card error). Send a new request.
BACS mandates also carry the bank’s own view of the mandate:
| Field | Values |
|---|---|
networkStatus |
PENDING, ACCEPTED, REFUSED, REVOKED. Null for cards and SEPA |
revocationReason |
ACCOUNT_CLOSED, BANK_ACCOUNT_RESTRICTED, BANK_OWNERSHIP_CHANGED, COULD_NOT_PROCESS, DEBIT_NOT_AUTHORIZED, or null |
When a guest’s bank replaces their card, or a BACS guest switches banks, the mandate stays Active and
its card or bank details are updated. You receive v1.payments.payment_method.updated.
Querying a payment method
Section titled “Querying a payment method”Payment methods are looked up by booking, so there is no workspaceId filter. Pass booking.id
and payment_method.id from the webhook. Access is checked against the booking’s workspace.
query PaymentMethod($filters: PaymentMandateFilters!) { payments { paymentMandates(filters: $filters) { nodes { id paymentMethodType status statusReason networkStatus revocationReason paymentMethodDetails { ... on PaymentMandateCardDetails { brand lastFourDigits expMonth expYear } ... on PaymentMandateBankAccountDetails { type lastFourDigits country } } } } }}{ "filters": { "bookingId": { "eq": "1234567890" }, "id": { "in": ["9f1c2b7e-3d4a-4e5f-8a6b-1c2d3e4f5a6b"] } }}Postman examples for both queries:
- https://www.postman.com/lavanda/lavanda-external-api/graphql-request/6acf54301a71ecc362e2e4f6
- https://www.postman.com/lavanda/lavanda-external-api/graphql-request/6acf560b7c3722ced568cd21
Values the API doesn’t recognise yet come back as UNKNOWN. Handle that case.
Examples
Section titled “Examples”- BACS collection succeeds —
payment.created(status Due), thenpayment.updated(Success). - BACS collection fails for insufficient funds —
payment.updatedwith Failed andINSUFFICIENT_FUNDS. The mandate stays Active, so the payment can be retried. - Debit not authorised —
payment.updatedwith Failed andDEBIT_NOT_AUTHORIZED. The bank revokes the mandate, so you also receivepayment_method.updatedwith the mandate Cancelled. - Dispute lost —
payment.updatedwith Disputed, thenpayment.updatedwith Dispute reversed. - Card replaced by the bank —
payment_method.updated. Query the mandate for the new last four digits and expiry.
- SEPA. SEPA mandates get the same webhooks, failure codes and dispute handling.
networkStatus,revocationReason, the bank-switch update andexpectedDebitDateare BACS only. - Testing debit not authorised. In Stripe’s test mode, the BACS
debit_not_authorizedtest accounts fail the payment but don’t deactivate the mandate, because test mode doesn’t send that update. In live, the mandate is cancelled as described above.
See also
Section titled “See also”- Staying up to date — the webhook + API pattern.
- Recurring payment methods (mandates) — setting up mandates and taking payments from them.
- Payments — payment states and what each payment shows.