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

# Accept in-person terminal payments

> Collect credit or PIN-debit payments on a physical or simulated terminal.

Terminal payments collect a card-present payment on a specific Terminal using
the same PaymentIntent as online payments. The PaymentIntent carries the amount
and currency; the terminal request selects the device and the tender. Use a
simulated Terminal for repeatable automated testing, then run the same payment
flow on a physical Terminal.

<Note>
  Terminal payments require an Ionic `tmr_…` ID owned by the same Merchant and
  mode as your secret key. You can create a simulated `tmr_…` with a test key;
  Ionic assigns physical Terminal IDs through the hardware provisioning flow.
  Send only the Ionic ID; never send a hardware serial number or device
  credential. See [the Terminal object](/concepts/terminal-object) for its
  lifecycle, connection, and capability states.
</Note>

## Before you begin

You need:

* a server-side Ionic secret key with `payments:read` and `payments:write`;
* a Location in the same Merchant and mode, identified by `loc_…`;
* a simulated test Terminal or an assigned physical Terminal that is ready for
  the requested tender;
* the terminal's Ionic ID, which starts with `tmr_`;
* a unique `Idempotency-Key` for each mutating POST request; reuse a value only
  to retry the same request. The read-only batch refresh does not use one.

Keep keys on your backend. Your browser or POS client should call your server,
and your server should call Ionic.

## Request physical hardware

Create a provisioning request when a Merchant needs a physical Terminal. The
request tracks hardware fulfillment and eventually points to the Ionic
Terminal resource used by the payment API. The
[Terminal object guide](/concepts/terminal-object) explains how to interpret
its independent setup, connection, and capability states.

