> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orqex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom checkout

> Build your own payment UI using the direct API: discover available methods, create an attempt, and handle the outcome.

The direct API gives you full control over the payment experience. You call the discovery endpoints to learn what is available for this intent, present the choice to your customer, create an attempt, and handle the result.

<Warning>
  Never hard-code country codes or method codes in your integration. The discovery endpoints are the contract — only methods returned at runtime are guaranteed to be routable for this intent.
</Warning>

## Flow

<Steps>
  <Step title="Create a payment intent">
    ```
    POST /payment/intents
    ```

    Required fields: `amount`, `currency`, `description`, `customer`. See [full field reference](#intent-fields) below.

    ```bash theme={null}
    curl -X POST https://api.orqex.com/v1/payment/intents \
      -H "Authorization: Bearer sk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "amount": 100,
        "currency": "EUR",
        "description": "Subscription renewal",
        "customer": {
          "email": "jan@example.de",
          "first_name": "Jan",
          "last_name": "Müller"
        }
      }'
    ```

    The response returns the intent with `status: pending`. Save the intent `id` — you will use it in every subsequent call.
  </Step>

  <Step title="Discover available countries">
    ```
    GET /payment/intents/{intentId}/countries
    ```

    Returns the list of countries where at least one method is available for this intent.

    ```json theme={null}
    {
      "data": [
        { "code": "DE", "name": "Germany", "flag": "🇩🇪" },
        { "code": "US", "name": "United States", "flag": "🇺🇸" }
      ],
      "meta": {
        "total": 2,
        "supports_any_country": false
      }
    }
    ```

    `meta.supports_any_country: true` means the intent can be attempted without selecting a specific country — present the method list without a country selector in that case.

    Both discovery endpoints return `422` when the intent is already in a final status.
  </Step>

  <Step title="Discover available methods for a country">
    ```
    GET /payment/intents/{intentId}/countries/{countryCode}/methods
    ```

    `countryCode` is a two-letter code (e.g. `DE`). An optional `currency` query parameter filters to methods that support a specific currency.

    ```json theme={null}
    {
      "data": [
        {
          "value": "card",
          "label": "Card",
          "description": "Pay with a credit or debit card",
          "icon_url": "https://...",
          "category": "card",
          "requires_phone": false
        }
      ]
    }
    ```

    Present the returned methods to your customer. Do not filter or hardcode them.
  </Step>

  <Step title="Create an attempt">
    ```
    POST /payment/intents/{intentId}/attempts
    ```

    Required: `method_code` (the `value` from the methods response), `country` (2-letter code), `phone` (a valid phone number object).
    Optional: `currency`.

    ```bash theme={null}
    curl -X POST https://api.orqex.com/v1/payment/intents/pi_.../attempts \
      -H "Authorization: Bearer sk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "method_code": "card",
        "country": "DE",
        "phone": { "number": "+4915100000000", "country": "DE" }
      }'
    ```

    The response returns the **updated payment intent** (not the attempt), including the active attempt with its `next_action` and `status`.
  </Step>

  <Step title="Handle the next action">
    Check `active_attempt.next_action.type` on the returned intent. See [Next actions](/payments/next-actions) for the complete reference.

    If the type is `collect_otp` or `complete_with_sdk`, you must call confirm or authorize once the customer supplies the required input. All other types resolve asynchronously — wait for the webhook or poll.
  </Step>

  <Step title="Confirm if required">
    Use **authorize** when you do not have the specific attempt id:

    ```
    POST /payment/intents/{intentId}/authorize
    ```

    Use **confirm** when you have the attempt id:

    ```
    POST /payment/intents/{intentId}/attempts/{attemptId}/confirm
    ```

    Both accept: `otp` (string, max 10 characters), `confirmation_data` (object). Both return the updated payment intent.
  </Step>

  <Step title="Check the final status">
    Poll `GET /payment/intents/{intentId}` or listen for the `payment.completed` / `payment.failed` webhook. See [Webhooks](/webhooks).

    A failed attempt does not fail the intent — while the intent remains `pending`, you may start another attempt.
  </Step>
</Steps>

## Intent fields

### POST /payment/intents

**Required:**

| Field                 | Type   | Notes                                      |
| --------------------- | ------ | ------------------------------------------ |
| `amount`              | number | Major units (e.g. `100` = 100.00). Min `1` |
| `currency`            | string | ISO 4217, 3 uppercase characters           |
| `description`         | string | Max 500 characters                         |
| `customer.email`      | string | Required                                   |
| `customer.first_name` | string | Max 100 characters                         |
| `customer.last_name`  | string | Max 100 characters                         |

**Optional:**

| Field                  | Type   | Notes                                                                |
| ---------------------- | ------ | -------------------------------------------------------------------- |
| `customer.address`     | string | Max 200 characters                                                   |
| `customer.city`        | string | Max 100 characters                                                   |
| `customer.state`       | string | Max 100 characters                                                   |
| `customer.country`     | string | Max 5 characters                                                     |
| `customer.zip`         | string | —                                                                    |
| `return_url`           | string | URL, max 2048 characters                                             |
| `webhook_url`          | string | URL, max 2048 characters                                             |
| `statement_descriptor` | string | Max 22 characters. `A–Z a–z 0–9 space . , - +`                       |
| `receipt_email`        | string | Max 255 characters                                                   |
| `entity_id`            | string | Your internal reference. Max 191 characters                          |
| `entity_created_at`    | date   | Must not be in the future                                            |
| `gateway_options`      | object | See [Gateway options](#gateway-options)                              |
| `metadata`             | object | Up to 10 key/value pairs                                             |
| `attempt`              | object | Start an attempt immediately — see [Inline attempt](#inline-attempt) |

## Inline attempt

If you already know the method and country, you can start an attempt in the same call as intent creation by including an `attempt` object:

```json theme={null}
{
  "amount": 100,
  "currency": "EUR",
  "description": "Subscription renewal",
  "customer": { ... },
  "attempt": {
    "method_code": "card",
    "country": "DE",
    "phone": { "number": "+4915100000000", "country": "DE" }
  }
}
```

This creates the intent and immediately runs the first attempt. The response is the same as `POST /payment/intents`, with the active attempt already populated.

## Inspect an attempt

For support and debugging, you can retrieve the raw record held by the underlying provider:

```
GET /payment/intents/{intentId}/attempts/{attemptId}/inspect
```

<ResponseField name="gateway" type="string">
  The gateway that processed this attempt.
</ResponseField>

<ResponseField name="gateway_transaction_id" type="string">
  The provider's transaction identifier.
</ResponseField>

<ResponseField name="retrieved_at" type="string">
  ISO 8601 timestamp of when the raw record was fetched.
</ResponseField>

<ResponseField name="payload" type="object">
  The raw record returned by the underlying provider, unmodified.
</ResponseField>

<Note>
  This endpoint is for diagnostics. Do not build business logic on `payload` — its structure varies by provider and may change without notice.
</Note>

## Gateway options

`gateway_options` is a free-form object keyed by gateway code. Pass it when you need to send gateway-specific parameters that do not have a first-class field in the intent.

Unknown gateway codes and gateways that accept no options are rejected with a `422` validation error on `gateway_options.{code}`.

The following gateways currently accept options:

| Gateway   | Field                      | Type    | Notes                                                                                                     |
| --------- | -------------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `stripe`  | `managed`                  | boolean | Stripe Managed Payments (merchant of record)                                                              |
| `stripe`  | `tax_code`                 | string  | Must start with `txcd_`. Max 255 characters                                                               |
| `pawapay` | `submerchant_legal_name`   | string  | Max 255 characters                                                                                        |
| `pawapay` | `submerchant_segment`      | string  | One of the segments PawaPay defines                                                                       |
| `test`    | `pending_duration_seconds` | integer | Sandbox only. How long a pending outcome stays pending before it settles. Range 1–600. Default 20 seconds |

```json theme={null}
{
  "gateway_options": {
    "stripe": {
      "managed": true,
      "tax_code": "txcd_10000000"
    }
  }
}
```
