# Storefront · Payments

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /storefront/verify-payment/{provider}/{configId}/{paymentId}

**Verify a payment with the provider**

`operationId: StorefrontController_verifyPayment`

Asks the provider directly whether a payment succeeded, rather than trusting the client. Use it to confirm a redirect-based payment when the customer returns to your site.

#### Signature

```http
GET /storefront/verify-payment/{provider}/{configId}/{paymentId} (provider: string, configId: string, paymentId: string) -> The verification result from the provider
```

#### Access

Public — no credentials required.

#### Notes

- Anonymous in the code as it stands: StorefrontController is `@PublicRoute()` at class level, and the handler's `@Roles(User)` is never checked because the guard returns early for public routes.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `provider` | path | string | yes | Payment provider. |
| `configId` | path | string | yes | Which configured gateway instance to verify against. |
| `paymentId` | path | string | yes | The provider's payment identifier. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The verification result from the provider |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/intent

**Create a Stripe PaymentIntent**

`operationId: StorefrontController_stripePaymentIntent`

Creates an online (card-not-present) PaymentIntent and returns its client secret for the browser Stripe SDK to confirm.

#### Signature

```http
POST /storefront/stripe/intent (body) -> The PaymentIntent, including `client_secret`
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/capture`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Intent details.

```json
{
  "amount": 129.99,
  "currency": "USD",
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The PaymentIntent, including `client_secret` |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/terminal/connection-token

**Get a Stripe Terminal connection token**

`operationId: StorefrontController_stripeTerminalConnectionToken`

Returns a single-use token that the Terminal SDK uses to initialise a card reader (BLE M2, WisePOS, …).

The token rotates on every call — fetch a fresh one each time the SDK asks, and never cache it.

#### Signature

```http
POST /storefront/stripe/terminal/connection-token () -> A single-use connection token
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/terminal/intent`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | A single-use connection token |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/terminal/location/ensure

**Find or create a Stripe Terminal location**

`operationId: StorefrontController_stripeTerminalEnsureLocation`

Maps one of your business locations onto a Stripe Terminal Location, creating it if it does not exist yet. The client sends only `businessLocationId`; the server reads the authoritative title and address from the location record, so Stripe never receives address data typed on a device.

If the location record is missing address fields Stripe requires, the request is refused with a message naming what to fix rather than creating a half-configured Location.

#### Signature

```http
POST /storefront/stripe/terminal/location/ensure (body) -> The existing or newly created Stripe Terminal Location
```

#### Access

Public — no credentials required.

#### Notes

- Safe to call repeatedly — an existing matching Location is returned rather than duplicated.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | LOCATION_INCOMPLETE | Location is missing required address fields | The business location record lacks an address field Stripe requires. | Complete the address on the location record — the error names the missing fields — then retry. |
| `404` | LOCATION_NOT_FOUND | Business location not found | `businessLocationId` does not resolve in the org. | Check the location id. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/terminal/connection-token`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which business location to map.

