This section describes how authentication keys are used by Token.io and how the API is signed.
You can access general API features using the Dashboard's Guest Experience method. However, to produce testable payloads for measuring/calibrating your integration and for production integrations, you'll need an authentication key. The Dashboard can generate an API key for you to test your integration in the Sandbox environment, or you can create a JWT key using the Authentication keys API endpoints, which can be used in both Sandbox and Production. For the Production environment, you'll need to use JWT authentication to connect securely to the bank.
The Token.io Production environment requires JWT authentication. The Sandbox environment accepts both JWT authentication or an API key.
Download this example [Postman collection] to see how JWT authentication works with Payments v2.
JWT, or JSON Web Token, is an open standard (RFC 7519) method of securely sending information in a JSON object between two parties. The JWT can be encrypted to hide the information, or digitally signed to verify the integrity of the claims contained within it. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
Token.io uses a JWT to authenticate the client and verify that the request has not been tampered with. Therefore, whenever you, as a TPP, are making calls to the Token.io gateway, you send the JWT in the authorization header using the bearer scheme.
Token.io uses public/private key pairs encrypted using the RSA or ECDSA crypto-systems.
The information contained in signed tokens is exposed, even though it can't be changed. For this reason, never put secret information within the token and, because all JWTs need to be treated as secure credentials, do not keep them longer than necessary.
JWTs can be broken down into three parts: header, payload, and signature. Each part is separated from the other by a dot (.), and follows this structure:
Header.Payload.Signature
The Header consists of the type of the token, which is JWT, and the signing algorithm used to generate the signature. A simple header looks like this, where ES256 is the SHA-256 hashing algorithm used to generate the signature:
{
"alg": "ES256",
"typ": "JWT",
"exp": "1723404033117", // expiration in milliseconds
"mid": "m:QWu4AjKk13DAAe1NiVLXeBRDwtt:5zKtXEAq", // member ID
"kid": "1x7df4vuFUHYQCa7", // key ID
"method": "POST", // HTTP method
"host": "api.dev.token.io:443", // base URL
"path": "/banks/iron/consents", // endpoint
"query": ""// only where required
}This header JSON is then Base64Url encoded to form the first part of the JWT.
eyJhbGciOiAiRVMyNTYiLMKgInR5cCI6ICJKV1QiLCAiZXhwIjogIjE3MjM0MDQwMzMxMTciLCAibWlkIjogIm06UVd1NEFqS2sxM0RBQWUxTmlWTFhlQlJEd3R0OjV6S3RYRUFxIiwgImtpZCI6ICIxeDdkZjR2dUZVSFlRQ2E3IiwgIm1ldGhvZCI6ICJQT1NUIiwgImhvc3QiOiAiYXBpLmRldi50b2tlbi5pbzo0NDMiLCAicGF0aCI6ICIvYmFua3MvaXJvbi9jb25zZW50cyIsICJxdWVyeSI6ICIifQThe Token.io JWT header parameters are described in the following table:
| Parameter | Description | Optional/Required? |
|---|---|---|
alg | Algorithm – case-sensitive string identifying the signing key algorithm (e.g., "RS256" or "EdDSA") | Required |
typ | Specifies "jwt" | Required |
exp | Expiration time in unix/epoch milliseconds, i.e. , milliseconds from the epoch of 1970-01-01T00:00:00Z (< 10 minutes from the time of the request is recommended) | Required |
mid | memberId string | Required |
kid | keyId string | Required |
method | String indicating HTTP method (e.g., GET / POST / PUT / DELETE) | Required |
host | String identifying the host of your request (e.g., "api.dev.token.io") | Required |
path | String defining the path of your request (e.g., "/banks/iron/consents") | Required |
query | Present if there is a query in your request (e.g., "type=access") | Only required if request has a query. |
The second part of the token is the payload, which contains the content of the body of the request. The body of the request is the requestPayload, populated with all relevant request fields, and must be identical to the body of the request in every respect or authentication will fail. Even the position of spaces must be identical. In calls that do not have a body (e.g., GET requests), no body content is required in the JWT; i.e., the payload is empty.
The JWT payload for a simple transfer might look like this:
{
"requestPayload": {
"to": {
"alias": {
"type": "DOMAIN",
"value": memberAliasDomain
},
"id": memberId
},
"transferBody": {
"currency": "GBP",
"amount": "2.00",
"instructions": {
"transferDestinations": [{
"fasterPayments": {
"sortCode": "608371",
"accountNumber": "32525024"
},
"customerData": {
"legalNames": ["testAccount"]
}
}
},
"description": "PKI-PIS-TEST",
"redirectUrl": "https://dlng.io/blank/",
"refId": str(exp)
}The payload is Base64Url encoded to form the second part of the token:
ewrCoCJyZXF1ZXN0UGF5bG9hZCI6IHsKwqDCoMKgwqAidG8iOiB7CsKgwqDCoMKgwqDCoMKgwqAiYWxpYXMiOiB7CsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJ0eXBlIjogIkRPTUFJTiIsCsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJ2YWx1ZSI6IG1lbWJlckFsaWFzRG9tYWluCsKgwqDCoMKgwqDCoMKgwqB9LArCoMKgwqDCoMKgwqDCoMKgImlkIjogbWVtYmVySWQKwqDCoMKgwqB9LArCoMKgwqDCoCJ0cmFuc2ZlckJvZHkiOiB7CsKgwqDCoMKgwqDCoMKgwqAiY3VycmVuY3kiOiAiR0JQIiwKwqDCoMKgwqDCoMKgwqDCoCJhbW91bnQiOiAiMi4wMCIsCsKgwqDCoMKgwqDCoMKgwqAiaW5zdHJ1Y3Rpb25zIjogewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAidHJhbnNmZXJEZXN0aW5hdGlvbnMiOiBbewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJmYXN0ZXJQYXltZW50cyI6IHsKwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJzb3J0Q29kZSI6ICI2MDgzNzEiLArCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgImFjY291bnROdW1iZXIiOiAiMzI1MjUwMjQiCsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgfSwKwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAiY3VzdG9tZXJEYXRhIjogewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgImxlZ2FsTmFtZXMiOiBbInRlc3RBY2NvdW50Il0KwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqB9CsKgwqDCoMKgwqDCoMKgwqB9CsKgwqDCoMKgfSwKwqDCoMKgwqAiZGVzY3JpcHRpb24iOiAiUEtJLVBJUy1URVNUIiwKwqDCoMKgwqAicmVkaXJlY3RVcmwiOiAiaHR0cHM6Ly9kbG5nLmlvL2JsYW5rLyIsCsKgwqDCoMKgInJlZklkIjogc3RyKGV4cCkKfQThe signature part of a JWT is used for verification of the header and payload fields. You create the signature by combining the base64url encoded representations of the header and payload with a dot (.), and sign it using the algorithm in the header and the public key that you have provided to Token.io.
For example if you want to use the ES256 algorithm, the signature will be created in the following way:
ES256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)to give this:
eyJhbGciOiAiRVMyNTYiLMKgInR5cCI6ICJKV1QiLCAiZXhwIjogIjE3MjM0MDQwMzMxMTciLCAibWlkIjogIm06UVd1NEFqS2sxM0RBQWUxTmlWTFhlQlJEd3R0OjV6S3RYRUFxIiwgImtpZCI6ICIxeDdkZjR2dUZVSFlRQ2E3IiwgIm1ldGhvZCI6ICJQT1NUIiwgImhvc3QiOiAiYXBpLmRldi50b2tlbi5pbzo0NDMiLCAicGF0aCI6ICIvYmFua3MvaXJvbi9jb25zZW50cyIsICJxdWVyeSI6ICIifQ.ewrCoCJyZXF1ZXN0UGF5bG9hZCI6IHsKwqDCoMKgwqAidG8iOiB7CsKgwqDCoMKgwqDCoMKgwqAiYWxpYXMiOiB7CsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJ0eXBlIjogIkRPTUFJTiIsCsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJ2YWx1ZSI6IG1lbWJlckFsaWFzRG9tYWluCsKgwqDCoMKgwqDCoMKgwqB9LArCoMKgwqDCoMKgwqDCoMKgImlkIjogbWVtYmVySWQKwqDCoMKgwqB9LArCoMKgwqDCoCJ0cmFuc2ZlckJvZHkiOiB7CsKgwqDCoMKgwqDCoMKgwqAiY3VycmVuY3kiOiAiR0JQIiwKwqDCoMKgwqDCoMKgwqDCoCJhbW91bnQiOiAiMi4wMCIsCsKgwqDCoMKgwqDCoMKgwqAiaW5zdHJ1Y3Rpb25zIjogewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAidHJhbnNmZXJEZXN0aW5hdGlvbnMiOiBbewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJmYXN0ZXJQYXltZW50cyI6IHsKwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCJzb3J0Q29kZSI6ICI2MDgzNzEiLArCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgImFjY291bnROdW1iZXIiOiAiMzI1MjUwMjQiCsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgfSwKwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAiY3VzdG9tZXJEYXRhIjogewrCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgImxlZ2FsTmFtZXMiOiBbInRlc3RBY2NvdW50Il0KwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqB9CsKgwqDCoMKgwqDCoMKgwqB9CsKgwqDCoMKgfSwKwqDCoMKgwqAiZGVzY3JpcHRpb24iOiAiUEtJLVBJUy1URVNUIiwKwqDCoMKgwqAicmVkaXJlY3RVcmwiOiAiaHR0cHM6Ly9kbG5nLmlvL2JsYW5rLyIsCsKgwqDCoMKgInJlZklkIjogc3RyKGV4cCkKfQ.eaU8LZJnMtY3mPl4vBXVCVUuyeSeAp8zoNaEOmKS4XYYour private key remains unknown to Token.io. Only the public key is shared.
When you upload your public key to Token.io, a keyId is generated. You'll need to specify this keyId in the Authorization header so we know which key you're using for signing. Any change or modification to this particular JWT thereafter, will fail verification.
All base64 URL encoding should be without padding in accordance with RFC 7515 Appendix C. Token.io also supports detached JWT in accordance with RFC 7515 Appendix F, which you can leverage to detach payload content from the request header.
Here's how:
// The request body
String body; // JWT payload
String header; // JWT header
String encodedHeader = Base64UrlEncode(header);
String encodedPayload = Base64UrlEncode(body);
String signingInput = encodedHeader + "." + encodedPayload;
String signature = sign(signingInput);
// Detached JWT
String jwt = encodedHeader + ".." + signature;Token.io supports both JWT practices — original and detached.
For detached JWT, the middle part between encodedHeader and signature (e.g., encodedBody) is optional and can be omitted, since it is a duplicate of the request body.
In general, detached JWT can save bandwidth, especially when the request body is large.
Key pairs, consisting of a private and public key, are required for JWT authentication. The public key serves to verify that the payload has been signed by the owner of the private key. As long as the private key has not been compromised, you can continue to use the key pair. The public key of the key pair must be uploaded to Token.io services and parsed to authenticate payments and data requests in Token.io. To ensure security and compliance requirements, the public key can be rotated on a regular basis.
We recommend that the initial JWT key is uploaded using the Dashboard, but subsequent JWT keys may be uploaded using either the Dashboard or the API Authentication keys endpoints.
You can create the key pair using any of the following algorithms:
EDDSA - equivalent to ED25519
ECDSA_SHA256 - equivalent to EC prime256v1
RS256 - equivalent to RSA 256 bits
The public key may have either of these formats:
PEM format
Token.io's custom Base64 URL format
The key pair can be generated using a programming language, such as Java, or by using Open SSL on the command line. The public key is then extracted from the key pair and formatted.
Any of the key types, EDDSA, ECDSA_SHA256, or RS256, can be generated in Java using the following library and code snippets. These examples will generate Base64 URL formatted public keys.
Library: https://mvnrepository.com/artifact/com.google.guava/guava
EDDSA/ED25519:
Java
KeyPair keyPair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
byte[] encodedPublicKey = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
.getPublicKeyData()
.getOctets();
String base64UrlEncodedPublicKey = BaseEncoding.base64Url().omitPadding().encode(encodedPublicKey);ED25519_SHA256:
Java
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
keyGen.initialize(256);
KeyPair keyPair = keyGen.generateKeyPair();
String base64UrlEncodedPublicKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());RS256/RSA:
Java
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
String base64UrlEncodedPublicKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());There are a number command variations you can use to generate key pairs, extract the public key and format it, based on your preference and security policy.
ED25519_SHA256:
Here's an example of an Open SSL command line script for generating ED25519_SHA256 key pairs, extracting the public key in Base64 URL format, ready for uploading in the Dashboard or API.
// Generate the key in pem format
openssl ecparam -genkey -name secp521r1 -noout -out key.pem
// Get the PKCS8 private key in pem format
openssl pkcs8 -topk8 -inform pem -in key.pem -outform pem -nocrypt -out private.pem
// Extract the public key
openssl ec -in private.pem -pubout -out public.pem
// Print the private key in base64 URL encoded
cat private.pem | sed -E "s/(-----[^-]* KEY-----)//" | sed 's/+/-/g' | sed 's/\//_/g' | tr -d '\n='
// Print the public key in base64 URL encoded. This is the string you will upload to Token.io Dashboard or API
cat public.pem | sed -E "s/(-----[^-]* KEY-----)//" | sed 's/+/-/g' | sed 's/\//_/g' | tr -d '\n='RS256/RSA:
Example Open SSL command line prompts for generating a Base64 URL encoded RS256 public key might look like this.
// Generate the key in pem format
openssl genrsa -out private.pem 2048
// Extract the public key
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
// Print the private key in base64 URL encoded
cat private.pem | sed -E "s/(-----[^-]* KEY-----)//" | sed 's/+/-/g' | sed 's/\//_/g' | tr -d '\n='
// Print the public key in base64 URL encoded. This is the string you will upload to Token.io Dashboard or API
cat public.pem | sed -E "s/(-----[^-]* KEY-----)//" | sed 's/+/-/g' | sed 's/\//_/g' | tr -d '\n='The following examples show command line prompts for generating public keys in the PEM format.
EDDSA/ED25519:
openssl genpkey -algorithm Ed25519 -out private-key.pem
openssl pkey -pubout -in private-key.pem -out public-key.pemED25519_SHA256:
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pemRS256/RSA:
openssl genpkey -algorithm rsa -out private-key.pem
openssl pkey -pubout -in private-key.pem -out public-key.pemTo upload a public key to Token.io using the Dashboard, follow these steps.
If your public key is in PEM format, you won't be able to upload the public.pem file directly in the Dashboard.
You'll need to open the file in a text editor then paste the key from there, as a string, into the Public Key field in the Upload Public Key modal.
In the Dashboard navigation, click Settings, then click Authentication Keys.
Under Authentication Keys, click the Public Key Authentication tab, then click Upload Key.
Enter your Public Key in the field provided, choose a Key Algorithm, then click Save.
This associates your public key with a KEY ID and KEY TYPE and lists it with any other public keys you've uploaded.

