Skip to main content

.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>
Windows

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");

VIPA serial auto-detection

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.

Android token

token is required when connecting to a Stitch Android terminal.

VIPA

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}");
StatusDescription
READYTerminal is ready for sessions
BUSYTerminal is currently processing a session
OFFLINETerminal is offline
LOGGED_OUTTerminal 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

StatusDescription
SuccessSession completed successfully. The outcome contains the resulting charge and/or mandate.
FailureSession 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" },
};
PropertyRequiredDescription
IntentRequiredDefines the purpose of the session (e.g. charge, mandate)
NonceRequiredA unique identifier for idempotency. If you attempt to create a session with a nonce that has already been used, an error will be returned
ExternalReferenceOptionalA custom identifier to reference this session in your own system. Multiple sessions can share the same external reference
MetadataOptionalKey-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;
IntentDescription
ChargeIntentTake a payment from the customer's card
RefundIntentRefund a previous charge back to the original card. Linked to a charge
DisbursementIntentPay 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

PropertyRequiredDescription
AmountRequiredThe charge amount in the currency's major unit (e.g. 30.00m for R30.00)
CurrencyRequiredISO 4217 currency code (only "ZAR" supported)
CashbackOptionalCashback 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

PropertyRequiredDescription
AmountRequiredThe refund amount in the currency's major unit. May be less than or equal to the original charge amount
CurrencyRequiredISO 4217 currency code (only "ZAR" supported). Must match the currency of the source session
TerminalSessionRequiredThe ID of the original terminal session being refunded (e.g. "ts_abc123")
ReasonRequiredThe reason for the refund

RefundReason

ValueDescription
FraudThe original charge was fraudulent
RequestedByUserThe customer requested the refund
DuplicateChargeThe 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

PropertyRequiredDescription
AmountRequiredThe disbursement amount in the currency's major unit
CurrencyRequiredISO 4217 currency code (only "ZAR" supported)

Money

public sealed record Money(decimal Amount, string Currency);
PropertyTypeDescription
AmountdecimalAmount in the currency's major unit
CurrencystringISO 4217 currency code

SessionStatus

ValueDescription
SuccessSession completed successfully
FailureSession failed

FailureReason

ValueDescription
ExpiredSession timed out before completion
CancelledByApiSession was cancelled via online API
CancelledBySDKSession was cancelled via CancellationToken
CancelledByTerminalTerminal cancelled transaction, e.g. timeout
BusyTerminalTerminal was busy with another session
OfflineTerminalTerminal is offline
DeclinedPayment 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);
PropertyTypeDescription
IdstringCharge ID (e.g. "ch_abc123")
AmountdecimalThe amount charged in the currency's major unit
CurrencystringThe charge currency in ISO 4217 format (e.g. "ZAR")
CashbackCashback?The cashback dispensed to the customer (null if no cashback)
StatusChargeStatusThe current status of the charge
FailureFailure?Failure details when Status is Failure (null otherwise)
CreatedAtDateTimeOffsetWhen the charge was created
UpdatedAtDateTimeOffsetWhen the charge was last modified
TypeChargeTypeThe type of charge (e.g. InPersonCard)
CardCardCard data (scheme, masked PAN, etc.)
RetrievalReferenceNumberstring?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);
PropertyTypeDescription
ReasonstringThe reason for the failure, available options: authorization_failed
authorization_declined
ResultCodeResultCode?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);
PropertyTypeDescription
ValuestringThe numeric result code value (e.g. "05")
DescriptorstringA machine-readable descriptor for the result code (e.g. "do_not_honour")
DetailstringA 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);
PropertyTypeDescription
AmountdecimalThe cashback amount in the currency's major unit
CurrencystringThe cashback currency in ISO 4217 format (e.g. "ZAR")

ChargeStatus

ValueDescription
ProcessingThe charge is being processed
SuccessThe charge was processed successfully
FailureThe charge did not complete successfully

ChargeType

ValueDescription
InPersonCardAn 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);
PropertyTypeDescription
IdstringRefund ID (e.g. "rf_abc123")
TerminalSessionIdstringThe terminal session ID of the original charge being refunded (e.g. "ts_abc123")
AmountdecimalThe amount refunded in the currency's major unit
CurrencystringThe refund currency in ISO 4217 format (e.g. "ZAR")
StatusRefundStatusThe current status of the refund
FailureFailure?Failure details when Status is Failure (null otherwise)
CreatedAtDateTimeOffsetWhen the refund was created
UpdatedAtDateTimeOffsetWhen the refund was last modified
CardCard?Card data (scheme, masked PAN, etc.)
RetrievalReferenceNumberstring?The retrieval reference number (RRN) assigned by the card network for this transaction

RefundStatus

