Getting started

If you're building a ePOS application which will not run on the same device as the Teya payments application, then PosLink is the solution. If your application will run on Windows OS, then you are in the right place.

Video Tutorial

Prefer watching over reading? Check out our SDK integration videos:

For Windows OS, Teya provides an executable that will be running in the background while your application is running.
The ePOS app should start Teya SDK app as a sub-process, and communicate with it via Standard input and output.
Your application should start Teya SDK app as soon as it starts itself, and stop it whenever it is exiting or when the user turns off Teya integration (if applicable). In normal situations, Teya SDK app's process should be tied to your app's process.
Teya SDK app will receive requests and return responses in the form of json-rpc messages.

Does your app run on the JVM?

If your ePOS app runs on the JVM (Java or Kotlin), you can add the SDK straight into your app as a library and skip everything on this page - no subprocess, no JSON-RPC.

See Run on the JVM.

The rest of this page describes the approach for apps that are not on the JVM.

1. Get the SDK

You can get the latest SDK from this link:

The zip file contains:

  1. A public certificate (more on this below, in Verify the SDK identity)
  2. The zip of the distributable executable and other important files. This folder and all its contents should be present on the device where Teya integration is needed, alongside your own ePOS application. The actual directory for this folder does not matter, but you'll have to know its path to start the Teya SDK app as a sub process. Whenever there's a new version, you should delete this folder entirely and paste the new one in the same directory.

Updating to a new version

Download the new zip and replace both things it contains:

  1. Delete the app folder entirely and paste the new one in the same directory.
  2. Replace the public certificate with the one from the same zip.

The two belong together. When a certificate expires, Teya reissues it and ships the new one with that release — announced in the changelog — so a certificate you kept from an earlier version can stop matching and the authenticity check fails. Taking both from the same zip is always correct, so it is the safest habit.

2. Connect to the SDK process

Verify the SDK identity

This step is optional, but recommended.
Your ePOS app must have confidence that it is communicating with the real Teya SDK app and not with a malicious application that is pretending to be it.
Depending on your ePOS devices and the security mechanisms that it inherently provides, you decide how to be sure of the executable's identity.

To help with this, Teya signs the main executable and all .dll files with a private key and provides a public certificate (.cer file) that you can use to verify the identity.

How exactly you extract the signature from the executable and verify it against the public certificate depends on the programming language and framework you are using.

Example in C#:

public static bool MatchSignatureCertificate(string exePath, string cerFilePath)
{
    try
    {
        // Load both certificates as X509Certificate2
        X509Certificate2 expectedCert = new X509Certificate2(cerFilePath);
        X509Certificate2 exeCert = new X509Certificate2(X509Certificate.CreateFromSignedFile(exePath));
        
        // Compare thumbprints
        return string.Equals(
            exeCert.Thumbprint, 
            expectedCert.Thumbprint, 
            StringComparison.OrdinalIgnoreCase);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error verifying signature: {ex.Message}");
        return false;
    }
}

Start the sub process

Once the Teya SDK app is verified, you can start it by invoking the main executable at runtime from your application's code. Exactly how to do it will depend on the language and framework you are using, but as an example on C# it would be something like

var processStartInfo = new ProcessStartInfo
{
    FileName = "path\\to\\executable.exe",
    RedirectStandardInput = true,
    RedirectStandardOutput = true,
    UseShellExecute = false
};

var process = Process.Start(processStartInfo);
Hint

If you are not using C#, try to copy paste the snippet above to your favourite AI assistant and ask:

"How to write this C# code in X language?"

Do the same for all snippets in this page, it should give you a good enough approximation so you can get started!

You can save this process handle to be able to send and receive messages from its standard input and output streams.

Putting it all together, a C# example could be:


private Process _process;

public void StartProcess()
{
    if (!MatchSignatureCertificate("path\\executable.exe", "path\\signing_cert.cer"))
    {
        throw new Exception("Teya executable is not signed with the expected key!");
    }

    var processStartInfo = new ProcessStartInfo
    {
        FileName = "path\\to\\executable.exe",
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        UseShellExecute = false
    };

    _process = Process.Start(processStartInfo);
}