<Steps>
  <Step title="Create or reuse a Location">
    Create the business Location where the Terminal will operate, or reuse an
    existing `loc_…` in the same Merchant and mode.
  </Step>

  <Step title="Request a Terminal">
    Use a secret key with `terminals:write`. Ionic selects and prepares the
    appropriate hardware; the request body contains only the Location, a
    merchant-facing label, and optional metadata.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/terminal_provisioning_requests \
      -H "Authorization: Bearer $IONIC_SECRET_KEY" \
      -H "Idempotency-Key: request-front-counter-terminal-1" \
      -H "Content-Type: application/json" \
      -d '{
        "location_id": "'$LOCATION_ID'",
        "label": "Front counter",
        "metadata": {"lane": "1"}
      }'
    ```

    Store the returned `tprov_…` as `PROVISIONING_REQUEST_ID`.
  </Step>

  <Step title="Track fulfillment">
    Retrieve the provisioning request until it becomes `ready`. If it becomes
    `requires_action`, show the additive `required_actions` values to the
    Merchant and continue retrieving the same request after they are complete.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/terminal_provisioning_requests/$PROVISIONING_REQUEST_ID \
      -H "Authorization: Bearer $IONIC_SECRET_KEY"
    ```

    The request can move through `requested`, `processing`,
    `requires_action`, `ready`, `failed`, or `canceled`. A `ready` request has
    a non-null `terminal_id`.
  </Step>

  <Step title="Check Terminal readiness">
    Retrieve the assigned `tmr_…` and begin payments only when its lifecycle
    status and the requested payment capability are both `ready`.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/terminals/$TERMINAL_ID \
      -H "Authorization: Bearer $IONIC_SECRET_KEY"
    ```
  </Step>
</Steps>

List provisioning requests with `GET /terminal_provisioning_requests` and
Terminal inventory with `GET /terminals`. Both collections are cursor-paginated
and scoped to the authenticated Merchant and test or live mode.

## Test without physical hardware

A simulated Terminal is a durable Merchant resource for local development, CI,
staging, and SDK integration tests. Create a test Location and Terminal once,
then reuse their `loc_…` and `tmr_…` IDs across test runs. The Terminal is
immediately ready for credit and PIN-debit requests and never creates a
physical provisioning request.

The test helper configures what the Terminal observes; it does not return a
canned API response. Processing still creates and transitions normal Ionic
PaymentIntent, Terminal Payment, Charge, PaymentMethod, Refund, batch, profile,
item, idempotency, and webhook records. This makes status handling and resource
relationships representative while keeping the card interaction deterministic.

<Steps>
  <Step title="Create or reuse a test Location">
    Use a test-mode secret key with `locations:write`. The key determines the
    Location's mode; do not send `livemode` in the body. Test and live
    Locations are separate resources.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/locations \
      -H "Authorization: Bearer $IONIC_TEST_SECRET_KEY" \
      -H "Idempotency-Key: create-ci-location-1" \
      -H "Content-Type: application/json" \
      -d '{
        "display_name": "CI checkout lab",
        "address": {
          "line1": "401 N Morgan St",
          "city": "Chicago",
          "state": "IL",
          "postal_code": "60642",
          "country": "US"
        },
        "timezone": "America/Chicago",
        "merchant_category_code": "5812",
        "external_reference": "ci-checkout-lab"
      }'
    ```

    Store the returned `loc_…` as `LOCATION_ID`. A Location is Ionic's record
    of your business location, not a hardware identifier. You can list,
    retrieve, and update it through the Location API; there is no delete
    endpoint.
  </Step>

  <Step title="Create a simulated Terminal">
    Use the same test-mode key with `terminals:write` and the Location from the
    previous step.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/terminals \
      -H "Authorization: Bearer $IONIC_TEST_SECRET_KEY" \
      -H "Idempotency-Key: create-ci-terminal-1" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "simulated",
        "location_id": "'$LOCATION_ID'",
        "label": "CI checkout reader",
        "metadata": {"test_suite": "checkout"}
      }'
    ```

    Store the returned `tmr_…` as `TERMINAL_ID`. You do not need to create a
    new Terminal for every test or payment.
  </Step>

  <Step title="Create a PaymentIntent">
    Create a fresh method-neutral PaymentIntent for the logical payment attempt,
    exactly as shown in the normal payment flow below. A scenario is scoped to
    one exact PaymentIntent and Terminal.
  </Step>

  <Step title="Arm the expected outcome">
    Arm one scenario before processing the PaymentIntent. This example arms a
    decline for insufficient funds.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/test_helpers/terminals/$TERMINAL_ID/payment_scenarios \
      -H "Authorization: Bearer $IONIC_TEST_SECRET_KEY" \
      -H "Idempotency-Key: scenario-order-1042-1" \
      -H "Content-Type: application/json" \
      -d '{
        "payment_intent_id": "pi_01JEXAMPLEF9W6B3C2D7K4M8N",
        "outcome": "declined",
        "failure_code": "insufficient_funds"
      }'
    ```

    Supported immediate outcomes are `succeeded`, `voided`, `declined`,
    `failed`, `canceled`, `unknown`, and `dispatch_not_started`. Declined and
    failed outcomes require a compatible `failure_code`.
  </Step>

  <Step title="Run the normal Terminal payment call">
    Call `POST /terminals/{id}/process_payment_intent` with the same Terminal
    and PaymentIntent IDs. The request has no simulator-only fields, so the
    same integration works with physical hardware.
  </Step>
</Steps>

A scenario persists until the next new command matching both IDs consumes it.
Retrying that consumed command with the same idempotency key is safe;
a different payment attempt cannot reuse the scenario. Only one unconsumed
scenario can be armed for an exact Terminal and PaymentIntent. A command with
no matching scenario returns a failed `terminal_payment` with
`failure_code: dispatch_not_started`.

To test reconciliation, set `outcome` to `unknown` and include a final
`resolution_outcome` plus `resolution_delay_seconds` from 1 through 86400.
Ionic first returns the attempt with an `unknown` result; retrieve that same
`terminal_payment` until the configured delay passes. Ionic reconciles the
result automatically, usually within about one second of the configured time.
Never submit a second Sale to resolve an unknown result.

Cancellation uses the normal
`POST /terminal_payments/{id}/cancel` endpoint. An unresolved `unknown`
scenario without a configured final result becomes `canceled`. If the scenario
already has a later `resolution_outcome`, that result remains authoritative,
which lets you test a late-approval race after cancellation was requested.

### Simulate current batches

Configure a durable current-batch situation, then call the same batch refresh
endpoint used with a physical Terminal. Keep a profile's `reference` stable
and submit that profile again to replace its items for the next observation.
Profiles omitted from an update remain unchanged, so an open batch cannot
disappear. Up to 10 profiles with 1,000 items per profile can be retained.

