Make payment

After Setup, you can now create payments!

Following the example on Connecting to the SDK, you can add a method on C# like this:

public async Task<JsonElement> MakePayment(string id, int totalAmount, int? tipAmount)
{
    var paymentDetails = new
    {
        transactionId = id, // business id for this transaction, or a random UUID
        amount = totalAmount, // total amount in minor units
        tip = tipAmount, // Optional tip amount in minor units (included in the total, not on top of it)
        currency = "GBP", // The ISO 4217 currency code (e.g., "GBP", "EUR").
        useTeyaDefaultUi = true, // if true, the SDK app will show an interactive "in progress" UI window
    };

    return await SendRequestAsync("makePaymentAndSubscribe", paymentDetails);
}

The SDK app will respond to this request with a JSON object in the "result" field of the json-rpc message like this:

{
  "type": "makePaymentAndSubscribe->onPaymentStateChanged",
  "transactionId": "tx-123",
  "amount": 8500,
  "tip": 0,
  "currency": "GBP",
  "gatewayPaymentId": "tr_019851ef-29f2-70e8-800d-fdd875bfa0a4",
  "state": "SUCCESSFUL",
  "inProgressState": null,
  "isFinal": true,
  "reason": null,
  "transactionTimestamp": 1736175612000,
  "debugErrorMessage": null,
  "metadata": {
    "card": {
        "last4": "0616",
        "issuingCountry": "GB",
        "brand": "MASTERCARD",
        "type": "DEBIT"
    },
    "entryMode": "MAGSTRIPE_SWIPED",
    "verificationMethod": "SIGNATURE",
    "applicationId": "A0000000012345",
    "merchantAcquiringId": "5030296",
    "responseCode": "00",
    "authorisationCode": "322290",
    "dcc": null
  }
}

For this method, given the different types of responses you can get and all the information that is available on each, we recommend that you create classes that represent this structure. You can also do the same for all other responses, but it may not be needed for simpler ones.

Here is the example of such structure on C#:

public class MakePaymentStateChange
{
    [JsonPropertyName("transactionId")]
    public string TransactionId { get; set; }

    [JsonPropertyName("amount")]
    public int Amount { get; set; }

    [JsonPropertyName("tip")]
    public int? Tip { get; set; }

    [JsonPropertyName("currency")]
    public string Currency { get; set; }

    [JsonPropertyName("gatewayPaymentId")]
    public string? GatewayPaymentId { get; set; }

