# Finance · Payments

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /finance/payments/action

**Execute a payment action**

`operationId: PaymentController_executeAction`

The general form of every payment operation: name the `action` and the `gateway`, and the request is dispatched to that provider.

Each action also has its own alias endpoint — `/charge`, `/refund` and so on — which fills in `action` and is otherwise identical. Use the aliases for readability; use this one when the action is chosen at runtime.

**`amount` is in the currency's minor unit — cents, not dollars.** `1000` is $10.00. Sending `10` charges ten cents.

A gateway refusal is reported as `success: false` with an `error`, not as an HTTP error status, so check the body — a `200` does not mean the money moved.

#### Signature

```http
POST /finance/payments/action (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A `success: false` body is the normal way a decline is reported. Do not treat `200` as proof of payment.
- Not idempotent — nothing dedupes a repeated charge. Retries must be guarded by the caller.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/charge`
- `POST /finance/payments/refund`

### 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

The action, the gateway, and whatever that action needs.

```json
{
  "action": "charge",
  "gateway": "stripe",
  "amount": 1000,
  "currency": "USD",
  "paymentMethodId": "pm_1PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/charge

**Charge a payment method**

`operationId: PaymentController_charge`

Takes money immediately: authorizes and captures in one step. This is the default way to collect a payment when there is no reason to hold the funds first.

**`amount` is in the currency's minor unit — cents, not dollars.** `1000` is $10.00. Sending `10` charges ten cents.

The `paymentRef` in the response is the handle for every later action — refund, verify, receipt — so store it against your order.

#### Signature

```http
POST /finance/payments/charge (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Not idempotent. Guard against double submission — a repeated request charges twice.
- Use `authorize` instead when you need to confirm stock or availability before taking the money.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/authorize`
- `POST /finance/payments/refund`

### 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

What to charge, and against what.

```json
{
  "gateway": "stripe",
  "amount": 1000,
  "currency": "USD",
  "customerId": "cus_PabcXYZ",
  "paymentMethodId": "pm_1PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/authorize

**Authorize a payment**

`operationId: PaymentController_authorize`

Reserves funds on the customer's card without taking them. The money is held but not moved, and the authorization must later be captured or voided.

Use this when the charge should be contingent — confirming stock, weighing a shipment, or adding a tip after the card is presented.

**Authorizations expire**, typically within a week, and the window is the gateway's. An expired authorization cannot be captured.

**`amount` is in the currency's minor unit — cents, not dollars.** `1000` is $10.00. Sending `10` charges ten cents.

#### Signature

```http
POST /finance/payments/authorize (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Every authorization must be captured or voided. One left alone holds the customer's funds until the gateway expires it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/capture`
- `POST /finance/payments/void`

### 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

What to authorize.

```json
{
  "gateway": "stripe",
  "amount": 1000,
  "currency": "USD",
  "paymentMethodId": "pm_1PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/capture

**Capture an authorized payment**

`operationId: PaymentController_capture`

Takes the funds an authorization was holding. Pass `amount` to capture less than was authorized — most gateways allow a partial capture but not one above the authorized amount.

Omitting `amount` captures the full authorization.

**`amount` is in the currency's minor unit — cents, not dollars.** `1000` is $10.00. Sending `10` charges ten cents.

#### Signature

```http
POST /finance/payments/capture (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- An expired or already-captured authorization is refused by the gateway and comes back as `success: false`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/authorize`

### 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 authorization to capture, and how much.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/refund

**Refund a payment**

`operationId: PaymentController_refund`

Returns money for a payment that was captured. Pass `amount` for a partial refund; omit it to refund in full.

Refunds can usually be issued more than once against a payment until the original total is reached, so this is **not idempotent** — a retry refunds again.

**`amount` is in the currency's minor unit — cents, not dollars.** `1000` is $10.00. Sending `10` charges ten cents.

#### Signature

```http
POST /finance/payments/refund (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Not idempotent. Verify with `verify` before retrying a refund whose response you lost.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/void`
- `POST /finance/payments/verify`

### 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 payment to refund, and how much.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ",
  "reason": "Customer returned the item"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/void

**Void an authorization**

`operationId: PaymentController_void`

Releases an authorization without taking the money, freeing the hold on the customer's card.

Void is for money that was never captured. Once funds have been taken, use `refund` instead — the two are not interchangeable, and gateways treat them differently.

#### Signature

```http
POST /finance/payments/void (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A captured payment cannot be voided. Refund it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/refund`
- `POST /finance/payments/authorize`

### 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 authorization to release.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ",
  "reason": "Order cancelled before dispatch"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/verify

**Verify a payment**

`operationId: PaymentController_verify`

Asks the gateway for the current state of a payment, rather than trusting what a client reported or what was last written locally.

This is the call to make when a response was lost, before retrying anything — it tells you whether the money actually moved.

#### Signature

```http
POST /finance/payments/verify (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Read-only — safe to call as often as needed.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/retry`

### 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 payment to check.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/cancel

**Cancel a payment**

`operationId: PaymentController_cancel`

Cancels a payment that is still in progress at the gateway — one awaiting confirmation or a customer action, which is neither a live authorization to void nor a captured payment to refund.

#### Signature

```http
POST /finance/payments/cancel (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Only applies to payments the gateway still considers pending. Use `void` for an authorization and `refund` for a captured payment.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `POST /finance/payments/void`

### 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 payment to cancel.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ",
  "reason": "Customer abandoned checkout"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/resend-receipt

**Resend a payment receipt**

`operationId: PaymentController_resendReceipt`

Asks the gateway to resend its receipt for a payment. Pass `email` to send it somewhere other than the address on the original payment.

#### Signature

```http
POST /finance/payments/resend-receipt (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Sends the *gateway's* receipt, not the platform's order confirmation. For that, use `GET /storefront/order/send-welcome/{orderNumber}`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |

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

#### See also

- `GET /storefront/order/send-welcome/{orderNumber}`

### 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 payment, and where to send the receipt.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ",
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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 /finance/payments/retry

**Retry a failed payment**

`operationId: PaymentController_retry`

Re-attempts a payment that previously failed, reusing the original transaction's details.

The original transaction must exist — this replays a known failure rather than creating a fresh payment. Verify the original state first, so a payment that actually succeeded is not charged a second time.

#### Signature

```http
POST /finance/payments/retry (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |
| `404` | — | Original transaction not found | No transaction matches `paymentRef`. | Retry replays a recorded transaction. Issue a fresh `charge` instead. |

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

#### See also

- `POST /finance/payments/verify`
- `POST /finance/payments/charge`

### 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 failed payment to retry.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ",
  "paymentMethodId": "pm_1PdefUVW"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Original transaction not found — No transaction matches `paymentRef`. |
| `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 /finance/payments/mark-paid

**Mark a payment as paid**

`operationId: PaymentController_markPaid`

Records a transaction as settled **without moving any money** — for payments taken outside the platform: a bank transfer, a cheque, cash at a counter.

The gateway is never contacted. This only changes what the ledger says, so use it when the money has genuinely arrived by another route.

The `author` header is recorded on the transaction, defaulting to `system` when absent — send it so the audit trail names a person.

#### Signature

```http
POST /finance/payments/mark-paid (body) -> The action result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- No money moves. Only use it when payment has genuinely been received outside the platform.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Gateway <gateway> not configured | The named gateway has no integration set up for this org. | Configure the gateway in the org integration settings. The name is matched against the configured integration. |
| `404` | — | Transaction not found | No transaction matches `paymentRef`. | The transaction must already exist — this settles a record, it does not create one. |

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

#### See also

- `POST /finance/payments/verify`

### 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. |
| `author` | header | string | yes |  |

### Request body

Which transaction to mark settled.

```json
{
  "gateway": "manual",
  "paymentRef": "txn_4a91",
  "reason": "Paid by bank transfer, ref BACS-99182"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The action result |
| `400` | Gateway <gateway> not configured — The named gateway has no integration set up for this org. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Transaction not found — No transaction matches `paymentRef`. |
| `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 /finance/payments/gateway-url

**Get the gateway dashboard URL for a payment**

`operationId: PaymentController_getGatewayUrl`

Builds a deep link to a payment in the provider's own dashboard, so an operator can jump from a transaction here to the full record at Stripe, PayPal or Helcim.

The URL is assembled from a fixed pattern per gateway — nothing is looked up and no provider is contacted, so the link is returned even for a reference that does not exist. **An unrecognised gateway yields an empty string**, not an error.

This is the only endpoint on the controller that does not read the `orgid` header, because it touches no org data.

#### Signature

```http
POST /finance/payments/gateway-url (body) -> The dashboard URL, or an empty string for an unrecognised gateway
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Purely string construction — the link is not validated and may 404 at the provider.
- Check for an empty `url` before rendering a link.

#### Errors

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

#### See also

- `GET /finance/payments/gateway-transactions`

### Request body

The gateway and payment reference.

```json
{
  "gateway": "stripe",
  "paymentRef": "pi_3PabcXYZ"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The dashboard URL, or an empty string for an unrecognised gateway |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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. |

Example response:

```json
{
  "url": "https://dashboard.stripe.com/payments/pi_3PabcXYZ"
}
```

## GET /finance/payments/gateway-transactions

**List transactions from the gateway**

`operationId: PaymentController_getGatewayTransactions`

Reads transactions **directly from the provider**, not from the platform ledger. Use it to reconcile — to see what the gateway thinks happened, independently of what was recorded here.

Paging is the provider's cursor style: pass the last id you saw as `startingAfter` to continue.

#### Signature

```http
GET /finance/payments/gateway-transactions (provider?: string, limit?: integer, startDate?: string, endDate?: string, startingAfter?: string) -> Transactions as the provider reports them
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Staff — a signed-in business user; customer and site-app tokens are refused`.

#### Notes

- The shape is the provider's own and differs between gateways — it is not normalised.
- This is a live call to the provider and counts against their rate limits.

#### Errors

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

#### See also

- `POST /finance/payments/verify`

### 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` | query | string | — | Provider to query. Defaults to `StripeProvider`. |
| `limit` | query | integer | — | How many to return. |
| `startDate` | query | string | — | Lower bound on transaction date. |
| `endDate` | query | string | — | Upper bound on transaction date. |
| `startingAfter` | query | string | — | Cursor — the last transaction id from the previous page. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Transactions as the provider reports them |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another 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. |

