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)
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:
- POST every key/value pair in
threeDSecureChallengeFieldsas a form tothreeDSecureChallengeUrlin the cardholder's browser. The field set is dynamic and varies by challenge type, but always containsthreeDSRef. - Collect the URL-encoded form body the challenge posts back to your redirect endpoint.
- Call this endpoint again with the same
salesInvoiceIds,memberIdand card reference, putting thethreeDSRefvalue intothreeDSecureChallengeReferenceand the raw response body intothreeDSecureChallengeResponse.
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.
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 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
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
{
"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. |