    [JsonPropertyName("state")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PaymentState State { get; set; }

    [JsonPropertyName("inProgressState")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public InProgressState? InProgressState { get; set; }

    [JsonPropertyName("isFinal")]
    public bool IsFinal { get; set; }

    [JsonPropertyName("reason")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PaymentStateReason? Reason { get; set; }

    [JsonPropertyName("metadata")]
    public MetadataInfo? Metadata { get; set; }

    [JsonPropertyName("transactionTimestamp")]
    public long? TransactionTimestamp { get; set; }

    [JsonPropertyName("debugErrorMessage")]
    public string? DebugErrorMessage { get; set; }

    public enum PaymentState
    {
        PENDING,
        NEW,
        IN_PROGRESS,
        CANCELLING,
        CANCELED,
        SUCCESSFUL,
        PROCESSING_FAILED,
        COMMUNICATION_FAILED
    }

    public enum InProgressState
    {
        SENT_TO_PAYMENT_APP,
        RECEIVED,
        WAITING_FOR_CARD_ENTRY,
        TIP_SELECTION,
        CARD_PRESENTED,
        PIN_ENTRY,
        INSERT_CARD,
        DCC_SELECTION,
        REMOVE_CARD,
        APP_SELECTION,
        UNKNOWN
    }

    public enum PaymentStateReason
    {
        PROCESSING_FAILED_DECLINED_ONLINE,
        PROCESSING_FAILED_DECLINED_OFFLINE,
        PROCESSING_FAILED_TIMEOUT,
        PROCESSING_FAILED_CONNECTION_ERROR,
        PROCESSING_FAILED_COMM_TIMEOUT,
        PROCESSING_FAILED_CARD_PROCESSING_ERROR,
        UNKNOWN,
        CANCELED_BY_EPOS,
        CANCELED_BY_USER,
        EXPIRED,
        COMMUNICATION_FAILED_NETWORK,
        COMMUNICATION_FAILED_UNEXPECTEDLY,
        COMMUNICATION_FAILED_AUTH_REQUIRED
    }

    public class MetadataInfo
    {
        [JsonPropertyName("card")]
        public Card? CardInfo { get; set; }

        [JsonPropertyName("entryMode")]
        [JsonConverter(typeof(JsonStringEnumConverter))]
        public EntryModeInfo? EntryMode { get; set; }

        [JsonPropertyName("verificationMethod")]
        [JsonConverter(typeof(JsonStringEnumConverter))]
        public VerificationMethodInfo? VerificationMethod { get; set; }

        [JsonPropertyName("applicationId")]
        public string? ApplicationId { get; set; }

        [JsonPropertyName("merchantAcquiringId")]
        public string? MerchantAcquiringId { get; set; }

        [JsonPropertyName("responseCode")]
        public string? ResponseCode { get; set; }

        [JsonPropertyName("authorisationCode")]
        public string? AuthorisationCode { get; set; }

        [JsonPropertyName("dcc")]
        public Dcc? DccInfo { get; set; }

        public class Card
        {
            [JsonPropertyName("last4")]
            public string Last4 { get; set; }

            [JsonPropertyName("issuingCountry")]
            public string? IssuingCountry { get; set; }

            [JsonPropertyName("brand")]
            [JsonConverter(typeof(JsonStringEnumConverter))]
            public CardBrand Brand { get; set; }

            [JsonPropertyName("type")]
            [JsonConverter(typeof(JsonStringEnumConverter))]
            public CardType? Type { get; set; }
        }

        public enum EntryModeInfo
        {
            E_COM,
            EMV_PIN,
            CONTACT_ICC,
            EMV_CONTACTLESS,
            KEYED,
            MAGSTRIPE_SWIPED,
            ON_FILE,
            MAGSTRIPE_FALLBACK
        }

        public enum VerificationMethodInfo
        {
            NONE,
            ELECTRONIC_SIGNATURE,
            ON_DEVICE,
            MANUAL,
            SIGNATURE,
            OFFLINE_PIN,
            ONLINE_PIN,
            OFFLINE_PIN_PLUS_SIGNATURE,
            SECURED_ELECTRONIC_COMMERCE
        }

        public enum CardBrand
        {
            VISA,
            MAESTRO,
            MASTERCARD,
            UNION_PAY,
            JCB,
            DINERS,
            AMEX,
            OTHER
        }

        public enum CardType
        {
            DEBIT,
            CREDIT,
            PREPAID,
            CHARGE,
            PRIVATE,
            DEFERRED_DEBIT,
            UNKNOWN
        }

        public class Dcc
        {
            [JsonPropertyName("conversionFee")]
            public string ConversionFee { get; set; }

            [JsonPropertyName("disclaimer")]
            public string Disclaimer { get; set; }

            [JsonPropertyName("exchangeRate")]
            public string ExchangeRate { get; set; }

            [JsonPropertyName("transactionAmount")]
            public int TransactionAmount { get; set; }

            [JsonPropertyName("transactionCurrency")]
            public string TransactionCurrency { get; set; }
        }
    }

    public override string ToString()
    {
        var metadataString = Metadata == null ? "null" :
            $"{{ CardInfo: {(Metadata.CardInfo == null ? "null" :
                $"{{ Last4: {Metadata.CardInfo.Last4}, IssuingCountry: {Metadata.CardInfo.IssuingCountry}, " +
                $"Brand: {Metadata.CardInfo.Brand}, Type: {Metadata.CardInfo.Type} }}")}, " +
            $"EntryMode: {Metadata.EntryMode}, VerificationMethod: {Metadata.VerificationMethod}, " +
            $"ApplicationId: {Metadata.ApplicationId}, MerchantAcquiringId: {Metadata.MerchantAcquiringId}, " +
            $"ResponseCode: {Metadata.ResponseCode}, AuthorisationCode: {Metadata.AuthorisationCode}, " +
            $"DccInfo: {Metadata.DccInfo} }}";

        return $"MakePaymentStateChange {{ " +
               $"TransactionId: {TransactionId}, " +
               $"GatewayPaymentId: {GatewayPaymentId}, " +
               $"State: {State}, " +
               $"InProgressState: {InProgressState}, " +
               $"Amount: {Amount}, " +
               $"Tip: {Tip}, " +
               $"Currency: {Currency}, " +
               $"IsFinal: {IsFinal}, " +
               $"Reason: {Reason}, " +
               $"TransactionTimestamp: {TransactionTimestamp}, " +
               $"DebugErrorMessage: {DebugErrorMessage}, " +
               $"Metadata: {metadataString} }}";
    }
}

After creating these classes, let's change the MakePayment method to return an instance of MakePaymentStateChange:

public async Task<MakePaymentStateChange> MakePayment(string id, int totalAmount, int? tipAmount)
{
    var paymentDetails = new
    {
        transactionId = id,
        amount = totalAmount,
        tip = tipAmount,
        currency = "GBP",
        useTeyaDefaultUi = true,
    };
    var response = await SendRequestAsync("makePaymentAndSubscribe", paymentDetails);
    return JsonSerializer.Deserialize<MakePaymentStateChange>(response.ToString());
}

Getting intermediate state updates

Something else that is unique to this method call, is that it will make the Teya SDK app actually send events for state updates related to this payment.
These messages will follow the same structure (and so you can also use the same MakePaymentStateChange) class but will not contain the "id" field in the json-rpc message body.

If you go back to our example in Connecting to the SDK, you'll notice that we have an if statement in the ProcessResponse method checking for the presence of the "id" field.