```bash theme={null}
curl https://api.ionicfi.com/v1/test_helpers/terminals/$TERMINAL_ID/batch_scenarios \
  -H "Authorization: Bearer $IONIC_TEST_SECRET_KEY" \
  -H "Idempotency-Key: batch-scenario-closeout-1" \
  -H "Content-Type: application/json" \
  -d '{
    "profiles": [{
      "reference": "default",
      "items": [
        {
          "operation": "sale",
          "amount": 1250,
          "transaction_id": "sim_sale_1",
          "external_reference": "order_123",
          "sequence": "1"
        },
        {
          "operation": "refund",
          "amount": 250,
          "transaction_id": "sim_refund_1",
          "external_reference": "refund_123",
          "sequence": "2"
        }
      ]
    }]
  }'

curl https://api.ionicfi.com/v1/terminal_batches/refresh \
  -H "Authorization: Bearer $IONIC_TEST_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"terminal_id":"'$TERMINAL_ID'"}'
```

The refresh creates or updates normal `terminal_batch`, settlement-profile,
and batch-item resources. With no configured batch scenario, a simulated
Terminal reports an empty `default` profile instead of returning an unsupported
operation. The simulator also supports the normal `POST /refunds` flow for a
succeeded Terminal payment; refund resources and state transitions are handled
by the same Ionic payment lifecycle.

## Payment flow

