# Integrate Stripe Terminal into point-of-sale and property management systems

A sequential guide for POS and PMS partners covering integration setup, payment flows, offline mode, testing, and best practices.

This guide covers each step of a Stripe Terminal integration for point-of-sale (POS) and property management system (PMS) partners. It includes integration setup, payment flow, offline mode, testing, and best practices.

## Stripe Terminal overview

A Stripe Terminal deployment consists of four components:

1. Your POS or PMS application (web, mobile, or desktop)
2. Your backend
3. A Stripe Terminal reader
4. The Stripe Terminal SDK

The SDK facilitates communication between your POS application, the firmware running on the reader, and the Stripe API so you can accept in-person payments the same way you accept online payments with Stripe.

## Choose your integration

When choosing an integration shape, consider the following:

- **Offline mode**: If your users need to accept payments without an internet connection (store and forward), choose the Android SDK, iOS SDK, React Native SDK, Java SDK, or .NET SDK. The JavaScript SDK and server-driven integration don’t support offline mode.
- **Device connection type**: If your users require a wired Ethernet connection, choose an SDK that supports the Stripe T600, BBPOS WisePOS E, or Stripe S700/S710 readers.
- **Reader selection**: The iOS, Android, and React Native SDKs support all readers, including Tap to Pay. The server-driven integration, Java SDK, and .NET SDK support only smart readers (T600, S700, WisePOS E, and Verifone devices).

| Capability | Mobile SDKs (iOS, Android, React Native) | JavaScript | Desktop SDKs (.NET, Java) | Server-driven |
| --- | --- | --- | --- | --- |
| Offline payments | Yes | No | Yes | No |
| Stripe T600, S700, S710, BBPOS WisePOS E, Verifone M425/P630/V660p/UX700 | Yes | Yes | Yes | Yes |
| BBPOS WisePad 3, Stripe M2 | Yes (not USB) | No | .NET: No, Java: Yes | No |
| Tap to Pay | Yes | No | No | No |