    if (!root.TryGetProperty("id", out JsonElement idElement))
    {
        // This might be a notification, not a response to a request
        Console.WriteLine($"Received notification: {responseJson}");
        return;
    }

For these state updates, this is where you can handle them.
For example, if we simply want to log the intermediate states:

if (!root.TryGetProperty("id", out JsonElement idElement))
{
    root.TryGetProperty("result", out JsonElement resultElement);

    var state = JsonSerializer.Deserialize<MakePaymentStateChange>(resultElement);

    Debug.WriteLine("State change = " + state);
    return;
}

That said, you can do anything with these updates, like exposing them through some sort of callback to the rest of your application and handle them however you prefer.
If you only care about the final step, you can ignore these updates as well.

Hiding Teya UI (& stopping state change updates)

On some less common cases, it may be helpful to programmatically hide the Teya UI and stop receiving state change updates. Usually if the card machine is having some issue like no connectivity or no battery at the time when request is made, the state can be stuck for a really long time.

In this case, your ePOS app can choose to offer a way for the merchant to "force close" the Teya UI.
To do that, you'd implement a new method like:

public async Task<JsonElement> HideTeyaUi(string id)
{
    var parameters = new
    {
        transactionId = id,
    };
    
    return await SendRequestAsync("unsubscribeAll", parameters);
}

The SDK app will respond to this request just with an acknowledgement like:

{
  "type": "unsubscribeAll->ack"
}

Tip explanation

The tip parameter allows you to specify an optional tip amount, in the smallest unit of the currency (e.g., pence for GBP, cents for EUR/USD). When you provide a tip, it is treated as included within the total amount you pass.

For example, if you set:

amount = 100
tip = 20

Then:

The total payment amount will be £1.00 (100 in minor units).
Out of this, £0.20 (20 in minor units) will be considered the tip amount.
In other words, the tip is not added on top of the amount, it is included within the total amount you specify.