```json
{
  "businessLocationId": "loc_downtown"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The existing or newly created Stripe Terminal Location |
| `400` | Location is missing required address fields — The business location record lacks an address field Stripe requires. |
| `404` | Business location not found — `businessLocationId` does not resolve in the org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/terminal/intent

**Create a card-present PaymentIntent**

`operationId: StorefrontController_stripeTerminalIntent`

Creates a PaymentIntent restricted to `card_present`, for a physical reader driven by the Terminal SDK.

Set `captureMethod: "manual"` to authorise now and capture later — the tip-adjustment flow — then complete it with `POST /storefront/stripe/capture`.

#### Signature

```http
POST /storefront/stripe/terminal/intent (body) -> The card-present PaymentIntent
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/capture`
- `POST /storefront/pos/tab/{id}/settle`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Card-present intent details.

```json
{
  "amount": 25.92,
  "currency": "USD",
  "orderId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The card-present PaymentIntent |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/capture

**Capture an authorized PaymentIntent**

`operationId: StorefrontController_stripeCapture`

Captures a PaymentIntent that was created with `captureMethod: "manual"`. Pass `amount` to capture less than was authorised, or more where the gateway permits it — this is how a tip added after the card is presented gets charged.

#### Signature

```http
POST /storefront/stripe/capture (body) -> The captured PaymentIntent
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | NOT_CAPTURABLE | PaymentIntent is not in a capturable state | The intent was created with automatic capture, was already captured, or has expired. | Only intents created with `captureMethod: "manual"` and still authorised can be captured. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/terminal/intent`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which intent to capture, and how much.

```json
{
  "paymentIntentId": "pi_3PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The captured PaymentIntent |
| `400` | PaymentIntent is not in a capturable state — The intent was created with automatic capture, was already captured, or has expired. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/stripe/checkout-session

**Create a Stripe Checkout session**

`operationId: StorefrontController_stripeCheckSession`

Creates a hosted Stripe Checkout session for a one-off purchase and returns the URL to redirect the customer to.

#### Signature

```http
POST /storefront/stripe/checkout-session (body) -> The session, including the redirect URL
```

#### Access

Public — no credentials required.

#### Notes

- Confirm the outcome with `GET /storefront/verify-payment/...` when the customer returns — do not trust the redirect alone.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/stripe/subscription-session`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Session details.

```json
{
  "items": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2,
      "price": 12
    }
  ],
  "successUrl": "https://shop.example.com/thanks",
  "cancelUrl": "https://shop.example.com/cart",
  "currency": "USD"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The session, including the redirect URL |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/paypal/config

**Get the PayPal configuration**

`operationId: StorefrontController_paypalConfig`

What the browser PayPal SDK needs for this org: the public fields of its active PayPal integration only — `data.provider`, `data.clientId`, `data.sandbox` — plus `configured` (false when the app secret is missing, so the button should not be offered). Credentials never leave the server. With no active PayPal config the response is an empty 200.

#### Signature

```http
GET /storefront/paypal/config () -> Public PayPal fields, or an empty body
```

#### Access

Public — no credentials required.

#### Notes

- Prefer `GET /storefront/payment-gateways`, which returns only the public fields of each gateway including PayPal's `clientId` and `sandbox`.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /storefront/payment-gateways`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Public PayPal fields, or an empty body |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/payments/charge-link

**Create a standalone charge and its payment link**

`operationId: StorefrontController_createChargeLink`

A charge with no invoice behind it: writes a pending `sf_transaction` (type `charge`, source `take-payment`) for `amount` and returns the URL the customer pays at — the site's Payment page with `?ref=<id>`, which is also what the QR encodes. `url` is null, with a `reason`, when the site has no Payment page configured (Site Config › Site Features).

#### Signature

```http
POST /storefront/payments/charge-link (body) -> The charge and its link
```

#### Access

Public — no credentials required.

#### Notes

- Anonymous: StorefrontController is `@PublicRoute()` at class level and this handler does not override it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | AMOUNT_REQUIRED | An amount greater than zero is required | `amount` is missing, not a number, or not positive. | — |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/payments/charge-link/{id}/send`
- `GET /storefront/payments/charge-link/{id}/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "amount": 45,
  "currency": "USD",
  "description": "Deposit for event booking",
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The charge and its link |
| `400` | An amount greater than zero is required — `amount` is missing, not a number, or not positive. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/payments/charge-link/{id}/send

**Send a charge's payment link**

`operationId: StorefrontController_sendChargeLink`

Emails and/or texts the charge's payment link (the same URL the QR encodes) with the `payment-request` / `payment-request-sms` templates. Addresses default to the ones given when the charge was created; `channels` defaults to every channel with an address.

#### Signature

```http
POST /storefront/payments/charge-link/{id}/send (id: string, body) -> Channels sent and the link
```

#### Access

Public — no credentials required.

#### Notes

- Anonymous: StorefrontController is `@PublicRoute()` at class level and this handler does not override it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | CHARGE_NOT_FOUND | Charge not found | No transaction with that id. | — |
| `400` | NO_PAYMENT_PAGE | No Payment page is configured on the site — set one in Site Config > Site Features. | The site has no Payment page. | — |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Charge id (`sf_transaction` sk) from POST /storefront/payments/charge-link. |

### Request body

```json
{
  "phone": "+15555550123",
  "channels": [
    "sms"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Channels sent and the link |
| `400` | No Payment page is configured on the site — set one in Site Config > Site Features. — The site has no Payment page. |
| `404` | Charge not found — No transaction with that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/payments/charge-link/{id}/status

**Has a charge been paid**

`operationId: StorefrontController_chargeLinkStatus`

Polled by the till while the customer pays on their own device. `paid` is true once the transaction status is paid, succeeded or completed.

#### Signature

```http
GET /storefront/payments/charge-link/{id}/status (id: string) -> Payment state
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | CHARGE_NOT_FOUND | Charge not found | No transaction with that id. | — |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Charge id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Payment state |
| `404` | Charge not found — No transaction with that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/payment-gateways

**List available payment gateways**

`operationId: StorefrontController_listPaymentGateways`

The org's configured, **active** gateways from Stripe, PayPal, Offline, Gift card and Account balance (the `default` config of each), in that order — build the payment selector from this rather than hard-coding options.

When `amount` and `currency` are both given, a Stripe PaymentIntent is created up front and attached to the Stripe entry as `payIntent`, so the client can mount Stripe Elements without a second round-trip. `email` is passed to that intent.

**Only the public view of each gateway is returned**: `sk`, `name`, `datatype`, `configured` and `data` limited to presentation and client-SDK fields (`name`, `provider`, `type`, `status`, `displayName`, `description`, `clientId`, `publishableKey`, `publicKey`, `clientToken`, `testMode`, `sandbox`, `default`, `merchantName`, `supportedMethods`, `instructions`, `bankDetails`). Secret keys, API keys and webhook secrets never leave the server. `configured` is false when the gateway is missing a credential it needs to take money (Stripe secret + publishable key, PayPal app secret + client id, Helcim API token).

#### Signature

```http
GET /storefront/payment-gateways (amount?: string, currency?: string, email?: string) -> The gateways, public fields only
```

#### Access

Public — no credentials required.

#### Notes

- A gateway that errors while loading is skipped (logged), not returned as an error.
- Inactive or deprecated gateway configs are never listed.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/take-payment`
- `GET /storefront/paypal/config`
- `POST /storefront/stripe/intent`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `amount` | query | string | — | With `currency`: create a Stripe PaymentIntent for this amount. |
| `currency` | query | string | — | With `amount`. |
| `email` | query | string | — | Customer email for the eager Stripe intent. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The gateways, public fields only |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
[
  {
    "sk": "66f1…",
    "name": "default",
    "datatype": "config",
    "configured": true,
    "data": {
      "provider": "StripeProvider",
      "name": "default",
      "publishableKey": "pk_live_…"
    },
    "payIntent": {
      "id": "pi_3Pab…",
      "client_secret": "pi_3Pab…_secret_…",
      "publishableKey": "pk_live_…"
    }
  }
]
```

## POST /storefront/take-payment

**Take a payment**

`operationId: StorefrontController_takePayment`

Charges the customer through the configured gateway and returns the payment result. Use the returned reference as `paymentRef` when creating the order.

#### Signature

```http
POST /storefront/take-payment (body) -> The payment result, including the reference to pass as `paymentRef`
```

#### Access

Public — no credentials required.

#### Notes

- Not idempotent. A retry charges again — capture the reference from the first response before retrying.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GATEWAY_ERROR | The payment gateway rejected the request. | Stripe returned an error — an invalid amount, a missing configuration, or a declined card. | The gateway message is passed through. Fix the reported problem and retry; do not retry an unchanged declined request. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/checkout-cart`
- `GET /storefront/payment-gateways`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Payment details.

```json
{
  "amount": 129.99,
  "currency": "USD",
  "gateway": "stripe",
  "paymentMethod": "pm_1PabcXYZ",
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The payment result, including the reference to pass as `paymentRef` |
| `400` | The payment gateway rejected the request. — Stripe returned an error — an invalid amount, a missing configuration, or a declined card. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