<Steps>
  <Step title="Create a PaymentIntent">
    Create the same PaymentIntent used by other Ionic payment flows. Do not
    attach a `payment_method`, send a `payment_token`, or confirm it first. The
    intent must remain in `requires_payment_method`, use USD, and use automatic
    capture.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/payment_intents \
      -H "Authorization: Bearer $IONIC_SECRET_KEY" \
      -H "Idempotency-Key: pos-order-1042-intent" \
      -H "Content-Type: application/json" \
      -d '{
        "amount": 123,
        "currency": "usd",
        "capture_method": "automatic"
      }'
    ```
  </Step>

  <Step title="Process it on a terminal">
    Select the Ionic terminal ID in the route and pass the PaymentIntent ID in
    the request. `payment_type` selects the tender behavior on the terminal,
    `credit` or PIN-based `debit`. It is not a PaymentMethod type.

    ```bash theme={null}
    curl https://api.ionicfi.com/v1/terminals/$TERMINAL_ID/process_payment_intent \
      -H "Authorization: Bearer $IONIC_SECRET_KEY" \
      -H "Idempotency-Key: pos-order-1042-terminal-attempt-1" \
      -H "Content-Type: application/json" \
      -d '{
        "payment_intent_id": "pi_01JEXAMPLEF9W6B3C2D7K4M8N",
        "payment_type": "credit"
      }'
    ```

    For PIN debit, send `"payment_type": "debit"`. The selected terminal must
    have a ready PIN-debit capability.
  </Step>

  <Step title="Inspect the Terminal Payment">
    HTTP `200` means Ionic recorded the attempt and returned a
    `terminal_payment`; it does not by itself mean the payment was approved.
    Complete the order only when `status` is `succeeded`.

    ```json theme={null}
    {
      "id": "tpr_01JEXAMPLE2K8V4N9C6M1Q7R",
      "object": "terminal_payment",
      "merchant_id": "mer_01JEXAMPLE7A5P9D3F8K2N4Q",
      "terminal_id": "tmr_01JEXAMPLE8H2Q91P4T6K0M3N5",
      "payment_intent_id": "pi_01JEXAMPLEF9W6B3C2D7K4M8N",
      "charge_id": "ch_01JEXAMPLEC4V8B2N6M9K3P7",
      "livemode": false,
      "amount_requested": 123,
      "amount_approved": 123,
      "amount_remaining": 0,
      "currency": "usd",
      "payment_type": "credit",
      "status": "succeeded",
      "cancellation_requested": false,
      "cancellation_requested_at": null,
      "failure_code": null,
      "failure_message": null,
      "created_at": 1786939200,
      "updated_at": 1786939212
    }
    ```
  </Step>
</Steps>

## Handle every status

| Status                    | Meaning                                     | Safe next action                                                                                                                                                                                                                                        |
| ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `succeeded`               | Approved                                    | Complete the sale.                                                                                                                                                                                                                                      |
| `voided`                  | Authorized, then voided; no funds settle    | Offer another tender if payment is still needed.                                                                                                                                                                                                        |
| `declined`                | Explicitly declined                         | Show `failure_message` and offer another tender.                                                                                                                                                                                                        |
| `failed`                  | Known processing failure                    | Show `failure_message`, then retry with a new `Idempotency-Key` or use another tender.                                                                                                                                                                  |
| `canceled`                | Buyer canceled or the interaction timed out | If `cancellation_requested` is `true`, the PaymentIntent is canceled: create a new PaymentIntent if payment is still needed. Otherwise, retry the same PaymentIntent only after it returns to `requires_payment_method`, using a new `Idempotency-Key`. |
| `pending` / `in_progress` | The attempt is still running                | Retrieve the same Terminal Payment until it reaches a final status.                                                                                                                                                                                     |
| `unknown`                 | The financial outcome is not yet confirmed  | Retrieve the same attempt until it resolves; do not start another sale.                                                                                                                                                                                 |

Declines and failures are returned as Terminal Payment resources with HTTP
`200`. Branch on `status`; treat `failure_code` as display detail, because new
codes can appear without a new `status` value.

### Failure codes

`failure_code` is nullable and appears only when Ionic has a known
nonapproval. These are stable Ionic values; raw device responses are not
returned.

| Status and code                          | Meaning                                                           | Safe next action                                                     |
| ---------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- |
| `failed` / `dispatch_not_started`        | Ionic rejected the request before payment processing began.       | Correct the configuration or input, then retry deliberately.         |
| `failed` / `processor_error`             | Ionic confirmed a final processing failure.                       | Show the message, then retry deliberately or request another tender. |
| `failed` / `invalid_routing`             | The Merchant's payment setup is invalid.                          | Correct the payment setup before retrying.                           |
| `failed` / `pin_processing_error`        | PIN processing failed.                                            | Correct the Terminal configuration or request another tender.        |
| `declined` / `do_not_honor`              | The payment received a generic decline.                           | Request another tender.                                              |
| `declined` / `insufficient_funds`        | The payment was declined for insufficient funds.                  | Request another tender.                                              |
| `declined` / `incorrect_pin`             | The payment was declined for an incorrect PIN.                    | Allow another PIN attempt only when your payment flow permits it.    |
| `declined` / `pin_attempts_exceeded`     | The PIN retry limit was reached.                                  | Request another tender; do not encourage another PIN attempt.        |
| `declined` / `transaction_not_permitted` | This transaction is not permitted for the selected payment setup. | Request another tender or correct Merchant enablement.               |

New `failure_code` values are additive. Use `status` as the workflow boundary
and fall back to `failure_message` when a code is unfamiliar.

## Handle API errors

An API error means Ionic could not return a normal Terminal Payment response.
Most errors below happen before a Sale is sent, but a timeout or persistence
failure can leave the financial outcome uncertain.

| HTTP and `error.code`                                                  | Payment submitted?           | Safe next action                                                                                                              |
| ---------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `400 INVALID_REQUEST`                                                  | Not sent                     | Correct the JSON or request shape.                                                                                            |
| `400 INVALID_DATA`                                                     | Not sent                     | Correct the identifier, currency, or requested tender.                                                                        |
| `401 AUTHENTICATION_REQUIRED`, `API_KEY_INVALID`, or `API_KEY_REVOKED` | Not sent                     | Correct authentication.                                                                                                       |
| `403 INSUFFICIENT_PERMISSIONS`                                         | Not sent                     | Grant the required `payments:read` or `payments:write` permission.                                                            |
| `403 MERCHANT_INACTIVE`                                                | Not sent                     | Restore Merchant eligibility.                                                                                                 |
| `403 LIVE_PAYMENTS_NOT_ENABLED`                                        | Not sent                     | Complete live enablement or use test mode.                                                                                    |
| `404 PAYMENT_INTENT_NOT_FOUND`                                         | Not sent                     | Correct the PaymentIntent ID, Merchant, or mode.                                                                              |
| `404 TERMINAL_NOT_FOUND`                                               | Not sent                     | Correct the Terminal ID, Merchant, mode, or assignment.                                                                       |
| `409 TERMINAL_BUSY`                                                    | Not sent by this request     | Wait for the active Terminal command to finish.                                                                               |
| `409 PAYMENT_PROCESSING`                                               | Not sent by this request     | Retrieve the existing attempt; do not start another Sale.                                                                     |
| `422 INVALID_PAYMENT_STATE`                                            | Not sent                     | Inspect and correct the PaymentIntent state or capture method.                                                                |
| `422 TERMINAL_NOT_READY`                                               | Not sent                     | Finish Terminal setup or enable the requested capability.                                                                     |
| `429 RATE_LIMIT_EXCEEDED`                                              | Not sent                     | Honor `Retry-After` and retry the identical logical request with the same idempotency key.                                    |
| `503 TERMINAL_OFFLINE`                                                 | Not sent                     | Restore connectivity, then retry deliberately.                                                                                |
| `503 TERMINAL_CONNECTION_UNAVAILABLE`                                  | Not sent                     | Verify the Terminal assignment, registration, and payment setup.                                                              |
| `503 SERVICE_UNAVAILABLE`                                              | Not sent                     | Retry after the control-plane dependency recovers.                                                                            |
| `504 GATEWAY_UNCERTAIN`                                                | May have been sent           | Do not create another Sale. Inspect the PaymentIntent and any known `tpr_…`, retain the idempotency key, and contact support. |
| `500 INTERNAL_ERROR`                                                   | Do not infer from HTTP alone | Inspect the PaymentIntent and any known `tpr_…`; do not blindly create another Sale.                                          |

<Warning>
  A client timeout or disconnected HTTP socket is not evidence that the
  Terminal payment failed. Reuse an idempotency key only for the identical
  route and body, inspect the PaymentIntent, and retrieve any known `tpr_…`
  before considering a new attempt. Include the response `X-Request-ID` when
  contacting support.
</Warning>

## Retrieve an unresolved attempt

If the result is `pending`, `in_progress`, or `unknown`, retrieve the same
`tpr_…` resource. Do not process the PaymentIntent again to resolve a timeout.

```bash theme={null}
curl https://api.ionicfi.com/v1/terminal_payments/$TERMINAL_PAYMENT_ID \
  -H "Authorization: Bearer $IONIC_SECRET_KEY"
