# Capture a PayPal order

URL: /docs/api/storefront/checkout/capture-a-paypal-order

## POST /v1/checkout/paypal/capture

```http
POST https://api.elaraone.app/api/v1/checkout/paypal/capture
```

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Cart-Token` | header | string | yes | The cart token from POST /api/v1/cart. |
| `X-Client-IP` | header | string |  | Optional. IP address of the shopper (one IPv4/IPv6, no port, no list). Honoured only together with a valid X-Tenant-Secret; ignored otherwise. Used for rate limits and recorded with form submissions. |
| `X-Tenant-Secret` | header | string |  | Optional here. Server secret of the store: server-to-server only, never from a browser. Enables X-Client-IP. |
| `Idempotency-Key` | header | string |  | Optional; recommended (a UUID made once per user action, reused only to retry that action). Only a success is stored (24 hours): the same key with the same body, URI and credentials (Authorization, X-Cart-Token, X-Chat-Token) replays it (`Idempotent-Replayed: true`); anything else under that key is 422 `IDEMPOTENCY_KEY_REUSED`. An error is never stored: after a 4xx or 5xx, retrying with the same key runs the request again. |
| `X-Session-Id` | header | string |  | Optional. The shopper's `sid` cookie value (8-128 of letters, digits, `.` `_` `:` `-`). Recorded on the basket as a SHA-256 hash only (never the raw value), so the order can be checked against the consent in force when it is placed, even when Stripe or PayPal settle it later without a request. Send it on every basket write; an absent header keeps the stored hash, and a malformed one is ignored. |
| `X-Visitor-Id` | header | string |  | Optional. The `vid` cookie value, when the shopper has one. Recorded on the basket as a SHA-256 hash only (never the raw value), so the order can be checked against the consent in force when it is placed, even when Stripe or PayPal settle it later without a request. Send it on every basket write; an absent header keeps the stored hash, and a malformed one is ignored. |
| `X-Consent-Token` | header | string |  | Optional. The `consent` cookie value (the `consent_token` from `PUT consent`). Recorded on the basket as a SHA-256 hash only (never the raw value), so the order can be checked against the consent in force when it is placed, even when Stripe or PayPal settle it later without a request. Send it on every basket write; an absent header keeps the stored hash, and a malformed one is ignored. |
| `X-GA-Client-Id` | header | string |  | Optional. The GA4 client id from the `_ga` cookie that the Google tag of the merchant set: its last two numbers (`GA1.1.1234567890.1699999999` → `1234567890.1699999999`, `^\d{1,10}\.\d{1,10}$`). Send it only when the shopper has granted analytics AND advertising. Kept with the basket and the order only while both are in force (removed when either is withdrawn, on erasure, and after its window), and sent to Google Analytics as the `client_id` of the purchase, so GA4 joins it to the visit. An absent header keeps the stored id; a malformed one is ignored (never a 422). |

### Request body (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `paypal_order_id` | string | yes |  |

### Responses

- **201**: Success.
- **402**: Payment required. Branch on `error.code`: - `TENANT_PAYMENT_REQUIRED`: this store is on payment hold. Show a "store temporarily unavailable" page; do not retry the request. - `PAYMENT_FAILED`: the payment gateway declined or could not capture the payment; the cart and stock are unchanged and the shopper may try again. When the payment provider refused the payment (a card Stripe declined, 3-D Secure not finished, a funding source PayPal refused), `error.details.decline_code` says why: `card_declined`, `card_expired`, `insufficient_funds`, `incorrect_card_details`, `authentication_required`, `try_another_method`, `unknown`. Word the error from it, or reopen the PayPal buttons for `try_another_method`; treat a value you do not know as `unknown`. Other refusals (the basket total changed, the card authorisation was released, a payment made for another store) have no `details`: show `error.message`.
- **404**: Not found: `FEATURE_NOT_ENABLED` when this store does not have the Shop feature on.
- **409**: The PayPal payment was taken but did not become an order. Branch on `error.code`; `error.details.reason` says why when known: - `PAYMENT_REFUNDED`: the order could no longer be placed (stock gone, cart changed since the shopper paid) and the payment has been refunded. Start a new checkout. - `PAYMENT_REFUND_PENDING`: the order could not be placed and the automatic refund is being retried; the store has been notified. Do not ask the shopper to pay again for this cart. - `PAYMENT_ALREADY_USED`: this PayPal order already paid for another checkout (another cart, or an order already placed from it). Also 409 `PROMOTIONS_CHANGED`: the basket's discounts are no longer what this payment was set up for (an offer ended or changed, a code or offer ran out of uses, this customer reached a code's limit, a first-order offer no longer applies). Nothing was charged. `error.details`: `reason` (`changed` \| `used_up` \| `limit_reached` \| `first_order`), `promotion_ids`, `coupon`, and `cart` (the recalculated basket, the `GET cart` shape). Show the new total and set the payment up again. The same refusal on the direct (offline / test) payment path. A card payment Stripe already took in the browser is refunded instead (`PAYMENT_REFUNDED`). Also 409 `IDEMPOTENCY_IN_PROGRESS`: a request with this Idempotency-Key is still being processed; retry shortly with the same key.
- **422**: Validation error Also 422 `IDEMPOTENCY_KEY_REUSED`: this Idempotency-Key already belongs to a different request (body, URI or credentials).