ValueDescription
ProcessingThe refund is being processed
SuccessThe refund was processed successfully
FailureThe 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);
PropertyTypeDescription
IdstringDisbursement ID (e.g. "dsb_abc123")
AmountdecimalThe amount disbursed in the currency's major unit
CurrencystringThe disbursement currency in ISO 4217 format (e.g. "ZAR")
StatusDisbursementStatusThe current status of the disbursement
FailureFailure?Failure details when Status is Failure (null otherwise)
CreatedAtDateTimeOffsetWhen the disbursement was created
UpdatedAtDateTimeOffsetWhen the disbursement was last modified
CardCardCard data (scheme, masked PAN, etc.) for the card the funds were paid to
RetrievalReferenceNumberstring?The retrieval reference number (RRN) assigned by the card network for this transaction

DisbursementStatus

ValueDescription
ProcessingThe disbursement is being processed
SuccessThe disbursement was processed successfully
FailureThe disbursement did not complete successfully

Card

public sealed record Card(
string Bin,
string Last4,
CardExpiry Expiry,
CardNetwork? Network,
FundingType? FundingType,
CardIssuer? Issuer);
PropertyTypeDescription
BinstringThe first 8 digits of the card number
Last4stringThe last 4 digits of the card number
ExpiryCardExpiryThe card expiry date
NetworkCardNetwork?The card network (may be null)
FundingTypeFundingType?The funding type of the card (may be null)
IssuerCardIssuer?The card issuer details (may be null)

CardExpiry

public sealed record CardExpiry(string Month, string Year);
PropertyTypeDescription
MonthstringThe expiry month in MM format (01-12)
YearstringThe expiry year in YY format (e.g. "25" for 2025)

CardNetwork

ValueDescription
VisaVisa
MastercardMastercard
AmexAmerican Express
DinersDiners Club

FundingType

ValueDescription
CreditCredit card
DebitDebit card
PrepaidPrepaid card

CardIssuer

public sealed record CardIssuer(string Name, string Country);
PropertyTypeDescription
NamestringThe name of the financial institution that issued the card
CountrystringThe 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

PropertyTypeDescription
TypeTerminalTypeThe terminal type (Vipa or Android)
Pathstring?Device path (e.g. "/dev/ttyUSB0", "COM3"), or null to auto-detect (VIPA only)
Pinstring?Termianl login PIN. Required for VIPA terminals
ApiKeystring?Stitch API key used to authenticate the SDK with Stitch backend services. Required for VIPA terminals

TcpConfig

PropertyTypeDescription
TypeTerminalTypeThe terminal type (Vipa or Android)
HoststringHostname or IP address of the terminal
PortintTCP port number (e.g. 16107)
Tokenstring?Authentication token for the terminal. Only required for Stitch Android terminals; null for VIPA
Pinstring?Termianl login PIN. Required for VIPA terminals
ApiKeystring?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);
PropertyTypeDescription
SerialstringTerminal serial number
StatusTerminalStatusValueTerminal 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; }
}
PropertyTypeDescription
ErrorCodeErrorCodeThe error code indicating the type of failure
MessagestringA 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

ValueDescription
TimeoutOperation timed out
CancelOperation cancelled
BusyDevice busy
FailedOperation failed
NotFoundResource not found
InternalInternal error
ApiSDK / API error
ConfigConfiguration error
InvalidArgumentInvalid argument
BufferBuffer overflow
MemoryMemory allocation failed
WrongStateWrong state for this operation
NotSupportedOperation not supported
NetworkNetwork error
ServerServer error
UnauthorizedUnauthorized
CommunicationCommunication error
LoggedOutTerminal 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().

ScenarioError CodeDescription
Terminal offline / not connectedCommunicationTerminal is not connected or not responding
Terminal busyBusyAnother session is already active on this terminal
Terminal not logged inLoggedOutFor Android terminals that needs to login on the terminal itself
Incorrect PINUnauthorizedThrown by ConnectAsync for a VIPA terminal if the pin supplied in ConnectionConfig is incorrect
Duplicate nonceApiA session with this nonce already exists
Invalid request (missing fields, bad values)InvalidArgumentMissing required fields or invalid values (e.g. null amount)
Network error (VIPA)NetworkCannot 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)

Ambiguous outcomes on connection loss

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

MethodReturnsDescription
new Terminal(ConnectionConfig config)TerminalConstruct 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()ValueTaskAsynchronously disconnect and release native resources
terminal.Dispose()voidSynchronous 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);
PropertyTypeDescription
IdstringThe unique session ID (e.g. "ts_abc123")
IntentTerminalSessionIntentThe original intent
StatusSessionStatusSession status: Success or Failure
FailureReasonFailureReason?Why the session failed. null unless Status is Failure
OutcomeSessionOutcome?The outcome (charge/mandate). Only present when Status is Success
NoncestringThe unique identifier for idempotency
ExternalReferencestring?The custom identifier. null if not set
MetadataIReadOnlyDictionary<string, string>Key-value pairs for additional information
CreatedAtDateTimeOffsetWhen the session was created
UpdatedAtDateTimeOffsetWhen 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}");
}