.NET (Stitch.Terminal)
Overview
The Stitch Terminal SDK is designed for POS systems and other merchant-facing applications to seamlessly connect to payment terminals that process payments on Stitch.
You create a session on a terminal by calling terminal.StartSessionAsync(request, ...). The SDK returns a Task<TerminalSession> that completes when the session reaches a terminal state.
Requirements
The SDK targets .NET 8 . Make sure your project targets .NET 8 or later:
Install
Install the release of the package:
dotnet add package Stitch.Terminal
Or pin a specific version in your .csproj:
<ItemGroup>
<PackageReference Include="Stitch.Terminal" Version="1.0.0" />
</ItemGroup>
On Windows POS systems, the USB driver for VIPA terminals must be installed before the SDK can communicate with the device.
Terminal
The Terminal class allows the library to interface with different payment terminal implementations. Each terminal instance handles the protocol required by a particular terminal type (e.g. VIPA for Verifone devices).
Each Terminal instance manages a single active terminal connection at a time. Create multiple Terminal instances to connect to multiple terminals simultaneously.
Create terminals and connect to terminals at application startup. This gives terminals enough time to initialize, connect to backend and be ready when you begin a transaction.
Creating a Terminal
Construct a terminal with a ConnectionConfig, then call ConnectAsync to establish the connection:
var config = ConnectionConfig.Serial(TerminalType.Vipa, "/dev/ttyUSB0");
var terminal = new Terminal(config);
TerminalStatus status = await terminal.ConnectAsync();
ConnectAsync returns the terminal's status once connected.
Each connection type has its own static factory method with only the parameters relevant to that type:
// Serial connection (VIPA or Android)
var serialLinux = ConnectionConfig.Serial(TerminalType.Vipa, "/dev/ttyUSB0", pin: "1234", apiKey: "sk_test_abc123"); // Linux
var serialWindows = ConnectionConfig.Serial(TerminalType.Android, "COM3"); // Windows
// Serial connection with auto-detection (VIPA only)
var serialAuto = ConnectionConfig.Serial(TerminalType.Vipa, path: null, pin: "1234", apiKey: "sk_test_abc123");
// TCP/IP connection to a Stitch Android terminal (requires token)
var androidTcp = ConnectionConfig.Tcp(
TerminalType.Android,
"192.168.1.101",
16107,
token: "tkn_abc123");
For VIPA terminals using serial connections, passing null as the path enables automatic device detection. The SDK will scan for and connect to an available serial device.
token is required when connecting to a Stitch Android terminal.
pin is required for VIPA terminals. When provided, the SDK will automatically log in during terminal connection.
Login is a one-time setup step. Once logged in, the terminal remains authenticated for the lifetime of the application.
For Android terminals, login is not required via the SDK. You must log in directly on the terminal device, and any pin provided in ConnectionConfig is ignored.
apiKey is only used by VIPA terminals. It authenticates the SDK with Stitch backend services.
Terminal Status
Get the status of a connected terminal.
TerminalStatus status = await terminal.GetStatusAsync();
Console.WriteLine($"Serial: {status.Serial}");
Console.WriteLine($"Status: {status.Status}");
| Status | Description |
|---|---|
READY | Terminal is ready for sessions |
BUSY | Terminal is currently processing a session |
OFFLINE | Terminal is offline |
LOGGED_OUT | Terminal requires login before transacting |
Disposing the terminal
If DisposeAsync is called while StartSessionAsync is in progress, the pending session task completes with Status = Failure and FailureReason = OfflineTerminal. The same connection-loss caveat applies for Android terminals.
To stop a session without disposing the terminal, use a CancellationToken instead, this keeps the connection open for subsequent sessions.
Only one Terminal instance can hold an active connection to a given device at a time. Always dispose the previous Terminal before creating a new one for the same serial path or TCP host/port; otherwise ConnectAsync on the new instance throws StitchTerminalException.
Terminal Session
A Terminal Session defines the intent to process a charge, refund, or disbursement in person using a physical payment terminal.
Session lifecycle
| Status | Description |
|---|---|
Success | Session completed successfully. The outcome contains the resulting charge and/or mandate. |
Failure | Session failed and will not be retried. |
Starting a session
Build a request and call terminal.StartSessionAsync(request, cancellationToken). The returned Task<TerminalSession> completes once the session reaches a terminal state (Success or Failure).
// Build the intent (what to charge)
var intent = new ChargeIntent(
Amount: 25.00m,
Currency: "ZAR");
// Build the request
var request = new TerminalSessionCreateRequest
{
Intent = intent,
Nonce = Guid.NewGuid().ToString("N"),
ExternalReference = "order-456",
Metadata = new Dictionary<string, string> { ["orderId"] = "12345" },
};
// Await the final session (Success or Failure)
TerminalSession session = await terminal.StartSessionAsync(request);
switch (session.Status)
{
case SessionStatus.Success:
Charge charge = session.Outcome!.Charge!;
Console.WriteLine($"Success! Charge ID: {charge.Id}");
break;
case SessionStatus.Failure:
Console.WriteLine($"Failed: {session.FailureReason}");
break;
}
Cancellation
Pass a CancellationToken to cancel a pending session. When the token is triggered, the session is cancelled on the terminal, the awaited task completes with a TerminalSession whose Status is Failure and FailureReason is CancelledByApi, and no OperationCanceledException is thrown.
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
TerminalSession session = await terminal.StartSessionAsync(request, cts.Token);
You can also cancel manually by calling cts.Cancel(). Because StartSessionAsync blocks until the session completes, cts.Cancel() must be triggered from a different execution context, eg a UI event handler, background task, or any other code path that holds a reference to cts:
using var cts = new CancellationTokenSource();
// Trigger cancellation from elsewhere, eg a cancel button on the POS
cancelButton.Click += (_, _) => cts.Cancel();
TerminalSession session = await terminal.StartSessionAsync(request, cts.Token);
Cancellation only takes effect while the session is still pending. If the token is triggered after the session has already reached a terminal state, the call has no effect, a successful charge is not reversed, and a failed session remains failed. To reverse a completed charge, use the standard refund flow instead.
Data Types
TerminalSessionCreateRequest
The request object for creating a terminal session.
var request = new TerminalSessionCreateRequest
{
Intent = intent,
Nonce = Guid.NewGuid().ToString("N"),
ExternalReference = "order-456",
Metadata = new Dictionary<string, string> { ["orderId"] = "12345" },
};
| Property | Required | Description |
|---|---|---|
Intent | Required | Defines the purpose of the session (e.g. charge, mandate) |
Nonce | Required | A unique identifier for idempotency. If you attempt to create a session with a nonce that has already been used, an error will be returned |
ExternalReference | Optional | A custom identifier to reference this session in your own system. Multiple sessions can share the same external reference |
Metadata | Optional | Key-value pairs to store additional, structured information relevant to your integration |
TerminalSessionIntent
The intent defines what the session should do: take a charge from the customer's card, refund a previous charge back to the same card, or pay out funds to a card as a disbursement.
public abstract record TerminalSessionIntent;
public sealed record ChargeIntent(
decimal Amount,
string Currency,
Money? Cashback = null) : TerminalSessionIntent;
public sealed record RefundIntent(
decimal Amount,
string Currency,
string TerminalSession,
RefundReason Reason) : TerminalSessionIntent;
public sealed record DisbursementIntent(
decimal Amount,
string Currency) : TerminalSessionIntent;
| Intent | Description |
|---|---|
ChargeIntent | Take a payment from the customer's card |
RefundIntent | Refund a previous charge back to the original card. Linked to a charge |
DisbursementIntent | Pay funds to a card as a stand-alone payout |
Charge intent (once-off payment)
var intent = new ChargeIntent(Amount: 30.00m, Currency: "ZAR");
Charge with cashback
var intent = new ChargeIntent(
Amount: 30.00m,
Currency: "ZAR",
Cashback: new Money(5.00m, "ZAR"));
ChargeIntent
| Property | Required | Description |
|---|---|---|
Amount | Required | The charge amount in the currency's major unit (e.g. 30.00m for R30.00) |
Currency | Required | ISO 4217 currency code (only "ZAR" supported) |
Cashback | Optional | Cashback to be dispensed to the customer in addition to the charge amount |
Refund intent (linked refund)
A refund returns funds from a previous charge back. The original session ID must be supplied as the TerminalSession.
var intent = new RefundIntent(
Amount: 30.00m,
Currency: "ZAR",
TerminalSession: "ts_abc123",
Reason: RefundReason.RequestedByUser);
RefundIntent
| Property | Required | Description |
|---|---|---|
Amount | Required | The refund amount in the currency's major unit. May be less than or equal to the original charge amount |
Currency | Required | ISO 4217 currency code (only "ZAR" supported). Must match the currency of the source session |
TerminalSession | Required | The ID of the original terminal session being refunded (e.g. "ts_abc123") |
Reason | Required | The reason for the refund |
RefundReason
| Value | Description |
|---|---|
Fraud | The original charge was fraudulent |
RequestedByUser | The customer requested the refund |
DuplicateCharge | The original charge was a duplicate |
Disbursement intent (stand-alone payout)
A disbursement pays funds to a card without being linked to a session. The customer presents their card on the terminal to receive the payout.
var intent = new DisbursementIntent(
Amount: 50.00m,
Currency: "ZAR");
DisbursementIntent
| Property | Required | Description |
|---|---|---|
Amount | Required | The disbursement amount in the currency's major unit |
Currency | Required | ISO 4217 currency code (only "ZAR" supported) |
Money
public sealed record Money(decimal Amount, string Currency);
| Property | Type | Description |
|---|---|---|
Amount | decimal | Amount in the currency's major unit |
Currency | string | ISO 4217 currency code |
SessionStatus
| Value | Description |
|---|---|
Success | Session completed successfully |
Failure | Session failed |
FailureReason
| Value | Description |
|---|---|
Expired | Session timed out before completion |
CancelledByApi | Session was cancelled via online API |
CancelledBySDK | Session was cancelled via CancellationToken |
CancelledByTerminal | Terminal cancelled transaction, e.g. timeout |
BusyTerminal | Terminal was busy with another session |
OfflineTerminal | Terminal is offline |
Declined | Payment was declined |
SessionOutcome
The outcome when a session completes successfully. Contains the resulting charge, refund, or disbursement, depending on the session's intent.
public sealed record SessionOutcome(
Charge? Charge,
Refund? Refund,
Disbursement? Disbursement);
Charge
The charge object from a successful session.
public sealed record Charge(
string Id,
decimal Amount,
string Currency,
Cashback? Cashback,
ChargeStatus Status,
Failure? Failure,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
ChargeType Type,
Card Card,
string? RetrievalReferenceNumber);
| Property | Type | Description |
|---|---|---|
Id | string | Charge ID (e.g. "ch_abc123") |
Amount | decimal | The amount charged in the currency's major unit |
Currency | string | The charge currency in ISO 4217 format (e.g. "ZAR") |
Cashback | Cashback? | The cashback dispensed to the customer (null if no cashback) |
Status | ChargeStatus | The current status of the charge |
Failure | Failure? | Failure details when Status is Failure (null otherwise) |
CreatedAt | DateTimeOffset | When the charge was created |
UpdatedAt | DateTimeOffset | When the charge was last modified |
Type | ChargeType | The type of charge (e.g. InPersonCard) |
Card | Card | Card data (scheme, masked PAN, etc.) |
RetrievalReferenceNumber | string? | The retrieval reference number (RRN) assigned by the card network for this transaction |
Failure
Failure details for a charge, refund, or disbursement. Present when the operation's Status is Failure.
public sealed record Failure(string Reason, ResultCode? ResultCode);
| Property | Type | Description |
|---|---|---|
Reason | string | The reason for the failure, available options: authorization_failed authorization_declined |
ResultCode | ResultCode? | The result code from the card network. Only present when Reason is authorization_declined |
ResultCode
The result code from the card network explaining why authorization failed.
public sealed record ResultCode(string Value, string Descriptor, string Detail);
| Property | Type | Description |
|---|---|---|
Value | string | The numeric result code value (e.g. "05") |
Descriptor | string | A machine-readable descriptor for the result code (e.g. "do_not_honour") |
Detail | string | A human-readable explanation of the result code |
Cashback
The cashback dispensed to the customer as part of the charge.
public sealed record Cashback(decimal Amount, string Currency);
| Property | Type | Description |
|---|---|---|
Amount | decimal | The cashback amount in the currency's major unit |
Currency | string | The cashback currency in ISO 4217 format (e.g. "ZAR") |
ChargeStatus
| Value | Description |
|---|---|
Processing | The charge is being processed |
Success | The charge was processed successfully |
Failure | The charge did not complete successfully |
ChargeType
| Value | Description |
|---|---|
InPersonCard | An in-person card charge |
Refund
The refund object from a successful refund session. A refund returns funds linked to an original charge.
public sealed record Refund(
string Id,
string TerminalSessionId,
decimal Amount,
string Currency,
RefundStatus Status,
Failure? Failure,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
Card? Card,
string? RetrievalReferenceNumber);
| Property | Type | Description |
|---|---|---|
Id | string | Refund ID (e.g. "rf_abc123") |
TerminalSessionId | string | The terminal session ID of the original charge being refunded (e.g. "ts_abc123") |
Amount | decimal | The amount refunded in the currency's major unit |
Currency | string | The refund currency in ISO 4217 format (e.g. "ZAR") |
Status | RefundStatus | The current status of the refund |
Failure | Failure? | Failure details when Status is Failure (null otherwise) |
CreatedAt | DateTimeOffset | When the refund was created |
UpdatedAt | DateTimeOffset | When the refund was last modified |
Card | Card? | Card data (scheme, masked PAN, etc.) |
RetrievalReferenceNumber | string? | The retrieval reference number (RRN) assigned by the card network for this transaction |
RefundStatus
| Value | Description |
|---|---|
Processing | The refund is being processed |
Success | The refund was processed successfully |
Failure | The refund did not complete successfully |
Disbursement
The disbursement object from a successful disbursement terminal session. A disbursement pays funds to a card without a linked source charge. The customer presents their card on the terminal to receive the payout.
public sealed record Disbursement(
string Id,
decimal Amount,
string Currency,
DisbursementStatus Status,
Failure? Failure,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
Card Card,
string? RetrievalReferenceNumber);
| Property | Type | Description |
|---|---|---|
Id | string | Disbursement ID (e.g. "dsb_abc123") |
Amount | decimal | The amount disbursed in the currency's major unit |
Currency | string | The disbursement currency in ISO 4217 format (e.g. "ZAR") |
Status | DisbursementStatus | The current status of the disbursement |
Failure | Failure? | Failure details when Status is Failure (null otherwise) |
CreatedAt | DateTimeOffset | When the disbursement was created |
UpdatedAt | DateTimeOffset | When the disbursement was last modified |
Card | Card | Card data (scheme, masked PAN, etc.) for the card the funds were paid to |
RetrievalReferenceNumber | string? | The retrieval reference number (RRN) assigned by the card network for this transaction |
DisbursementStatus
| Value | Description |
|---|---|
Processing | The disbursement is being processed |
Success | The disbursement was processed successfully |
Failure | The disbursement did not complete successfully |
Card
public sealed record Card(
string Bin,
string Last4,
CardExpiry Expiry,
CardNetwork? Network,
FundingType? FundingType,
CardIssuer? Issuer);
| Property | Type | Description |
|---|---|---|
Bin | string | The first 8 digits of the card number |
Last4 | string | The last 4 digits of the card number |
Expiry | CardExpiry | The card expiry date |
Network | CardNetwork? | The card network (may be null) |
FundingType | FundingType? | The funding type of the card (may be null) |
Issuer | CardIssuer? | The card issuer details (may be null) |
CardExpiry
public sealed record CardExpiry(string Month, string Year);
| Property | Type | Description |
|---|---|---|
Month | string | The expiry month in MM format (01-12) |
Year | string | The expiry year in YY format (e.g. "25" for 2025) |
CardNetwork
| Value | Description |
|---|---|
Visa | Visa |
Mastercard | Mastercard |
Amex | American Express |
Diners | Diners Club |
FundingType
| Value | Description |
|---|---|
Credit | Credit card |
Debit | Debit card |
Prepaid | Prepaid card |
CardIssuer
public sealed record CardIssuer(string Name, string Country);
| Property | Type | Description |
|---|---|---|
Name | string | The name of the financial institution that issued the card |
Country | string | The country code (ISO 3166-1 alpha-2) of the card issuer (e.g. "ZA") |
ConnectionConfig
public abstract record ConnectionConfig;
public sealed record SerialConfig(
TerminalType Type,
string? Path,
string? Pin = null,
string? ApiKey = null) : ConnectionConfig;
public sealed record TcpConfig(
TerminalType Type,
string Host,
int Port,
string? Token = null,
string? Pin = null,
string? ApiKey = null) : ConnectionConfig;
SerialConfig
| Property | Type | Description |
|---|---|---|
Type | TerminalType | The terminal type (Vipa or Android) |
Path | string? | Device path (e.g. "/dev/ttyUSB0", "COM3"), or null to auto-detect (VIPA only) |
Pin | string? | Termianl login PIN. Required for VIPA terminals |
ApiKey | string? | Stitch API key used to authenticate the SDK with Stitch backend services. Required for VIPA terminals |
TcpConfig
| Property | Type | Description |
|---|---|---|
Type | TerminalType | The terminal type (Vipa or Android) |
Host | string | Hostname or IP address of the terminal |
Port | int | TCP port number (e.g. 16107) |
Token | string? | Authentication token for the terminal. Only required for Stitch Android terminals; null for VIPA |
Pin | string? | Termianl login PIN. Required for VIPA terminals |
ApiKey | string? | Stitch API key used to authenticate the SDK with Stitch backend services. Required for VIPA terminals |
TerminalStatus
Returned by terminal.GetStatusAsync().
public sealed record TerminalStatus(string Serial, TerminalStatusValue Status);
| Property | Type | Description |
|---|---|---|
Serial | string | Terminal serial number |
Status | TerminalStatusValue | Terminal status: Ready, Busy, Offline, or LoggedOut |
Error Handling
SDK methods throw StitchTerminalException for protocol, connection, and argument errors. Session-level failures (e.g. Declined, OfflineTerminal) are delivered as a TerminalSession with a Failure status.
StitchTerminalException
public sealed class StitchTerminalException : Exception
{
public ErrorCode ErrorCode { get; }
}
| Property | Type | Description |
|---|---|---|
ErrorCode | ErrorCode | The error code indicating the type of failure |
Message | string | A human-readable error message |
try
{
var intent = new ChargeIntent(15.00m, "ZAR");
var request = new TerminalSessionCreateRequest
{
Intent = intent,
Nonce = Guid.NewGuid().ToString("N"),
};
TerminalSession session = await terminal.StartSessionAsync(request);
if (session.Status == SessionStatus.Success)
{
Console.WriteLine($"Success! Charge ID: {session.Outcome!.Charge!.Id}");
}
else
{
Console.WriteLine($"Session failed: {session.FailureReason}");
}
}
catch (StitchTerminalException ex)
{
var message = ex.ErrorCode switch
{
ErrorCode.Timeout => "Timed out",
ErrorCode.Cancel => "Cancelled",
_ => $"Error: {ex.Message}",
};
Console.WriteLine(message);
}
ErrorCode
| Value | Description |
|---|---|
Timeout | Operation timed out |
Cancel | Operation cancelled |
Busy | Device busy |
Failed | Operation failed |
NotFound | Resource not found |
Internal | Internal error |
Api | SDK / API error |
Config | Configuration error |
InvalidArgument | Invalid argument |
Buffer | Buffer overflow |
Memory | Memory allocation failed |
WrongState | Wrong state for this operation |
NotSupported | Operation not supported |
Network | Network error |
Server | Server error |
Unauthorized | Unauthorized |
Communication | Communication error |
LoggedOut | Terminal is logged Out |
Connection failures
Connection failures can occur either before a session is created or during an active session. Failures that occur while calling SDK functions are thrown immediately as exceptions. Failures that occur during a pending session are reported asynchronously by completing the Task<TerminalSession> with a Failure session.
The connection to the terminal is established by ConnectAsync and maintained for the lifetime of the Terminal instance. If the SDK is unable to connect to the terminal (for example, if the terminal is offline or incorrect PIN), ConnectAsync throws a StitchTerminalException.
If the connection to the terminal is lost after ConnectAsync has succeeded, the next SDK method call will throw the relevant exception. This will typically occur when creating a new terminal session or when polling GetStatusAsync().
| Scenario | Error Code | Description |
|---|---|---|
| Terminal offline / not connected | Communication | Terminal is not connected or not responding |
| Terminal busy | Busy | Another session is already active on this terminal |
| Terminal not logged in | LoggedOut | For Android terminals that needs to login on the terminal itself |
| Incorrect PIN | Unauthorized | Thrown by ConnectAsync for a VIPA terminal if the pin supplied in ConnectionConfig is incorrect |
| Duplicate nonce | Api | A session with this nonce already exists |
| Invalid request (missing fields, bad values) | InvalidArgument | Missing required fields or invalid values (e.g. null amount) |
| Network error (VIPA) | Network | Cannot reach Stitch servers |
Failure during an onging session
If the connection to a terminal is lost is while StartSessionAsync is ongoing, the session will end as failure with failure reason OfflineTerminal. The ongoing transaction on the terminal will fail and never go into an approved state. This includes if the POS system loses connectivity (for example, the POS device powers down or the process stops)
When the connection is lost during a session for standalone terminals (Android terminals), a charge may have already been authorized by the card network before the terminal went offline. In such cases, the charge will be automatically reversed.
API Reference
Terminal
| Method | Returns | Description |
|---|---|---|
new Terminal(ConnectionConfig config) | Terminal | Construct a terminal with the given connection config. |
terminal.ConnectAsync(CancellationToken ct = default) | Task<TerminalStatus> | Establish the connection to the terminal. Returns the terminal status once connected |
terminal.StartSessionAsync(TerminalSessionCreateRequest request, CancellationToken ct = default) | Task<TerminalSession> | Start a session on this terminal. The returned task completes when the session reaches a terminal state (Success or Failure) |
terminal.GetStatusAsync(CancellationToken ct = default) | Task<TerminalStatus> | Get the terminal status |
terminal.DisposeAsync() | ValueTask | Asynchronously disconnect and release native resources |
terminal.Dispose() | void | Synchronous disposal; calls DisposeAsync().GetAwaiter().GetResult() |
TerminalSession
An immutable snapshot of a session, returned by terminal.StartSessionAsync().
public sealed record TerminalSession(
string Id,
TerminalSessionIntent Intent,
SessionStatus Status,
FailureReason? FailureReason,
SessionOutcome? Outcome,
string Nonce,
string? ExternalReference,
IReadOnlyDictionary<string, string> Metadata,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
| Property | Type | Description |
|---|---|---|
Id | string | The unique session ID (e.g. "ts_abc123") |
Intent | TerminalSessionIntent | The original intent |
Status | SessionStatus | Session status: Success or Failure |
FailureReason | FailureReason? | Why the session failed. null unless Status is Failure |
Outcome | SessionOutcome? | The outcome (charge/mandate). Only present when Status is Success |
Nonce | string | The unique identifier for idempotency |
ExternalReference | string? | The custom identifier. null if not set |
Metadata | IReadOnlyDictionary<string, string> | Key-value pairs for additional information |
CreatedAt | DateTimeOffset | When the session was created |
UpdatedAt | DateTimeOffset | When the session was last updated |
Example: end-to-end flow
using Stitch.Terminal;
var config = ConnectionConfig.Tcp(
TerminalType.Vipa,
"192.168.1.100",
16107,
pin: "1234",
apiKey: "sk_test_abc123");
var terminal = new Terminal(config);
await terminal.ConnectAsync();
var request = new TerminalSessionCreateRequest
{
Intent = new ChargeIntent(25.00m, "ZAR"),
Nonce = Guid.NewGuid().ToString("N"),
ExternalReference = "order-456",
};
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
TerminalSession session = await terminal.StartSessionAsync(request, cts.Token);
if (session.Status == SessionStatus.Success)
{
var charge = session.Outcome!.Charge!;
Console.WriteLine($"Approved. Charge {charge.Id} for {charge.Amount} {charge.Currency}");
}
else
{
Console.WriteLine($"Declined: {session.FailureReason}");
}