If you decide to delete a key, click to select the row in the list, then click Delete.
The public key can be managed using the Authentication keys endpoints in the Token.io API:
We recommend that the initial JWT key is uploaded using the Dashboard. Subsequently the Authentication keys endpoints can be used to update existing authentication keys, in either Base64 URL or PEM format.
To submit a public key to Token.io, use the following endpoint:
The inputs to this endpoint include:
key algorithm
public key
expiry date for the key in epoch format (optional)
If successful, a key ID is returned in the response. If Token.io is unable to complete the request successfully, an appropriate error is returned, e.g., for cases where Token.io is unable to parse the submitted public key, a 400 HTTP error code is returned.
A sample request and response is shown below:
Request:
POST https://api.sandbox.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys
{
"keyAlgorithm": "ED25519",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAGxDta2XXlr6Vxqk4kJq3+bLowoimRo+B52stoO7AWNg=\n-----END PUBLIC KEY-----"
}Response:
{
"keyId": "_NouLPTuo7WBLBV6"
}If you need to submit a new public key, you use the same endpoint and provide the new key to Token.io.
To retrieve the current set of active keys at Token.io, use the following endpoint:
The response contains a list of keys submitted to Token.io, including the following information:
key ID
public key
key algorithm
expiry date (if set for the key)
A sample request and response is shown below:
Request:
GET https://api.sandbox.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keysResponse:
{
"key": [{
"id": "cJSOA7nQscQBScnE",
"publicKey": "_XB9nuYp65XSrXx5irLfMGD9qVsxzkAEWw7NCs8TFgk",
"algorithm": "ED25519",
"expiresAtMs": "1731530316000"
},
{
"id": "7h22Ata7h7ZLv5el",
"publicKey": "tknYBDBSkSbcLqPwk3z6VQFbqAZrInsZ4xHR4CRKzZM",
"algorithm": "ED25519",
"expiresAtMs": "1731530316000"
},
{
"id": "eep-VtCNYXo00LIS",
"publicKey": "SxJdoqbrUNYDA_cALwTMerehR4F_Gmelcwtk_sjpBJI",
"algorithm": "ED25519",
"expiresAtMs": "1731530316000"
}
]
}To retrieve a specific key, use the following endpoint:
GET /member/{member_id}/keys/{key_id}
The response contains a list of keys submitted to Token.io. It will include the following information:
key ID
public key
key algorithm
expiry date (if set for the key)
A sample request and response is shown below:
Request:
GET https://api.sandbox.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys/cJSOA7nQscQBScnEResponse:
{
"key": {
"id": "cJSOA7nQscQBScnE",
"publicKey": "_XB9nuYp65XSrXx5irLfMGD9qVsxzkAEWw7NCs8TFgk",
"algorithm": "ED25519",
"expiresAtMs": "1731530316000"
}
}To remove a key from Token.io, use this endpoint:
DELETE /member/{member_id}/keys/{key_id}
A sample request is shown below:
Request:
DELETE https://api.sandbox.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys/eep-VtCNYXo00LISIn this case, if successful, an HTTP 200 response is returned. If Token.io is unable to complete the request successfully, an appropriate error is returned.
For cases where Token.io is unable to complete the delete operation successfully, a 400 HTTP error code is returned. For example, if the key provided is invalid, then the HTTP error code will be 400 and the error response will be: “INVALID_ARGUMENT: Key with given keyId does not exist”
You may receive the following error responses when using the Authentication key endpoints:
Invalid keyId
Request:
GET https://api.dev.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys/1234Response:
{
"INVALID_ARGUMENT: Key with given keyId does not exist"
}Invalid public key
Request:
POST https://api.dev.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys
{
"memberId: "m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq",
"keyAlgorithm": "RS256",
"publicKey": "invalid"
}Response:
{
"INVALID_ARGUMENT: Provided public key is not in a recognised format for algorithm: RS256"
}Public key provided is of a different algorithm to what's in the algorithm field
Request:
POST https://api.dev.token.io/members/m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq/keys
{
"memberId: "m:3qVTbXqXZza2VTKa28BPbExmxz9t:5zKtXEAq",
"keyAlgorithm": "RS256",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAGxDta2XXlr6Vxqk4kJq3+bLowoimRo+B52stoO7AWNg=\n-----END PUBLIC KEY-----"
}Response:
{
"INVALID_ARGUMENT: Provided public key is not in a recognised format for algorithm: RS256"
}The Authorization header proper contains the "Bearer" schema plus the jwt.
String Authorization = "Bearer " + jwtHere's an example of the completed Authorization header with no payload:
Bearer
eyJhbGciOiJFZERTQSIsImtpZCI6IjF4N2RmNHZ1RlVIWVFDYTciLCJtaWQiOiJtOlhUalhlMkFQZTRvdmVaalE4cHoyNGdEbUZEcTo1ekt0WEVBcSIsImhvc3QiOiJsb2NhbGhvc3Q6ODAwMCIsIm1ldGhvZCI6IlBPU1QiLCJwYXRoIjoiL2JhbmtzL2lyb24vdXNlcnMifQ..bi3wxEoMHIul_F2f
7gCDvgjHQKCjIyP9_SkQns-yXpS0UqoaOqSJrW89COexU71gt-mH3jH6mtp2aksEywvFDgAs the token is sent in the Authorization header, Cross-Origin Resource Sharing (CORS) isn't an issue because this type of authorization doesn't use cookies.
Here are complete code samples for constructing the JWT authentication:
Java
{
private String createAuthorization(
String method,
String uriPath,
String memberId,
Signer signer,
@Nullable String requestBody,
@Nullable String queryString) {
JsonObject header = JsonObject();
String alg = "eddsa";
if (signer.getAlgorithm().equalsIgnoreCase("EC")) {
alg = "es256";
} else if (signer.getAlgorithm().equalsIgnoreCase("RSA")) {
alg = "rs256";
}
header.addProperty("alg", alg);
header.addProperty("kid", signer.getKeyId());
header.addProperty("mid", memberId);
header.addProperty("host", hostUri);
header.addProperty("method", method);
header.addProperty("path", uriPath);
if (queryString != null) {
header.addProperty("query", queryString);
}
String body = nullToEmpty(requestBody);
Encoder encoder = Base64.getUrlEncoder().withoutPadding();
String encodedHeader = encoder.encodeToString(header.toString().getBytes());
String encodedBody = encoder.encodeToString(body.getBytes());
String signature = signer.sign(encodedHeader + "." + encodedBody);
String jwt = encodedHeader + "." + "." + signature;
"Bearer" + " " + jwt;
}JavaScript
getJwtAuth(method, host, path, body, query) {
headers = {};
(method) headers = { ...headers, method: method.toString().toUpperCase() };
(host) headers = { ...headers, host: host };
(path) headers = { ...headers, path: path };
headers = { ...headers, alg: "ES256" };
keyId = generatePublicKeyId(fs.readFileSync("keys/public.key", "utf8"));
(keyId) headers = { ...headers, kid: keyId };
(memberId) headers = { ...headers, mid: memberId };
(query) {
headers = { ...headers, query: JSON.stringify(query) };
}
// get time stamp in future
exp = Math.round(Date.now()) + 300;
headers = { ...headers, exp: exp };
encodedBody = base64Url(body ? JSON.stringify(body) : "");
encodedHeader = base64Url(JSON.stringify(headers));
//create signature
signer = crypto.createSign("sha256")
signer.update(encodedHeader + "." + encodedBody);
signature = signer.sign(fs.readFileSync("keys/private.pem"));
signature = base64Url(signature);
"Bearer" + " " + encodedHeader + "." + encodedBody + "." + signature;
}Here is a working sample query:
curl -X GET https://api.dev.token.io/banks/bdp-budapest/info \
-H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkhZalJ4N2dDQzZuR29sd2MiLCJtaWQiOiJtOjJuVU5IYUFETFJyZktqbm1aV3YyYlZUV0o0b0s6NXpLdFhFQXEiLCJob3N0IjoiYXBpLmRldi50b2tlbi5pbyIsIm1ldGhvZCI6IkdFVCIsInBhdGgiOiIvYmFua3MvYmRwLWJ1ZGFwZXN0L2luZm8ifQ..iea5LVEmvFrr6JQPXj_hRucHx0EpRs6Ma8q8M-YYYHzovQ7T2ys8Wn6fG-3Cux2lU20nT4k3ZgjM2nSTCw9mlA6wVtUbodvcFnBry-OgGitHVOMEKR1JELt3XB_JNrJBhQGnDTFlYRTdc_hS7inxIr9mEJRNs5SAyAsHvEUKEbVyMFsHzcfAu_qrvRG6RRV7lc-SW0vckBs3LXaU-bfkf7p4TM4yqBXHii_0k-zLUYWJzt9E1JbAF-cLok_8cvaxRhKe3WF__45ZL8bTuLiAA9anTl3yUaRQ6G--xw7pWG4D730VYI2HvR1azmKac-dtNM9lOM2GPnfOgtRl2TW3Sw'There are some edge cases in which the path in the header contains a character that is not URL-safe. For these cases, Token.io expects a URL-encoded endpoint but a raw path in the JWT Authorization header.
Here's an example:
curl --location --request GET
'https://api.dev.token.io/accounts/a:GbNbxvMDQJmkcDXjW9AxhRYtKGYTebWWZKxekEtuWVkX:8QSNh
wKjRP1x/transaction/O%3B5823' \\
\--header 'Authorization: Bearer
`eyJhbGciOiAiRVMyNTYiLCJtaWQiIDogIm06M2FZNlJYVGRTWUxpdmJoWUZVeUdtcVlrR2R2Wjo1ekt0WEVBcSIsImtpZCI6ICIyTjVYTDdGMWdyTW1CazVVIiwibWV0aG9kIjoiR0VUIiwiaG9zdCI6ICJhcGkuZGV2LnRva2VuLmlvIiwicGF0aCI6Ii9hY2NvdW50cy9hOkdiTmJ4dk1EUUpta2NEWGpXOUF4aFJZdEtHWVRlYldXWkt4ZWtFdHVXVmtYOjhRU05od0tqUlAxeC90cmFuc2FjdGlvbi9POzU4MjMiLCJxdWVyeSI6IiJ9`..`MEUCIEPMA6pXUM3Vl222BPCTLmxENEPQBGvX69PjxYQAH7R2AiEA7bL9XvgsJXFTphRQt5c9ZRHOKh12880p7Xu5olrnRcw`Here, it's important to note that the raw transaction ID in the example above is URL-encoded in the path, in accordance with the RESTful standard.
Copy-paste your desired JWT components — header and payload — into jwt.io Debugger to decode, verify, and generate JWTs.
The Token.io sandbox environment can use an API key or JWT authentication.
The API key digitally identifies your application or project and its usage permissions when it calls the API.
To generate an API key for use in the Sandbox environment, follow these steps.
In the Dashboard navigation, click Settings, then click Authentication Keys.
Under Authentication Keys, click the Generate an API key tab.
If you already have one, your current API Key is obscured. To see it, click the "eye" icon on the right. You can generate a new one, but it will replace the current one, which will no longer be valid.
If you are a new TPP, click Generate API Key, then click Get Key.
Choose an appropriate place to Save the key on your local machine and remember its location. This is now your secret key.
Do not share the API key with anyone outside of your organization.
The GET /accounts/{accountId} and GET /accounts calls need to pass in your API key and the access tokenId in the header. The tokenId indicates on which user's behalf you're making the request (on-behalf-of).
{{}}{{}}Here's an example using an API key:
-H "Authorization: Basic bS0yYm0yV3o5cFI1WlduMUw1M1NGeE4zSlZ4N2VMLTV6S3RYRUFxOjFmODM0YzcxLTkwMDctNGE4Ny1hZTU0LTk0MGQ2NzVh YmJjYg=="-H "on-behalf-of: ta:3eYPU1BEKKunfmYgQuSKXFCeo851C5Y3XiZW3XA465TU:5zKtXEAq"Remember to replace the values above in yellow with your API key and access tokenId, respectively. If the authorization header is not included, a GET /accounts/{accountId} or GET /accounts call will fail.
POST /transfer calls only need to include the API key in the authorization header.
{{}}Here's an example using an API key:
-H "Authorization: BasicS0yYm0yV3o5cFI1WlduMUw1M1NGeE4zSlZ4N2VMLTV6S3RYRUFxOjFmODM0YzcxLTkwMDctNGE4Ny1hZTU0LTk0MGQ2NzVhYmJjYg=="Remember to replace the value above in yellow with your API key. If this header is not included, your POST /transfer call will fail.
Although not required, once you're set up for message signing with the Token.io API, it's good practice to validate the signature included in a response to verify that the payload is legitimate and there has been no tampering.
Your digital signature validates the authenticity and integrity of your message. As the digital equivalent of a handwritten signature or stamped seal, a digital signature, used appropriately, should obviate tampering and impersonation. Digital signatures also provide evidence of origin, identity and the status of the message, acknowledging informed consent by the signer.
Digital signatures are based on public key cryptography, also known as asymmetric cryptography. Using a public key algorithm, such as RSA, you can generate two keys that are mathematically linked: one private and one public. Digital signatures work through public key cryptography's two mutually-authenticating cryptographic keys. The individual who is creating the digital signature uses their own private key to encrypt signature-related data; the only way to decrypt that data is with the signer's public key. This is how digital signatures are authenticated.
Signatures for the following messages are signed by Token.io's public key and can be verified:
any webhook sent by Token.io
These can and should be validated by the TPP to ensure response payload integrity and security.
All other messages are not signed by Token.io's public key and therefore the signatures cannot be verified.
See Webhook signature validation for further information.
The redirect URL you receive in response to a token request will have several URL-encoded query parameters appended to it, as seen in the following example.
{REDIRECT_URL}?signature=%7B%22memberId%22%3A%22m%3A2MsNiHgYNphSz2j9GbE83Yqr7iod%3A5zKtXEA %22%2C%22keyId%22%3A%221x7df4vuFUHYQCa7%22%2C%22signature%22%3A%227snNY7SxIijh4t6OVpxJ7HrND
4CAxwO4K5cox2J6SL0zNeU9AKyUz8MkLijlignsz-_gZJniueY--I8prGlvCg%22%7D&state=%257B%257D&tokenI d=ta%3A6c2PJCKHWVNHVETNMDLeP2pqQ7Kp1FXPKNwj7tofkTfH%3A5zKtXEAq
Defined in the table below, the query parameter names are highlighted above so you can easily spot them.
| Parameter | Description |
|---|---|
signature | The Token.io-generated signature based on the response using ED25519. |
state | Optional. The string specified by the TPP-developer so that state is persisted between the request and callback phases of the flow. |
tokenId | The ID of the requested token, which can be redeemed for information or payment. |
For GET /token-requests/{{request_id}}/result the following applies:
The
rawSignatureis thesignatureattribute within thesignatureobject (i.e.,signature.signature).The
rawMessageis thetokenIdplus thestate(where thestatehas been provided by the TPP).
To convert the information contained in the redirect to a usable form, you'll need to parse the appended string and the URL decode included parameters.
After parsing the URL data, you'll need to construct the message you'll be verifying and normalise the JSON.
{"state": {state},"tokenId": {tokenId}} // normalized messageNext, using this message and applying the signature and Token.io's public key, you can verify the response payload. The essential structure in node.js using node-forge library will look like this:
const isValid = forge.pki.ed25519.verify({
message: {normalizedMessage},
encoding: 'binary',
publicKey:{tokenPublicKey},
signature: {signature}
});The validation components are defined in the following table.
| Parameter | Description |
|---|---|
normalized message | Message constructed as above and normalized. |
tokenPublicKey | Token.io's public key using ED25519 algorithm |
signature | A signature generated by Token.io using the ED25519 cryptographic algorithm, based on the response; from URL |
If you want to use forge with node.js, it's available through npm at: https://npmjs.org/package/node-forge.
Sample scripts are included below. However, these are strictly examples. You will need to construct your own solution with the appropriate encoding algorithm and public key.
JavaScript
const forge = require('node-forge')
const stringify = require('fast-json-stable-stringify')
const rawSignature = ""; // raw signature from URL
const rawMessage = { "tokenId": "" }; // token id from URL
const tknPublicKey = ""; // token public key
// actual validation
const isValid = forge.pki.ed25519.verify({
message: stringify(rawMessage),
encoding: 'binary',
publicKey: bufferKey(tknPublicKey),
signature: bufferKey(rawSignature)
})
function bufferKey(key) {
const segmentLength = 4;
const string = key.toString();
const newLength = Math.ceil(string.length / segmentLength) * segmentLength;
const base64Buffer = string.padEnd(newLength, '=')
.replace(/-/g, '+')
.replace(/_/g, '/');
const base64 = Buffer.from(base64Buffer, 'base64');
return newUint8Array(base64);
}Java
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
class Validator {
public byte[] decode(String string) {
return Base64.getDecoder().decode(string.replaceAll("-", "+").replaceAll("_", "/"));
}
public boolean validateTokenSignature() {
final byte[] message = "your message payload".getBytes(StandardCharsets.UTF_8);
final string encodedPublickey = "the public key string";
final string encodedsignature = "the signature";
final AsymmetricKeyParameter keyParam = new Ed25519PublicKeyParameter(decode(encodedPublickey), 0);
final Signer verifier = new Ed25519Signer();
verifier.init(false, keyParam);
verifier.update(message, 0, message.length);
return verifier.verifySignature(decode(encodedsignature));
}
}We're using bouncy castle cryptography in the above code snippet:
https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on
You can obtain the Token.io Public Key from the Dashboard Settings > Member Information.

The following Java code snippet can be used for signature verification:
Java
public void verifyAuthorizationHeader(
String authorization,
Optional<String> body) {
if (authorization == ) {
throw UnauthorizedException("Authorization header is missing");
}
// Split and validate the authorization header
String[] authorizationParts = authorization.split(" ");
if (authorizationParts.length != 2) {
throw UnauthorizedException(String.format(
"The expected header format is \"%s <token>\"",
SCHEME));
}
// Split and validate the JWT parts
String[] parts = authorizationParts[1].split("\\.");
if (parts.length != 3) {
throw UnauthorizedException("Invalid authorization");
}
try {
// Decode and parse the JWT header
JwtHeader jwtHeader = gson.fromJson(
String(decoder.decode(parts[0])),
JwtHeader.class);
String alg = jwtHeader.getAlgorithm().toLowerCase();
if (!algorithmMap.containsKey(alg)) {
throw UnauthorizedException("Unsupported algorithm: " + alg);
}
// Construct the signing input
String signingInput = String.format(
"%s.%s",
parts[0],
encoder.encodeToString(body.orElse("").getBytes()));
// Extract algorithm and member details
Algorithm algorithm = algorithmMap.get(alg);
String memberId = jwtHeader.getMemberId();
String keyId = jwtHeader.getKeyId();
// Fetch member properties and key
MemberProperties member = fetcher.getMemberProperties(memberId);
Key key = getMemberKey(member, keyId);
if (!key.getAlgorithm().equals(algorithm)) {
throw AccessDeniedException("Invalid key algorithm: key=" + keyId);
}
// Perform signature verification
Crypto crypto = CryptoRegistry.getInstance().cryptoFor(key.getAlgorithm());
PublicKey publicKey = crypto.toPublicKey(key.getPublicKey());
crypto.verifier(publicKey).verify(signingInput, parts[2]);
} catch (NullPointerException | JsonSyntaxException e) {
throw UnauthorizedException("Invalid jwt in authorization");
}
}PSS padding is not support for RS256.
If you have any feedback about the developer documentation, please contact devdocs@token.io