# 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

All events are in the [webhook catalogue](https://webhooks-docs.lavanda.app/).

| 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.id` is 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_method` deliveries by `timestamp`.
- Your endpoint must be subscribed to `v1.payments.payment_method.created` and `.updated` to receive
  mandate updates. An endpoint subscribed only to `payment.*` events does not receive them.

## 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.failureCode` says why.
- **Canceled** — Stopped before it completed. `cancellationReason` says 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](/finance-and-accounting/payments/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

Payments are looked up by workspace, so `workspaceId` is required. Pass `workspace.id` and
`payment.id` from the webhook.

```graphql
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 }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "filters": {
    "workspaceId": { "eq": "1234567890" },
    "id": { "in": ["4c564450-4159-815b-81a4-000000190919"] }
  }
}
```

## 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, `statusReason` is `PAYMENT_METHOD_DETACHED`. If the guest completed the setup but the
  card could not be saved for future payments, `statusReason` is `CARD_DECLINED_AT_SETUP`,
  `CARD_EXPIRED_AT_SETUP`, `CARD_SECURITY_CODE_INCORRECT_AT_SETUP` or `CARD_NUMBER_INCORRECT_AT_SETUP`
  (`UNKNOWN` for 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

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.

```graphql
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
          }
        }
      }
    }
  }
}
```

```json
{
  "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

- **BACS collection succeeds** — `payment.created` (status Due), then `payment.updated` (Success).
- **BACS collection fails for insufficient funds** — `payment.updated` with Failed and
  `INSUFFICIENT_FUNDS`. The mandate stays Active, so the payment can be retried.
- **Debit not authorised** — `payment.updated` with Failed and `DEBIT_NOT_AUTHORIZED`. The bank
  revokes the mandate, so you also receive `payment_method.updated` with the mandate Cancelled.
- **Dispute lost** — `payment.updated` with Disputed, then `payment.updated` with Dispute reversed.
- **Card replaced by the bank** — `payment_method.updated`. Query the mandate for the new last four
  digits and expiry.

## Notes

- **SEPA.** SEPA mandates get the same webhooks, failure codes and dispute handling. `networkStatus`,
  `revocationReason`, the bank-switch update and `expectedDebitDate` are BACS only.
- **Testing debit not authorised.** In Stripe's test mode, the BACS `debit_not_authorized` test
  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

- [Staying up to date](/finance-and-accounting/integrations/ar-payments-api/staying-up-to-date/) — the webhook + API pattern.
- [Recurring payment methods (mandates)](/finance-and-accounting/payments/recurring-payment-methods/) — setting up
  mandates and taking payments from them.
- [Payments](/finance-and-accounting/payments/) — payment states and what each payment shows.