If the Teya screens turn out to be slow to draw on a particular till, you can set a different rendering engine. See Report a Problem.

Communicate with the SDK app

Once Teya SDK app is running, you can call methods on it via json-rpc messages. Messages should be sent as json with no white spaces and end in a new line character, such that each message (both requests and responses) are a single line.

An example of a request:

{"jsonrpc":"2.0","method":"initialize","params":{"requesterId":"pos-app-123","requesterVersion":"1.0.0","isProductionEnv":false,"clientId":"client-123","clientSecret":"secret-456"},"id":1}

in more visual friendly form:

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "requesterId": "pos-app-123",
    "requesterVersion": "1.0.0",
    "isProductionEnv": false,
    "clientId": "client-123",
    "clientSecret": "secret-456"
  },
  "id": 1
}

and the corresponding response:

{"jsonrpc":"2.0","result":{"type":"initialize->ack","integrationsAppVersion":"1.2.3"},"id":1}

in more visual friendly form:

{
  "jsonrpc": "2.0",
  "result": {
    "type": "initialize->ack",
    "integrationsAppVersion": "1.2.3"
  },
  "id": 1
}

As seen above, the json-rpc protocol mimics calling a method on another process, passing in a name and the parameters. The id serves as a link between request and response. If the method was able to be called, then a "result" object will be returned with return values (if applicable) and the type of response it is.