For detailed guidance on each integration shape and reader, see [set up a reader](https://docs.stripe.com/terminal/payments/setup-reader.md#sdk).

**Server-driven integration**: Your POS or PMS interacts with Stripe only through server-side Stripe SDK calls. The reader runs embedded Stripe software and connects to Stripe over the internet. Offline payments aren’t supported.

**SDK integration**: Your POS or PMS interacts with Stripe on both the client side (Terminal SDK) and server side (Stripe SDK). The reader connects to your POS over a local network and to Stripe over the internet. Offline payments are supported in this architecture.

## Set up your Stripe account

### Create a Stripe account

Create a Stripe account at [dashboard.stripe.com/register](https://dashboard.stripe.com/register). For development and testing purposes, you don’t need to activate your account for live payments.

### Find your API keys

Retrieve your [API keys](https://docs.stripe.com/keys.md) from the [Dashboard](https://dashboard.stripe.com/test/apikeys). Your application uses these keys to authenticate with Stripe.

### Find your account ID

Your account ID appears on the [Account details](https://dashboard.stripe.com/settings/account) page. You need this for non-Connect configurations and when identifying your account to Stripe Support.

## Set up Terminal readers

### Order readers

Order hardware from the [Stripe Dashboard](https://dashboard.stripe.com/test/terminal/shop). After receiving readers, register them before use. You only need to do this initial setup once.

For ordering and returns, see [Order and return readers](https://docs.stripe.com/terminal/fleet/order-and-return-readers.md).

The WisePOS E, S700, and S710 are identical from a software perspective. The S710 adds a cellular (4G/LTE) module for redundant connectivity, which reduces time spent in offline mode.

### Register readers to locations

Stripe manages readers under [Locations](https://docs.stripe.com/api/terminal/locations.md). You can create a location for each physical site, then register readers to those locations with the Stripe Dashboard or API.

- **Dashboard**: Go to the [Terminal section](https://dashboard.stripe.com/test/terminal), create a location, then click **Register reader** and enter the pairing code generated on your reader.

- **API**: First, create a [Location object](https://docs.stripe.com/api/terminal/locations.md). Then use the returned location ID to register a [Reader object](https://docs.stripe.com/api/terminal/readers.md) with the pairing code from your device.

## Accept card present payments

This section covers the key Stripe objects and SDK steps needed to collect card-present payments. Each subsection links to the relevant API documentation for implementation details.

### Stripe objects

| Object | Description | Example use |
| --- | --- | --- |
| [PaymentIntent](https://docs.stripe.com/api/payment_intents.md) | Represents your intent to collect a payment. Tracks payment state through a multi-step state machine. | Processing a card payment during a transaction |
| [SetupIntent](https://docs.stripe.com/api/setup_intents.md) | Guides you through saving a customer’s payment credentials for future use. | Saving a card without immediately charging it |
| [PaymentMethod](https://docs.stripe.com/api/payment_methods.md) | Payment instrument used in a transaction. | Tokenizing card details for one-time or saved use |
| [Customer](https://docs.stripe.com/api/customers.md) | Represents a shopper with one or more payment instruments. | Maintaining a single profile and fraud model per shopper |

### Install the Terminal SDK

Follow the SDK installation instructions at [Set up the Terminal SDK](https://docs.stripe.com/terminal/payments/setup-integration.md).

### Authenticate with restricted API keys

Stripe Apps can use [restricted API keys (RAKs)](https://docs.stripe.com/stripe-apps/api-authentication/rak.md#develop-app) to authenticate with Stripe APIs. Each app gets a unique key with only the permissions it needs.

If you have an existing integration to migrate to a RAK app, see [Migrate to a RAK app](https://docs.stripe.com/stripe-apps/plugins/rak.md).

For Connect integrations, you can grant Connect permissions to your app so it can act on behalf of connected accounts. Use the Stripe Apps CLI plugin:

```shell
stripe apps grant connect-permission "PERMISSION_NAME" "EXPLANATION"
```

Then increment the app version and upload with `stripe apps upload`. For the full permissions reference, see [Connect permissions](https://docs.stripe.com/stripe-apps/reference/permissions.md#connect).

### Set application information

Set `AppInfo` when initializing both the Terminal SDK and the Stripe SDK. Stripe uses this for observability and troubleshooting. If you interact with both SDKs, set this value for each. See [Identify your plugin](https://docs.stripe.com/building-plugins.md#identify-plugin) for instructions.

Use the following convention for the application name:

```
POSNameMiddleware
```

### Authenticate with connection tokens

A connection token grants your app access to a specific Stripe account and allows the Terminal SDK to connect to a reader. Don’t cache or hardcode connection tokens—the SDK calls your `ConnectionTokenProvider` whenever it needs to authenticate or reconnect, so your provider must create and return a new token for each request.

1. Your backend requests a connection token from Stripe.
2. Stripe returns a `secret` value.
3. On the client, implement a `ConnectionTokenProvider` that returns this secret.

See [Connection token](https://docs.stripe.com/terminal/payments/setup-integration.md?terminal-sdk-platform=ios#connection-token) for implementation details.

### Initialize the SDK

Use the connection token secret and a `TerminalListener` to initialize the Terminal SDK. Implement the `TerminalListener` interface to receive status notifications, then pass it and the `ConnectionTokenProvider` to `Terminal.InitTerminal`.

Initialize the SDK once when your application starts and maintain the connection throughout the day. Reconnect only if the app crashes, the device reboots, or the reader loses power.

See [Initialize the SDK](https://docs.stripe.com/terminal/payments/setup-integration.md?terminal-sdk-platform=ios#initialize) for implementation details.

### Discover and connect to a reader

After initializing the SDK, discover online readers and connect to one.

See [Connect to a reader](https://docs.stripe.com/terminal/payments/connect-reader.md?terminal-sdk-platform=ios&reader-type=internet) for implementation details.

### Create and collect a payment

When a customer checks out, create a PaymentIntent to start a new payment session. You can create the PaymentIntent on the client or server side.

> Server-side PaymentIntent creation isn’t supported in offline mode. If you plan to support offline payments, use client-side creation.

Use [test amounts](https://docs.stripe.com/terminal/references/testing.md#physical-test-cards) to simulate different results during development.

- **Client-side creation**: See [Create a PaymentIntent (client-side)](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=dotnet#create-payment).

- **Server-side creation**: See [Create a PaymentIntent (server-side)](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=android#create-server-side). Retrieve the `client_secret` from the PaymentIntent on your server, pass it to the client, then call `retrievePaymentIntent` followed by `collectPaymentMethod`.

After creating the PaymentIntent, send it to the reader to collect payment details. See [Collect payment details](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=dotnet#collect-payment).

Once the customer presents their card, confirm the payment. See [Confirm a payment](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=dotnet#confirm-payment). On success, the PaymentIntent transitions to `succeeded` in the Dashboard.

> Mobile wallet payments (Apple Pay, Google Pay, Samsung Pay) are handled as contactless EMV transactions. No additional parameters are required for mobile wallet transactions. When saving a mobile wallet for future transactions, review the [mobile wallet considerations](https://docs.stripe.com/terminal/features/saving-payment-details/save-after-payment.md?terminal-sdk-platform=server-driven#mobile-wallets-considerations).

### Handle failed payments

When a payment fails (for example, due to a card decline), the PaymentIntent transitions to `requires_payment_method`. Don’t attempt to confirm immediately—collect the payment method again before retrying.

See [Handling failures](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=dotnet#handling-failures) for implementation details.

### Retrieve and list PaymentIntents

After a payment completes, you can retrieve the PaymentIntent from your backend. The object includes card brand, funding type, last 4 digits, processing network, expiry, and more.

- [Retrieve a PaymentIntent](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=server-driven#payment-intent)
- [List all PaymentIntents](https://docs.stripe.com/api/payment_intents/list.md)

### Cancel a PaymentIntent or reader action

You can cancel a PaymentIntent at any time before capture. See [Cancel a payment](https://docs.stripe.com/terminal/features/refunds.md?terminal-sdk-platform=dotnet&lang=dotnet#canceling-payments).

To cancel an in-progress reader action, use the [Cancel action API](https://docs.stripe.com/api/terminal/readers/cancel_action.md).

## Process refunds

### Linked refunds

Trigger refunds from your backend using the [Refunds API](https://docs.stripe.com/terminal/features/refunds.md?terminal-sdk-platform=dotnet&lang=dotnet#refunds) or the Dashboard. Pass either the PaymentIntent ID or the latest charge ID. This approach works for all card networks except Interac.

> Refunds are initially returned as `succeeded` and can later transition to `failed` asynchronously—this reflects how card networks behave in production. Listen for `refund.failed` webhook events to handle failures.

If webhooks aren’t available, poll on the following schedule: every 5 minutes for the first 2 hours, every 30 minutes for hours 2–24, every 4 hours for days 1–7, then daily through day 30. Stop polling once the refund transitions to `failed` and check the `failure_reason` field for details. Use a maximum polling period of 30 days and implement exponential backoff.

### Interac refunds

Interac refunds must be processed in-person—the customer must present the original card at the reader. Initiate a refund API call to send the PaymentIntent to the reader, then Stripe processes the refund when the customer presents their card.

See [Refund an Interac payment](https://docs.stripe.com/terminal/payments/regional.md?integration-country=CA#refund-an-interac-payment).

### Unlinked refunds

Unlinked refunds let merchants refund to a different payment method than the original, or to refund payments originally processed on another payment service provider.

> Unlinked refunds are gated. Contact [Stripe Support](https://support.stripe.com/) with your Stripe account ID to request access.

The steps are:

1. Create a [Customer object](https://docs.stripe.com/api/customers/create.md).
2. Collect a card-present payment method using a [SetupIntent](https://docs.stripe.com/terminal/features/saving-payment-details/save-directly.md).
3. Retrieve the [generated PaymentMethod ID](https://docs.stripe.com/api/setup_attempts/object.md#setup_attempt_object-payment_method_details-card_present-generated_card) from the SetupIntent response.
4. Issue the refund to the saved payment method. See [Unlinked refunds](https://docs.stripe.com/refunds.md#unlinked-refunds).

## Collect offline payments

[Offline mode](https://docs.stripe.com/terminal/features/operate-offline/overview.md) activates automatically when the reader loses internet connectivity. The SDK stores encrypted transactions locally on the reader and forwards them to Stripe once connectivity is restored.

### Prerequisites

- You must have previously connected to the same reader using a valid connection token within the last 24 hours, on the same local network used for that online connection.
- The reader must remain on that network while offline—you can’t switch networks while offline.
- The reader’s software must have been updated within the last 30 days, or it requires an online connection before it can process offline payments.

### Enable offline mode

[Configure and enable offline mode](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-sdk-platform=dotnet&reader-type=internet&lang=dotnet#enable-offline-mode) on a [Configuration](https://docs.stripe.com/api/terminal/configuration.md) object, either through the API or the Dashboard. Assign the configuration to a location, or update the account’s default configuration to apply it to all locations.

### Handle offline events

Implement the `IOfflineListener` interface to receive events such as connectivity transitions and payment forwarding status. Pass this listener when initializing the SDK.

See [Handle offline events](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-card-present-integration=terminal&terminal-sdk-platform=dotnet&reader-type=internet&lang=dotnet#handle-offline-events).

### Create offline payments

When creating a PaymentIntent for offline use, set the `offline_behavior` parameter using a `CreateConfiguration` object:

| Behavior | Description |
| --- | --- |
| `RequireOnline` | The PaymentIntent must be processed online. Throws a `TerminalException` if the device is offline. |
| `PreferOnline` | Creates the PaymentIntent offline if needed, but processes online if connectivity is available. |
| `ForceOffline` | Forces an offline PaymentIntent regardless of connectivity. Use only when transaction speed is critical. |

See [Create a PaymentIntent offline](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-card-present-integration=terminal&terminal-sdk-platform=dotnet&reader-type=internet&lang=dotnet#create-payment-intent).

When offline, the PaymentIntent response returns `id=null` because no Stripe ID has been assigned yet.

Collecting the payment method doesn’t require extra parameters. Swiping is unavailable offline, and tapping is unavailable offline in SCA-regulated markets.

Confirming the PaymentIntent follows the same steps as the online flow. You must handle the Stripe-enforced USD 10,000 offline transaction limit in your error handling.

### Manage offline risk

To limit exposure from offline payments, track thresholds and enforce them in your POS logic using `OfflinePaymentsCount` and `OfflinePaymentAmountsByCurrency`. For smart readers, these are exposed on the reader status object; for mobile SDK and Tap to Pay integrations, they’re exposed on the SDK status object instead.

- Deny transactions above a per-transaction amount threshold.
- Deny transactions if the reader’s total stored amount exceeds a configured limit.
- Deny transactions if the number of stored transactions exceeds a configured count.

See [Managing risk](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-card-present-integration=terminal&terminal-sdk-platform=dotnet&reader-type=internet#managing-risk).

### Monitor payment forwarding

Once connectivity is restored, the reader forwards stored PaymentIntents automatically. To monitor this:

- Check `Terminal.OfflineStatus.Reader.NetworkStatus` for the reader’s connection state.
- Check `Terminal.OfflineStatus.Reader.OfflinePaymentsCount` for the number of pending payments.
- Implement `IOfflineListener` to log forwarded and failed-to-forward payments.

See [Monitor forwarding](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-card-present-integration=terminal&terminal-sdk-platform=dotnet&reader-type=internet#examine-offline).

When offline, retrieve receipt details from `paymentIntent.OfflineDetails.OfflineCardPresentDetails` to print proof of purchase for customers. See [Provide receipts](https://docs.stripe.com/terminal/features/operate-offline/collect-card-payments.md?terminal-card-present-integration=terminal&terminal-sdk-platform=dotnet&reader-type=internet#providing-receipts).

## Additional payment flows

### Tap to Pay

Tap to Pay lets compatible iPhones and Android devices accept contactless payments directly, without a separate reader. Supported card types include American Express, Mastercard, and Visa contactless cards, as well as NFC-based wallets (Apple Pay, Google Pay, Samsung Pay).

> Only the iOS, Android, and React Native SDKs support Tap to Pay.

See [Set up Tap to Pay](https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay.md?platform=ios).

### Alternative payment methods

Smart readers can display QR codes for alternative payment methods like WeChat Pay, Affirm, and PayNow, letting customers complete checkout on their mobile device.

> PayNow requires gated access. Contact [Stripe Support](https://support.stripe.com/) with your Stripe account number to request access.

See [Alternative payment methods](https://docs.stripe.com/terminal/payments/additional-payment-methods.md).

### Mail and telephone order (MOTO) payments

For use cases where the customer isn’t present—such as phone orders—you can accept MOTO payments through the Terminal SDK. The payment flow is similar to card-present, with additional parameters required.

See [MOTO overview](https://docs.stripe.com/terminal/features/mail-telephone-orders/overview.md) and [MOTO payments](https://docs.stripe.com/terminal/features/mail-telephone-orders/payments.md).

### Payment Links

Payment Links doesn’t use the Terminal SDK or readers. It uses the [Stripe Payment Links API](https://docs.stripe.com/payment-links.md).

To implement:

1. Set up a product catalog, then [create a Payment Link](https://docs.stripe.com/payment-links/create.md#api).
2. [Share the link](https://docs.stripe.com/payment-links/share.md?dashboard-or-api=api#share-online) with your customer.
3. Optionally, [limit the number of payments](https://docs.stripe.com/payment-links/customize.md#limit-payments) per link.
4. [Track interactions](https://docs.stripe.com/payment-links/url-parameters.md) and payment status using URL parameters.
5. [Deactivate the link](https://docs.stripe.com/payment-links/share.md?dashboard-or-api=api#deactivate-link) when it’s no longer needed.

## Test your integration

### Use a sandbox

Use a [sandbox](https://docs.stripe.com/sandboxes.md) with separate API keys and data for all development and validation—don’t process live payments during integration development.

### Use test card numbers

Before you go live, test every card brand you plan to support end to end. See [Test Terminal](https://docs.stripe.com/terminal/references/testing.md) for the full list of virtual and physical test cards, including the [simulated reader](https://docs.stripe.com/terminal/example-applications.md#connect-simulated-reader) and magic cent amounts to force specific outcomes.

While you can’t test offline mode with a simulated reader, you can test it using physical hardware:

1. Enable a hotspot on a device separate from your POS device.
2. Connect the POS device and reader to the hotspot.
3. Remove the SIM card from the hotspot device to simulate an internet outage.
4. Confirm that the POS device remains connected to the hotspot.

Stripe doesn’t currently provide a dedicated way to test mobile wallet payments. Because wallet transactions are handled as contactless EMV transactions, a successful contactless EMV test confirms that wallet payments work correctly.

### Test webhooks

Use the [Stripe CLI](https://docs.stripe.com/webhooks/quickstart.md) to forward webhooks to localhost and manually trigger events during development.

### Test identity and address verification

Use Stripe’s default [test values for identity and address verification](https://docs.stripe.com/connect/testing.md#identity-verification) during development.

## Best practices

### Subscribe to SDK changelog notifications

Watch the relevant GitHub repositories to receive SDK release notifications:

- [iOS](https://github.com/stripe/stripe-terminal-ios/)
- [Android](https://github.com/stripe/stripe-terminal-android/)
- [React Native](https://github.com/stripe/stripe-terminal-react-native/)
- [Java](https://github.com/stripe/stripe-terminal-java)

### Use idempotency keys

Use [idempotency keys](https://docs.stripe.com/api/idempotent_requests.md) to safely retry requests without creating duplicate payments. Use V4 UUIDs or another high-entropy random string. Keys can be up to 255 characters.

### Manage API keys securely

As a best practice, we recommend using a backend to store user secrets and field client requests. However, since many POS integrations operate on-premise without a backend, oftentimes the Stripe user’s secret key must be present on the POS device to accept payments through Stripe. In that case, we recommend the following controls be put in place:

- User secrets are encrypted at rest or user secrets are held in the app’s secure storage
- Access to user secrets should be restricted only to those persons or workloads with a valid need to access that secret.
- There should be an audit trail for all changes to a secret.

### Use metadata

Attach a unique local transaction ID to each PaymentIntent using the [metadata](https://docs.stripe.com/api/metadata.md) field. This is especially important for offline payments, which don’t receive a Stripe ID until they’re forwarded—metadata lets you reconcile them once they appear in your Dashboard.

### Configure webhooks

Configure [webhooks](https://docs.stripe.com/webhooks.md) to receive real-time notifications about payment updates. [Verify webhook signatures](https://docs.stripe.com/webhooks/signature.md) using the `Stripe-Signature` header to prevent replay attacks.

### Handle AVS for card-not-present flows

If you process card-not-present payments (such as MOTO or Payment Links) with cards issued by banks that support [AVS](https://en.wikipedia.org/wiki/Address_Verification_System), include billing address and zip code in each payment request to reduce declines.

## Connect integrations

### Collect platform fees

Use the `application_fee_amount` parameter when creating a PaymentIntent to collect a platform fee. Stripe automatically splits the transaction between the merchant and your platform balance. This parameter is also applied when offline payments are forwarded, so Connect fees work the same way in offline mode.

### Refunds in Connect

When using the `Stripe-Account` header (direct charges), the merchant owns the funds. Refunds are pulled from the merchant’s Stripe balance. If the balance is zero, the refund fails unless you’ve enabled account debits for that connected account.
