# Pays for sales invoices using a specified payment provider and can handle payments with and without 3D Secure…

`POST /api/payment/pay`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`), Consumer (`CONSUMER`) (any one of these products, not all)
- **Features:** Online Joining (`ONLINEJOIN`), Online Bookings (`ONLINEBOOK`), Front of House Generic Access (`FOHGENERIC`) (any one of these features, not all)

HTML version: [Pays for sales invoices using a specified payment provider and can handle payments with and without 3D Secure…](https://support.gladstonesoftware.cloud/reference/payments/pays-for-sales-invoices-using-a-specified-payment-provider-and-can-handle-paymen)

## Overview

Takes card payment for one or more sales invoices that already exist, and then marks them as
paid. It is the last step of a purchase: create the invoices first, then call this.

**This endpoint is not atomic, and one of its outcomes leaves you half-finished.** It performs two
separate operations — charge the card at the payment provider, then tell the platform to settle
the invoices. If the charge succeeds and the settlement fails, you get `responseStatus:
ErrorDoNotRepeatTransaction`. The member has been charged and the invoices are still outstanding.
Do not retry: a retry charges them a second time. Capture `transactionId` and reconcile manually.
Every other failure mode leaves nothing charged.

### The amount is not yours to choose

`paymentMethods[].amount` does not decide what is taken from the card. The payment provider
recalculates the total itself, from the unpaid invoices named in `salesInvoiceIds`, and charges
that. The value you send is used only for a sanity check — it must not exceed the invoice total —
and is then discarded. There is no partial payment: sending `amount: 1.00` against a £50 invoice
set charges £50.

Every id in `salesInvoiceIds` must exist, still be unpaid, and belong to `memberId`. If any one of
them fails that test the whole request is rejected and nothing is charged.

### When a 3D Secure challenge is required

The first call may come back `200` with `responseStatus: ThreeDSecureChallengeRequired` instead of
a result. When it does:

1. POST every key/value pair in `threeDSecureChallengeFields` as a form to
   `threeDSecureChallengeUrl` in the cardholder's browser. The field set is dynamic and varies by
   challenge type, but always contains `threeDSRef`.
2. Collect the URL-encoded form body the challenge posts back to your redirect endpoint.
3. Call this endpoint again with the **same** `salesInvoiceIds`, `memberId` and card reference,
   putting the `threeDSRef` value into `threeDSecureChallengeReference` and the raw response body
   into `threeDSecureChallengeResponse`.

The second call can itself return `ThreeDSecureChallengeRequired`; repeat until you get a terminal
status. Only `PaymentSuccessful` means the invoices have been settled. The most common cause of an
apparently endless challenge loop is sending back a `threeDSRef` from the wrong round, or a
re-encoded challenge response — pass both through unchanged.

### Zero-priced invoices

When the invoices total zero the provider is never called. The invoices are settled, the result is
`PaymentSuccessful` and `transactionId` is `null`. A card reference is not required in that case.

## Endpoint

`POST {base_url}/api/payment/pay`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `salesInvoiceIds` | array of integer | Yes | The sales invoices to settle, created before this call. Every id must exist, still be unpaid, and belong to `memberId` — if the provider cannot match all of them the request is rejected and nothing is charged. The list must not contain the same id twice; duplicates are rejected with `The sales invoice ids {ids} contains duplicate elements.` Their combined total is what gets charged, regardless of `paymentMethods[].amount`. On a repeat call after a 3D Secure challenge, send exactly the same set. |
| `memberId` | integer (int32) | Yes | The member the invoices belong to and whose card is charged. It is not just a label: the invoice lookup is scoped by it, so a mismatch between the member and the invoices fails the request. It also selects the wallet that `cardId` is resolved against and that `saveCardToWallet` and `saveRCPCard` write to, and the member's name and email address are passed to the payment provider for the transaction record. |
| `paymentMethods` | array of object | Yes | How to pay. Despite being an array, it must contain **exactly one** element — fewer is rejected with `At least one payment method must be specified` and more with `Only one payment method is supported`. Splitting a total across several tenders is not available on this endpoint; the array shape is groundwork for a future capability. |
| `paymentMethods[].paymentProvider` | string (enum) | Yes | The gateway to route the charge through, sent as a string. `GladstonePay` is the only accepted value; anything else is rejected with `The payment provider {provider} is not valid.` [Get all payment providers](get-all-payment-providers.md) may list other names, but they are configuration records and are not accepted here. |
| `paymentMethods[].amount` | number (double) | No | The amount you believe is due, to two decimal places. **It does not control what is charged** — the payment provider recalculates the total from the unpaid invoices and takes that. The only use made of this value is a guard: if it is greater than the invoice total (compared after rounding the total down to two decimal places) the request is rejected with `Payment should not be greater than the total invoice amount.` Omitting it sends `0`, which passes the guard, so a missing `amount` never blocks a payment and never reduces one. |
| `paymentMethods[].saveCardToWallet` | boolean | No | Store the card in the member's wallet at the payment provider so later payments can use `cardId` instead of a fresh token. Defaults to `false`. Only meaningful alongside `paymentToken`. Storage is best-effort and happens after the charge: if the provider does not return a wallet id, the payment still succeeds and no wallet record is created, with no error surfaced to you. |
| `paymentMethods[].saveRCPCard` | boolean | No | Make this card the member's default card for Recurrent Card Payments — the card later automatic collections are taken from. Defaults to `false`. It has conditions: with a new card (`paymentToken`) you must also send `saveCardToWallet: true`, or the request fails with `New card details can only be set as the RCP card if they are also saved to the member's wallet.` Where the operator has the `VERIFYRCPCARD` feature enabled, an RCP card can only be established from a `paymentToken` — an initial request using `cardId` fails with `When the VERIFYRCPCARD feature is enabled, card details can only be set as the RCP card if they are provided in the PaymentToken. They cannot be specified via a CardID.` — and any existing wallet entry for the member is deleted first so the provider treats the transaction as a new agreement. Like wallet storage, setting the RCP card is best-effort after the charge. |
| `paymentMethods[].paymentToken` | string | No | An opaque single-use token representing card details captured by the payment provider's own hosted field, so that the card number never reaches your servers or ours. Pass it through byte for byte. Mutually exclusive with `cardId` in effect — when `paymentToken` is present it is used and `cardId` is ignored. One of the two is required whenever the invoice total is greater than zero, otherwise the request fails with `The PaymentToken or CardId field is required if the total price of the invoices is greater than 0.` A token the provider rejects returns `responseStatus: InvalidPaymentToken`. |
| `paymentMethods[].cardId` | integer (int32) | No | A card already stored in this member's wallet at the payment provider, used instead of capturing card details again. Only consulted when `paymentToken` is absent. One of the two is required whenever the invoice total is greater than zero. Because a stored card carries no CVV, payments made this way are sent with the CVV and AVS/CV2 checks relaxed — they are not equivalent in risk terms to a freshly captured card. |
| `paymentMethods[].threeDSecureChallengeReference` | string | No | Omit on the first call. On a follow-up call, the `threeDSRef` value taken from the `threeDSecureChallengeFields` of the response that asked for the challenge — the provider uses it to reattach your request to the transaction already in flight. It is opaque; send it back unchanged, and take it from the most recent challenge response rather than the first, since a multi-step challenge issues a new one each round. Sending it without `threeDSecureChallengeResponse` has no effect: the presence of the *response* field, not this one, is what makes the endpoint treat the call as a continuation. |
| `paymentMethods[].threeDSecureChallengeResponse` | string | No | Omit on the first call. On a follow-up call, the URL-encoded form body the 3D Secure challenge posted back to your redirect endpoint, verbatim — it is URL-decoded, split on `&` and `=`, and each pair is forwarded to the provider as a separate field. Do not parse, re-encode or reorder it. Sending any non-empty value here switches the request into continuation mode, in which only this field, `threeDSecureChallengeReference` and `saveCardToWallet` are used; the card reference and device fields from the original call are not re-sent. |
| `paymentMethods[].threeDSecureChallengeOptionalField` | object | No | **Accepted and discarded.** The object is accepted and recorded in the payment audit log, but it is never forwarded to the payment provider. Send the browser fingerprint through `deviceInformation` and the standard HTTP headers instead. The nested fields below are documented only so you can recognise them; none of them has any effect. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserAcceptHeader` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. The provider is given the `Accept` header of your request instead. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserLanguage` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. The provider is given the `Accept-Language` header of your request instead. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserTimeZone` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. Use `deviceInformation.timeZone`, which is forwarded. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserUserAgent` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. The provider is given the `User-Agent` header of your request instead, so forward the cardholder's user agent on the request itself. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserJavaEnabledVal` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserJavascriptEnabledVal` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserScreenHeight` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. Screen dimensions reach the provider only through `deviceInformation.screenResolution`. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserScreenWidth` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. Screen dimensions reach the provider only through `deviceInformation.screenResolution`. |
| `paymentMethods[].threeDSecureChallengeOptionalField.browserScreenColorDepth` | string | No | Ignored — see `threeDSecureChallengeOptionalField`. Colour depth reaches the provider only as the third component of `deviceInformation.screenResolution`. |
| `paymentMethods[].deviceInformation` | object | No | Details of the cardholder's browser, forwarded to the payment provider as part of the 3D Secure risk assessment. This is the object that is actually used, unlike `threeDSecureChallengeOptionalField`. It is only read on the initial call — a continuation call carrying `threeDSecureChallengeResponse` ignores it. Supplying poor or no device information does not fail the request, but makes a challenge more likely. The remaining browser signals (IP address, user agent, `Accept`, `Accept-Encoding`, `Accept-Language`) are taken from your HTTP request headers, not from this object, so proxy the cardholder's headers through. |
| `paymentMethods[].deviceInformation.timeZone` | string | No | The cardholder's browser time zone offset. Forwarded to the provider when non-empty and omitted from the provider request otherwise. |
| `paymentMethods[].deviceInformation.capabilities` | string | No | The browser capability string used in 3D Secure device fingerprinting. Forwarded to the provider unchanged when non-empty and omitted otherwise. |
| `paymentMethods[].deviceInformation.screenResolution` | string | No | The cardholder's screen resolution as `WIDTHxHEIGHTxCOLOURDEPTH`, for example `1920x1080x24`. The colour depth must be one of `1`, `4`, `8`, `15`, `16`, `24`, `32` or `48`; any other positive value is silently rounded **down** to the nearest permitted one before the request reaches the provider. A value that does not split into three parts on `x` is passed through untouched. Omitted from the provider request when empty. |

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/payment/pay' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "salesInvoiceIds": [
    12345
  ],
  "memberId": 12345,
  "paymentMethods": [
    {
      "paymentProvider": "GladstonePay",
      "amount": 10.5,
      "saveCardToWallet": true,
      "saveRCPCard": true,
      "paymentToken": "string",
      "cardId": 12345,
      "threeDSecureChallengeReference": "string",
      "threeDSecureChallengeResponse": "string",
      "threeDSecureChallengeOptionalField": {
        "browserAcceptHeader": "string",
        "browserLanguage": "string",
        "browserTimeZone": "string",
        "browserUserAgent": "string",
        "browserJavaEnabledVal": "string",
        "browserJavascriptEnabledVal": "string",
        "browserScreenHeight": "string",
        "browserScreenWidth": "string",
        "browserScreenColorDepth": "string"
      },
      "deviceInformation": {
        "timeZone": "string",
        "capabilities": "string",
        "screenResolution": "string"
      }
    }
  ]
}'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `responseStatus` | string (enum) | The outcome, and the field to branch on — the HTTP status alone is not enough, because a `200` can mean either "paid" or "challenge me". `PaymentSuccessful` is the only value that means the invoices are settled. `ThreeDSecureChallengeRequired` means continue the 3D Secure flow. `ErrorDoNotRepeatTransaction` means the card was charged but the invoices were not settled — never retry on this one. `PaymentFailed` and `InvalidPaymentToken` mean the provider declined and nothing was taken, so a retry is safe. `InvalidRequest` means your request was wrong, `InvalidConfiguration` means the operator's provider settings are broken, and `ServiceUnavailable` and `InternalServerError` mean a downstream failure — check the accompanying HTTP status before assuming nothing was charged. |
| `transactionId` | string | The payment provider's identifier for the transaction, and the value to quote when reconciling or investigating. `null` when the invoices totalled zero, because no transaction was created. Keep it in particular when `responseStatus` is `ErrorDoNotRepeatTransaction`: it is the only handle on money that has been taken but not settled. Note that on some failures raised before the provider is reached this carries an internally generated reference rather than a provider transaction id. |
| `message` | string | The reason a request was rejected, populated on validation failures such as duplicate invoice ids or an `amount` above the invoice total. It is intended for your logs, not for cardholders: it can name request fields and is not localised. Absent on success. |
| `threeDSecureChallengeUrl` | string | Where to POST the challenge form in the cardholder's browser. Present **only** when `responseStatus` is `ThreeDSecureChallengeRequired`; the field is absent from every other response. Treat it as single-use and read it fresh from each challenge response rather than caching it. |
| `threeDSecureChallengeFields` | object | A flat map of form field names to values that must be POSTed to `threeDSecureChallengeUrl` exactly as given — the set is dynamic and depends on the challenge type, so iterate it rather than reading named fields. It always contains `threeDSRef`, which is the value you send back as `threeDSecureChallengeReference` on the follow-up call. Present only when `responseStatus` is `ThreeDSecureChallengeRequired`. |