```

Retrieval reads Ionic's latest durable state; the GET request does not itself
send a command to the Terminal. Ionic can update an `unknown` attempt when the
original payment reaches a confirmed result, and the resulting state is
available through this GET and terminal-payment webhooks. Retrieval never
submits a new Sale, so poll it as often as your integration requires.

## Request cancellation

Cancel only an active attempt, and treat cancellation as a request rather than
a financial outcome.

```bash theme={null}
curl -X POST \
  https://api.ionicfi.com/v1/terminal_payments/$TERMINAL_PAYMENT_ID/cancel \
  -H "Authorization: Bearer $IONIC_SECRET_KEY" \
  -H "Idempotency-Key: pos-order-1042-cancel-1"
```

`cancellation_requested: true` confirms that Ionic recorded the request.
Continue inspecting `status`: the payment can still be approved after you
request cancellation. Refund a late approval separately if you no longer want
the payment.

## Platforms and connected Merchants

An authorized Platform acting for a connected Merchant adds the delegation
header to every request:

```http theme={null}
Ionic-Account: mer_01JEXAMPLE7A5P9D3F8K2N4Q
```

The PaymentIntent, Terminal, Charge, and Terminal Payment must all belong to
that account and use the same test or live mode. Identifiers that belong to
another account or mode return `404`.

## Test before going live

Use a test-mode key and a reusable simulated Terminal for automated coverage.
Cover an approval, decline, failed dispatch, delayed unknown result, retried
request with the same `Idempotency-Key`, cancellation, refund, a batch refresh,
and retrieval of the resulting resources. Then run acceptance testing on an
Ionic-assigned physical test Terminal to validate the device, store network,
PIN entry, and live account setup before enabling production hardware.

<Warning>
  The simulator proves your integration and state handling; it does not
  certify a physical reader, PIN entry, receipt behavior, or store network.
  Keep physical-terminal acceptance testing as a separate release gate.
</Warning>
