# Pay one of your invoices by card

> The portal.invoices.pay operation: POST /api/v1/portal/me/invoices/{invoiceId}/pay in the REST API and the portal_invoices_pay tool in the MCP server.

Starts a card payment of one of the signed-in customer's open invoices, for its amount due, at the brand's
processor: a one-off payment (no card is saved). `expectedAmount` is the amount due the customer saw; if it
changed meanwhile, the payment is refused.

- **fake processor:** the payment is confirmed here with the chosen test card (`paymentMethod`): `succeeded`,
  `processing`, `requires_action` (pay again to pass the fake 3-D Secure challenge) or a decline;
- **Stripe:** the answer is `client_confirm` with the Payment Element's `clientSecret`; the browser confirms;
- **Mollie:** the answer is `redirect` with `redirectUrl`, Mollie's payment page; the customer comes back to the
  invoice's hosted page.

The invoice is marked paid when the processor's webhook confirms the payment: read it again with
`portal.invoices.get`. Customer portal only (`portal:write`).

## At a glance

- **REST:** `POST /api/v1/portal/me/invoices/{invoiceId}/pay`
- **MCP tool:** `portal_invoices_pay`
- **Operation ID:** `portal.invoices.pay`
- **Scope:** Brand
- **Permission:** `portal:write`
- **Risk tier:** `financial`
- **Dry run:** Supported (send `Ownrecord-Dry-Run: true`)
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** A customer signed in to the customer portal (API keys can't call it)
- **Since:** M9

## Request

### Path parameters

| Field       | Type       | Description                     |
| ----------- | ---------- | ------------------------------- |
| `invoiceId` | `inv_…` ID | One of your invoices (`inv_…`). |

### Body

A JSON object (`Content-Type: application/json`):

| Field                     | Type   | Required | Description                                                                                              |
| ------------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------- |
| `expectedAmount`          | object | Yes      | The amount due you saw; if it changed meanwhile, the payment is refused (`invoice_payment_unavailable`). |
| `expectedAmount.amount`   | string | Yes      | Integer micros as a string.                                                                              |
| `expectedAmount.currency` | string | Yes      |                                                                                                          |
| `paymentMethod`           | string | No       | Fake processor only (development, CI): the test card, e.g. `pm_fake_visa`.                               |

## Response

`200 OK` with a JSON object:

| Field            | Type                                                                              | Description                                                                                                                                                                                                                                                                                                                             |
| ---------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object`         | `"hosted_invoice_payment"`                                                        |                                                                                                                                                                                                                                                                                                                                         |
| `invoiceId`      | `inv_…` ID                                                                        |                                                                                                                                                                                                                                                                                                                                         |
| `outcome`        | one of `succeeded`, `processing`, `requires_action`, `client_confirm`, `redirect` | `succeeded` (the invoice is marked paid when the processor's webhook arrives), `processing`, `requires_action` (confirm again: a fake 3-D Secure challenge), `client_confirm` (Stripe: confirm in the Payment Element with `clientSecret`) or `redirect` (Mollie: send the buyer to `redirectUrl`; they come back to the invoice page). |
| `amount`         | money                                                                             |                                                                                                                                                                                                                                                                                                                                         |
| `clientSecret`   | string or null                                                                    |                                                                                                                                                                                                                                                                                                                                         |
| `publishableKey` | string or null                                                                    |                                                                                                                                                                                                                                                                                                                                         |
| `redirectUrl`    | string or null                                                                    |                                                                                                                                                                                                                                                                                                                                         |

## Examples

### Pay with a fake test card

```http
POST /api/v1/portal/me/invoices/inv_01m4tx5nj8emwbyf7y9vcf48b3/pay
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "expectedAmount": {
    "amount": "1200000000",
    "currency": "EUR"
  },
  "paymentMethod": "pm_fake_visa"
}
```

Response (`200`):

```json
{
  "object": "hosted_invoice_payment",
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
  "outcome": "succeeded",
  "amount": {
    "amount": "1200000000",
    "currency": "EUR"
  },
  "clientSecret": null,
  "publishableKey": null,
  "redirectUrl": null
}
```

## Errors

| Code                                                                                                       | HTTP status | When                                                                                                               | What to do                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                     | 400         | The invoice's ID or another field is malformed.                                                                    | Fix the fields listed in `details.issues` and send the request again.                                                                                                                      |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)                   | 404         | You have no such invoice (another customer's reads the same).                                                      | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                                                                       |
| [`invoice_payment_unavailable`](https://ownrecord.io/docs/reference/errors.md#invoice_payment_unavailable) | 409         | The invoice is paid or voided, has nothing due, its amount due changed, or the brand has no processor.             | The invoice is paid, voided or has nothing due, or the brand has no processor connection: check `details.reason`. Pay by bank transfer with the details on the invoice, or ask the seller. |
| [`processor_payment_declined`](https://ownrecord.io/docs/reference/errors.md#processor_payment_declined)   | 502         | The card was declined; try another card or a bank transfer.                                                        | Ask the customer for another payment method; `details.decline_code` says why the card was declined.                                                                                        |
| [`processor_request_failed`](https://ownrecord.io/docs/reference/errors.md#processor_request_failed)       | 502         | The processor couldn't create or confirm the payment; try again.                                                   | If `retryable` is true, retry with the same Idempotency-Key after a short wait; otherwise check `details.code`.                                                                            |
| [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed)               | 403         | The portal session is in the other mode than the request.                                                          | Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.                                                                |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)                     | 403         | The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations). | Ask an organization owner or admin for a role or scope that allows this action.                                                                                                            |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