## Sample response

```json title="200 OK"
{
  "responseStatus": "PaymentFailed",
  "transactionId": "string",
  "message": "string",
  "threeDSecureChallengeUrl": "string",
  "threeDSecureChallengeFields": "string"
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | Request validation failed before anything was charged. `responseStatus` is `InvalidRequest` and `message` gives the reason: `The sales invoice ids {ids} contains duplicate elements.`, `Payment should not be greater than the total invoice amount.`, `The total invoice amount must be greater than or equal to 0.`, `The PaymentToken or CardId field is required if the total price of the invoices is greater than 0.` or `The payment provider {provider} is not valid.` |
| `400` | Bad request | Basic validation failures on the body: `At least one payment method must be specified` or `Only one payment method is supported`. `salesInvoiceIds`, `memberId` and `paymentMethods` are all required. |
| `400` | Bad request | The RCP card rules were broken: `New card details can only be set as the RCP card if they are also saved to the member's wallet.` or `When the VERIFYRCPCARD feature is enabled, card details can only be set as the RCP card if they are provided in the PaymentToken. They cannot be specified via a CardID.` |
| `400` | Bad request | One or more `salesInvoiceIds` could not be matched to an unpaid invoice for `memberId`, or the invoices totalled zero at the provider. Nothing was charged. |
| `400` | Bad request | The card was declined or the token rejected. `responseStatus` is `PaymentFailed` or `InvalidPaymentToken`. Nothing was charged; the cardholder can retry. |
| `422` | Unprocessable entity | `responseStatus: InvalidConfiguration` — the operator's payment provider settings are missing or incomplete for the site the invoices belong to. Not retryable by you; it needs configuration work. |
| `500` | Internal server error | `responseStatus: ErrorDoNotRepeatTransaction` — **the card was charged and the invoices were not settled.** Do not retry. Record `transactionId` and reconcile manually. |
| `500` | Internal server error | `responseStatus: InternalServerError` or `PaymentFailed` from an unexpected provider error, before any charge. |
| `503` | Service unavailable | Part of the platform needed to take the payment could not be reached. If `responseStatus` is `ServiceUnavailable` the invoices were never retrieved and nothing was charged. If it is `ErrorDoNotRepeatTransaction`, **the card was charged** and the settlement call could not be made — do not retry. |