Given the nature of json-rpc, some languages have libraries that make it seem like you are just calling methods directly, abstracting away the serialization of the communication and providing a type-safe API (example on C# https://microsoft.github.io/vs-streamjsonrpc/).
However, using such a library is not required. Next sections will go deeper in how to do it manually.

Read responses asynchronously

Given there is a single pipe through which the responses come in, you need to have a single procedure reading from it at all times on a separate thread.
You can start this task as part of the same StartProcess mentioned before:

private Process _process;

public void StartProcess()
{
    //... check exe identity and start the process ...

    Task.Run(ReadResponsesAsync);
}

private async Task ReadResponsesAsync()
{
    using (StreamReader reader = _process.StandardOutput)
    {
        while (!reader.EndOfStream)
        {
            string? line = await reader.ReadLineAsync();

            if (string.IsNullOrEmpty(line))
                continue;

            ProcessResponse(line);
        }
    }
}

The ProcessResponse method would get the "id" from the response and serve the "result" object to whoever is listening for that id.
In the next section, we'll go over how we can do that.

Send requests and wait for the response

Ideally, you would want a method for each Teya SDK app method that would return the corresponding response.
However, you now have a thread that is reading all responses separate from the threads sending the requests, so you need a away for those two to communicate.

In C#, you can use a Dictionary of TaskCompletionSource for a given id, like this:

private Process _process;
// add this 
private Dictionary<string, TaskCompletionSource<JsonElement>> _pendingRequests = new Dictionary<string, TaskCompletionSource<JsonElement>>();

then:

private void ProcessResponse(string responseJson)
{
    JsonElement root = JsonDocument.Parse(responseJson).RootElement;

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

    string id = idElement.GetString();

    TaskCompletionSource<JsonElement> tcs = null;
    lock (_pendingRequests)
    {
        if (_pendingRequests.TryGetValue(id, out tcs))
        {
            _pendingRequests.Remove(id);
        }
    }

    if (tcs == null)
    {
        Console.WriteLine($"Received response for unknown request ID: {id}");
        return;
    }

    // Check if the response contains an error
    if (root.TryGetProperty("error", out JsonElement errorElement) && !errorElement.ValueKind.Equals(JsonValueKind.Null))
    {
        tcs.SetException(new Exception($"JSON-RPC error: {errorElement}"));
    }
    else if (root.TryGetProperty("result", out JsonElement resultElement))
    {
        tcs.SetResult(resultElement);
    }
    else
    {
        tcs.SetException(new Exception("Invalid JSON-RPC response: missing result"));
    }
}

After this, you are all set to writing a generic function that sends a request and awaits for its response:

private async Task<JsonElement> SendRequestAsync(string methodName, object parameters = null)
{
    if (_process == null || _process.HasExited)
    {
        throw new InvalidOperationException("SDK process is not running");
    }

    string reqId = Guid.NewGuid().ToString();

    object request;
    if (parameters == null)
    {
        request = new
        {
            jsonrpc = "2.0",
            id = reqId,
            method = methodName
        };
    }
    else
    {
        request = new
        {
            jsonrpc = "2.0",
            id = reqId,
            method = methodName,
            @params = parameters
        };
    }

    string requestJson = JsonSerializer.Serialize(request);

    var tcs = new TaskCompletionSource<JsonElement>();
    lock (_pendingRequests)
    {
        _pendingRequests[reqId] = tcs;
    }

    await _process.StandardInput.WriteLineAsync(requestJson);
    await _process.StandardInput.FlushAsync();

    return await tcs.Task;
}

With this in place, as an example, lets create a function to call the initialize method on the SDK app:

public async Task<JsonElement> Initialize()
{
    var parameters = new
    {
        requesterId = "pos-app-123",
        requesterVersion = "1.0.0",
        isProductionEnv = false, // true when going to production
        clientId = _clientId, // your client id (you'll have different for dev and prod)
        clientSecret = _clientSecret // your client secret (you'll have different for dev and prod)
    };
    return await SendRequestAsync("initialize", parameters);
}

Initialize is covered in more detail in 3. Initialize and set up the SDK below.

Error responses

If the SDK is unable to call the wanted method, it will not include a "result" field in the json-rpc response message. Instead it will include an "error" field.
This is what's expected on the json-rpc spec.

For "domain errors", that happen as a result of calling the wanted method, then those will still be provided through the "result" field.

There are three scenarios for "error" instead of "result":

1. The SDK was not able to parse the json in the request

This is "code": -1 which means INVALID_REQUEST_FORMAT.
There's also a "message" field with more information about what went wrong when trying to parse the JSON request.

{
    "jsonrpc": "2.0",
    "error": {
        "code": -1,
        "message": "Failed to parse request, reason = Expected ...",
        "data": {
            "integrations_app_version": "1.0.0"
        }
    }
}

2. The SDK was not in the correct state to be able to call the wanted method

This is "code": -2 which means INVALID_STATE.
There's also a "message" field with more information about what went wrong when trying to to call the wanted method.

{
    "jsonrpc": "2.0",
    "error": {
        "code": -2,
        "message": "PosLink not initialized. Please run 'initialize' first.",
        "data": {
            "integrations_app_version": "1.0.0"
        }
    },
    "id": 2
}

3. The request was parseable but there was an illegal argument associated with it

This is "code": -3 which means ILLEGAL_ARGUMENT_EXCEPTION.
There's also a "message" field with more information about what went wrong when trying to call the wanted method.

{
    "jsonrpc": "2.0",
    "error": {
        "code": -3,
        "message": "Which argument was illegal and why...",
        "data": {
            "integrations_app_version": "1.0.0"
        }
    },
    "id": 2
}

3. Initialize and set up the SDK

clientId and clientSecret (used in the initialize example above) are your Client ID and Client Secret. See Setup & Authentication for what these credentials mean and where to get them.

We use eposInstanceId to uniquely distinguish multiple ePOS instances within the same store. This can be useful for scenarios such as filtering or grouping transactions, identifying which ePOS initiated a payment, or troubleshooting issues.
For example, to make the eposInstanceId unique, you could combine the merchant ID with the device ID, e.g., &lt;merchantId&gt;-&lt;deviceId&gt;.

Make sure that the clientId and clientSecret are correct and match the environment you want to connect to (which is defined by the isProductionEnv).

After receiving the response for initialize, call setup. This runs the SDK app's built-in authentication, store selection, and card machine selection UI the first time (see Setup & Authentication for what this covers) - subsequent calls will likely succeed without showing any UI. You always have to call setup after initialize and before calling transaction related methods.

public async Task<JsonElement> Setup()
{
    return await SendRequestAsync("setup");
}

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

Success

{
  "type": "setup->onResponse",
  "response": "SUCCESS",
  "store": {
      "id": "b4b1ee39-167d-4bd5-aace-babcdece094e",
      "name": "Your store name",
      "address": {
          "city": "Gatwick",
          "country": "GB",
          "streetAddressLine1": "London",
          "zipcode": "RH6 0NP"
      }
  },
  "terminal": {
      "id": "eEGSLWR4",
      "name": "Terminal Name",
      "serialNumber": "2047790661"
  }
}

In this case, it also contains details about the store and terminal the user connected to.

When calling this Setup method you should check the JsonElement returned to see whether it was success. If you call any other transaction related method and Setup did not succeed, then the SDK app will return an error response with code: -2 which means INVALID_STATE.

The final details of both authentication and the store/card machine link are stored on disk by the SDK. You can clear them individually by calling the appropriate method below (or together if you call both).

After calling either of them (or both), you'll have to call the setup method again, which will show UI corresponding to the flow that was cleared.

Get card machines

Retrieve the card machines for the currently linked store with the getTerminals method. Pass forceRefresh: true to bypass the cache and fetch from the network, or forceRefresh: false to use cached results.

public async Task<JsonElement> GetTerminals(bool forceRefresh = false)
{
    var parameters = new
    {
        forceRefresh = forceRefresh
    };
    return await SendRequestAsync("getTerminals", parameters);
}

The SDK app will respond with a JSON object in the "result" field:

Success

{
  "type": "getTerminals->onResult",
  "terminals": [
    {
      "id": "eEGSLWR4",
      "name": "Terminal Name",
      "serialNumber": "2047790661"
    },
    {
      "id": "xK9mP2Lq",
      "name": "Another Terminal",
      "serialNumber": "3058801772"
    }
  ]
}

Use linkTerminal to switch card machines (see Setup & Authentication). Pass a specific card machine ID (e.g. one returned by getTerminals), or null to show the SDK's built-in selection UI.

// Option 1: Show the terminal selection UI
public async Task<JsonElement> LinkTerminal()
{
    var parameters = new
    {
        terminalId = (string?)null
    };
    return await SendRequestAsync("linkTerminal", parameters);
}

// Option 2: Link directly by terminal ID
public async Task<JsonElement> LinkTerminal(string terminalId)
{
    var parameters = new
    {
        terminalId = terminalId
    };
    return await SendRequestAsync("linkTerminal", parameters);
}

The SDK app will respond with a JSON object in the "result" field:

Success

{
  "type": "linkTerminal->onResult",
  "terminal": {
    "id": "eEGSLWR4",
    "name": "Terminal Name",
    "serialNumber": "2047790661"
  }
}

Setup without card machine linking

Use setupAuthAndStore to defer card machine selection (see Setup & Authentication) and link one later with linkTerminal before making payments.

public async Task<JsonElement> SetupAuthAndStore()
{
    return await SendRequestAsync("setupAuthAndStore");
}

The SDK app will respond with a JSON object in the "result" field:

Success

{
  "type": "setupAuthAndStore->onResponse",
  "response": "SUCCESS",
  "store": {
      "id": "b4b1ee39-167d-4bd5-aace-babcdece094e",
      "name": "Your store name",
      "address": {
          "city": "Gatwick",
          "country": "GB",
          "streetAddressLine1": "London",
          "zipcode": "RH6 0NP"
      }
  }
}

No terminal is included in this response - call linkTerminal to link one before making payments.

Clear authentication

Call "clearUserAuth" to clear authentication and the device link (see Setup & Authentication).

Following the same C# example, that would involve adding this new method:

public async Task<JsonElement> ClearUserAuth()
{
    return await SendRequestAsync("clearUserAuth");
}

For this method, the SDK will not respond with any additional information, it will just respond with an acknowledgement.

Call "clearDeviceLink" to clear only the store/card machine link, preserving authentication.

Following the same C# example, that would involve adding this new method:

public async Task<JsonElement> ClearDeviceLink()
{
    return await SendRequestAsync("clearDeviceLink");
}

For this method, the SDK will not respond with any additional information, it will just respond with an acknowledgement.

Sample app

A complete Windows sample application demonstrating POSLink integration with the Teya ePOS SDK. This app shows how to connect a Windows ePOS application to a separate Teya payment card machine over the cloud.

This sample app is a reference implementation for learning and development purposes. It is not intended for production use.

View on GitHub