Collect a Payment
Collect an in-person payment with Stripe Terminal by registering listeners early, initializing the plugin, connecting a reader, and confirming a PaymentIntent.
Register application-level listeners
Register Terminal event listeners once per JavaScript application startup, as early as possible during bootstrap—for example from main.ts, an application initializer, or a singleton service initialized at startup—and before initializing or starting an operation. Keep them registered for the lifetime of their application-level owner.
enum TerminalEventsEnum
| Member | Value |
|---|---|
Loaded |
'terminalLoaded' |
DiscoveredReaders |
'terminalDiscoveredReaders' |
DiscoveringReaders |
'terminalDiscoveringReaders' |
CancelDiscoveredReaders |
'terminalCancelDiscoveredReaders' |
ConnectedReader |
'terminalConnectedReader' |
DisconnectedReader |
'terminalDisconnectedReader' |
ConnectionStatusChange |
'terminalConnectionStatusChange' |
UnexpectedReaderDisconnect |
'terminalUnexpectedReaderDisconnect' |
ConfirmedPaymentIntent |
'terminalConfirmedPaymentIntent' |
CollectedPaymentIntent |
'terminalCollectedPaymentIntent' |
Canceled |
'terminalCanceled' |
Failed |
'terminalFailed' |
RequestedConnectionToken |
'terminalRequestedConnectionToken' |
ReportAvailableUpdate |
'terminalReportAvailableUpdate' |
StartInstallingUpdate |
'terminalStartInstallingUpdate' |
ReaderSoftwareUpdateProgress |
'terminalReaderSoftwareUpdateProgress' |
FinishInstallingUpdate |
'terminalFinishInstallingUpdate' |
BatteryLevel |
'terminalBatteryLevel' |
ReaderEvent |
'terminalReaderEvent' |
RequestDisplayMessage |
'terminalRequestDisplayMessage' |
RequestReaderInput |
'terminalRequestReaderInput' |
PaymentStatusChange |
'terminalPaymentStatusChange' |
ReaderReconnectStarted |
'terminalReaderReconnectStarted' |
ReaderReconnectSucceeded |
'terminalReaderReconnectSucceeded' |
ReaderReconnectFailed |
'terminalReaderReconnectFailed' |
Typed addListener overloads cover most of these members. DiscoveringReaders and CancelDiscoveredReaders are emitted by native discovery start and cancel but do not have dedicated overloads; see the API page.
Initialize
Prefer an authenticated app-side request through RequestedConnectionToken and setConnectionToken. This lets your app attach its normal authorization credentials and validate failures. Register the listener before initialize; the Terminal SDK asks for a new, single-use connection token whenever it needs one. Set isTest while developing.
method initialize(...)
initialize(options: { tokenProviderEndpoint?: string; isTest: boolean; }) => Promise<void>
tokenProviderEndpoint compatibility mode
tokenProviderEndpoint is available for simple deployments, but the v8.2.0 native clients send a bare HTTP POST: callers cannot add an authorization header or request body. Use it only when your server can authenticate and protect that request by other means. Never expose an unrestricted public token-creation endpoint.
When tokenProviderEndpoint is set, the plugin sends an HTTP POST with an empty body. The response must be JSON with a secret string:
{ "secret": "pst_..." }
That value is a Stripe Terminal connection token. Create it on the server with your secret API key (stripe.terminal.connectionTokens.create()). Never put the secret key, restricted keys that can create tokens, or raw connection tokens in the app binary, logs, or a public client config.
The official demo exposes POST /connection/token and returns { secret }; adapt its authentication and authorization to your application.
Web initialize requires a fresh plugin instance: calling it again after a successful init throws Stripe Terminal has already been initialized.
Supply a connection token securely
Omit tokenProviderEndpoint and register RequestedConnectionToken before initialize. When the SDK needs a token, the plugin emits that event and waits for setConnectionToken({ token }).
Fetch with your normal authorization mechanism, require a successful response, validate secret, and pass it as token. Call setConnectionToken only while a fetch is pending; Android and iOS reject extra calls with Stripe Terminal do not pending fetchConnectionToken. Never log the response or token.
method setConnectionToken(...)
setConnectionToken(options: { token: string; }) => Promise<void>
Create a PaymentIntent on your backend
Create the PaymentIntent on your server. The official demo uses POST /connection/intent and returns { paymentIntent } as the client secret.
Requirements that match the plugin and demo:
payment_method_typesmust includecard_present- Keep the Stripe secret key on the server
- Pass only the client secret into
collectPaymentMethod({ paymentIntent }) - Do not create or confirm card-present PaymentIntents with a publishable key in the app
Example server shape from the demo:
await stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
payment_method_types: ['card_present'],
capture_method: 'automatic',
});
Discover readers
Discover nearby or simulated readers. Provide a TerminalConnectTypes value and a Stripe Terminal locationId when the connection type needs it.
locationId is used during Internet discovery and is required when connecting Tap to Pay, Bluetooth, and Android USB readers. Internet discovery can filter by location; Tap to Pay and Bluetooth pass the location into the connection configuration.
Nuances:
- Web supports
Internetonly. Any othertypeis unavailable. - iOS Bluetooth reports readers through
DiscoveredReadersmultiple times as the scan updates. See Stripe: connect a Bluetooth reader (iOS). SetbluetoothScanWaitTime(milliseconds) sodiscoverReaderswaits before resolving with the current list.0or omitted returns the first scan result. - iOS also emits
DiscoveringReaderswhen the scan starts. USB, HandOff, andSimulatedas atypeare unimplemented. - Android requires
ACCESS_FINE_LOCATIONat runtime ordiscoverReadersrejects.Simulatedis treated as Bluetooth discovery.HandOffis Apps on Devices. - Call
cancelDiscoverReadersif the user leaves the scan UI. Web is a no-op for cancel. Always give the user a way to stop a long Bluetooth scan.
Listen for DiscoveredReaders in addition to awaiting the promise. On iOS Bluetooth the listener is the live list; the promise may resolve earlier than the last event.
method discoverReaders(...)
discoverReaders(options: DiscoverReadersOptions) => Promise<{ readers: ReaderInterface[]; }>
interface DiscoverReadersOptions
| Prop | Type | Description |
|---|---|---|
type |
TerminalConnectTypes |
|
locationId |
string |
|
bluetoothScanWaitTime |
number |
Only applies to Bluetooth scan discovery (iOS only). During discovery, readers are reported via DiscoveryDelegate.didUpdateDiscoveredReaders. This timeout controls how long to wait before resolving the discoverReaders method with the current list. If this setting is not specified or is set to 0, the initial scan results will be returned. |
enum TerminalConnectTypes
| Member | Value |
|---|---|
Simulated |
'simulated' |
Internet |
'internet' |
Bluetooth |
'bluetooth' |
Usb |
'usb' |
TapToPay |
'tap-to-pay' |
HandOff |
'hand-off' |
Connect a reader
Connect to one of the discovered readers before collecting payment details. The reader object must come from the current discovery result (serialNumber is the plugin's primary identifier).
autoReconnectOnUnexpectedDisconnect defaults to false and is applied for Tap to Pay and Bluetooth. Android USB currently enables auto-reconnect in the native connection config. Internet connections do not take this flag.
merchantDisplayName and onBehalfOf apply to iOS Tap to Pay (LocalMobileReader). On Android, set connected-account and display values on the PaymentIntent instead.
method connectReader(...)
connectReader(options: { reader: ReaderInterface; autoReconnectOnUnexpectedDisconnect?: boolean; merchantDisplayName?: string; onBehalfOf?: string; }) => Promise<void>
Collect a payment method
Pass the PaymentIntent client secret from your backend to collectPaymentMethod. The plugin retrieves that PaymentIntent, then collects on the connected reader.
method collectPaymentMethod(...)
collectPaymentMethod(options: { paymentIntent: string; }) => Promise<void>
Confirm the payment intent
Process and confirm the collected PaymentIntent. confirmPaymentIntent rejects if you have not successfully collected first (PaymentIntent not found for confirmPaymentIntent).
method confirmPaymentIntent()
confirmPaymentIntent() => Promise<void>
ConfirmedPaymentIntent is a client UI signal, not fulfillment authority. Fulfill the order only after your backend verifies a Stripe webhook such as payment_intent.succeeded.
Handle cancellation and errors
cancelCollectPaymentMethodcancels an in-flight collect. On success the promise resolves andCanceledis emitted.Failedis emitted whencollectPaymentMethodorconfirmPaymentIntentfails. The same call's promise also rejects. The payload may includemessage,code, anddeclineCode.- Do not use
ConnectionStatusChangeto detect unexpected disconnects. UseUnexpectedReaderDisconnect, and for Bluetooth/USB alsoDisconnectedReader. See Reader Lifecycle.
method cancelCollectPaymentMethod()
cancelCollectPaymentMethod() => Promise<void>
Disconnect the reader
Disconnect when the payment flow is finished or the reader is no longer needed.
method disconnectReader()
disconnectReader() => Promise<void>
import {
StripeTerminal,
TerminalConnectTypes,
TerminalEventsEnum,
} from '@capacitor-community/stripe-terminal';
const paymentStatusListener = await StripeTerminal.addListener(
TerminalEventsEnum.PaymentStatusChange,
({ status }) => console.log(status),
);
const confirmedListener = await StripeTerminal.addListener(
TerminalEventsEnum.ConfirmedPaymentIntent,
() => console.log('Payment processed; waiting for the server webhook'),
);
const failedListener = await StripeTerminal.addListener(
TerminalEventsEnum.Failed,
(error) => console.error(error),
);
// Register the authenticated RequestedConnectionToken provider first.
await StripeTerminal.initialize({ isTest: true });
const { readers } = await StripeTerminal.discoverReaders({
type: TerminalConnectTypes.TapToPay,
locationId: '**************',
});
const reader = readers[0];
if (!reader) throw new Error('No compatible reader found');
await StripeTerminal.connectReader({
reader,
});
try {
const response = await fetch('https://example.com/connection/intent', {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(`PaymentIntent request failed: ${response.status}`);
const { paymentIntent } = (await response.json()) as { paymentIntent: string };
await StripeTerminal.collectPaymentMethod({ paymentIntent });
await StripeTerminal.confirmPaymentIntent();
} finally {
await StripeTerminal.disconnectReader();
}
// Remove the three listeners when their application-level owner is destroyed.
import { StripeTerminal, TerminalEventsEnum } from '@capacitor-community/stripe-terminal';
await StripeTerminal.addListener(
TerminalEventsEnum.RequestedConnectionToken,
async () => {
try {
const response = await fetch('https://example.com/connection/token', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) throw new Error(`Connection token request failed: ${response.status}`);
const data = (await response.json()) as { secret?: unknown };
if (typeof data.secret !== 'string' || !data.secret) {
throw new Error('Connection token response is missing secret');
}
await StripeTerminal.setConnectionToken({ token: data.secret });
} catch (error) {
// An empty token fails the pending native callback instead of leaving it hanging.
try {
await StripeTerminal.setConnectionToken({ token: '' });
} finally {
console.error('Unable to supply a connection token', error);
}
}
},
);
await StripeTerminal.initialize({
isTest: true,
});