> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://infonite.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://infonite.dev/_mcp/server.

# Exchange a ticket

PUT https://clients.infonite.tech/api/executions/t10n
Content-Type: application/json

Turn the ticket an execution issued into a reusable token.

**Why this exists.** Running an engine every month should not mean keeping somebody's bank password in your database. Tokenization moves that problem: you run the credentials once, we keep them encrypted, and you keep a token that only works for that engine, that customer and that set of features — through your application and no other.

**Neither half opens the credentials alone.** The key you receive here is not the encryption key: it is one input to a derivation that also needs a secret living only in our runtime, and the result — an AES-256-GCM key — exists only for the moment it is used. We store the ciphertext and a non-reversible verifier of your key, never the key itself. The full design is in the [Credential Tokenization guide](/guides/credential-tokenization).

**How you get here.** Start an execution with `base_configurations.tokenized_access: true`. If your application is allowed to tokenize, the `202` carries a `ticket`. That ticket is single-use and short-lived: it exists to be exchanged, here, for the real token.

**What you get back.** A `token_id` and a `token_key`, plus the context the token is locked to: engine, customer, the `locking_hash` of the credentials, and the features it may request. **Store both halves and treat the key like a password** — it is the only copy, nothing on our side holds it, and it is never shown again. Lose it and the stored credentials can no longer be opened by anyone: revoke the token and mint a new one from a fresh run with the real credentials.

**The token expires 90 days after this exchange, and using it does not extend that.** The expiry is fixed when the token is minted; when it passes, the stored credentials are deleted. Track `date_expiration` and mint a fresh token — from a new parametrized execution — before it lapses. Send it to [Start a Tokenized Execution](api:POST/executions/init/v1/tokenized).

A `403` covers every way this can fail — no such ticket, wrong ticket, already exchanged, tokenization not enabled. That is deliberate: an attacker guessing tickets learns nothing from the answer.

> This endpoint needs to be executed with an app secret, so should always be used in server side without exposing the secret to customers.


Reference: https://infonite.dev/api-reference/direct-executions/direct-executions-api/tokenization/direct-executions-v-1-token-exchange

## Authentication

- `X-APP-SECRET` header (required) — Application Secret

## Servers

- `https://clients.infonite.tech/api` (Legacy Server, default)
- `https://clients.infonite.io/api` (Production Server)

## Request

### Body (application/json)

This endpoint expects an object.

- `execution_id` (string, required) — The execution whose `ticket` you are exchanging — the one you asked tokenized access for.
- `ticket` (string, required) — The `ticket` that execution returned. Single-use: this call spends it.

## Response

### 200

**200 OK**The token, and the **only copy of your half of the key**. Store both: the key is not recoverable and we do not keep it — without it the credentials cannot be opened, by us or by anyone. The ticket is spent by this call. Asking again with the same one answers `403`.

- `token` (object, required) — Credentials token containing the token ID and key.
  - `token_id` (string, required) — Identifies the stored credentials. Safe to log and to keep beside your customer record — on its own it opens nothing.
  - `token_key` (string, required) — The secret half, and the only copy of it. Store it encrypted, keep it out of your logs, and treat losing it as losing the credentials: nobody — including us — can open them without it.
- `context` (object, required) — Credentials context.
  - `engine_reference` (string, required) — The engine this execution runs, exactly as the catalogue publishes it. It is echoed on every event and every record, so a stored result says which source it came from with no lookup on your side.
  - `app_id` (string, required) — The application this execution was launched with — the one your secret belongs to. Worth keeping when your product uses more than one, a sandbox and a production app being the usual case: every record and every event we send carries it.
  - `customer_id` (string, required) — The `customer_id` you supplied when the execution was initialised, returned as you sent it — so an answer can be routed to the right case with no lookup on your side.
  - `features` (list of enum, required) — The features this token was minted for. **Fixed**: an execution using it may ask for fewer, never for one that is not on this list — a token cannot grow into new data about somebody.
    - Allowed values: `accounts_read`, `cards_read`, `deposits_read`, `loans_read`, `credits_read`, `investment_accounts_read`, `funds_read`, `stocks_read`, `pensions_read`, `accounts_certificates`, `direct_debits_read`, `customer_information_read`, `source_contracts_read`, `cloud_resource_read`, `supplier_invoices_read`, `client_invoices_read`, `labor_check`, `public_pensions`, `public_document_verification`, `yearly_individual_tax`, `vehicles_data`, `driver_data`, `academic_data`, `properties_data`, `credit_registry_data`
  - `locking_hash` (string, optional) — A deterministic fingerprint of the fixed part of the credentials — the username, typically. The same access always produces the same value, so a returning set of credentials is recognisable without storing any of it. It appears once a login has been attempted, and it is versioned (`v1.…`) so the derivation can change without the old values becoming ambiguous.

## Errors

### 403 Forbidden Error

**403 Forbidden****The same answer for every reason**, on purpose: no such ticket, a ticket that was already exchanged, one that expired, one belonging to another application — and an application without the tokenization capability. A ticket is a secret, and an error that told them apart would be a way to probe it.

- `detail` (string, required) — Error message

### 422 Unprocessable Entity Error

Validation Error

- `detail` (list of object, optional)
  - `loc` (list of string or integer, required)
  - `msg` (string, required)
  - `type` (string, required)
  - `input` (any, optional)
  - `ctx` (object, optional)

## Examples

**Request**

```json
{
  "execution_id": "6aa3d8b418d1c5dc9a8e3d36",
  "ticket": "ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM"
}
```

**Response**

```json
{
  "token": {
    "token_id": "aaa48f1586c7ed54a13f559d",
    "token_key": "l5uVMTAaHf4oATDUzvahDjMAb7IRPPm2wHFGDSm3vmCseqZaujybW0rayVntpMy8"
  },
  "context": {
    "engine_reference": "DEMOBANKXXXXFIN100ES9999-mobile",
    "app_id": "4aa3dcbab3287e2385bb5cec",
    "customer_id": "my-customer-1",
    "features": [
      "accounts_read",
      "cards_read",
      "deposits_read",
      "loans_read",
      "investment_accounts_read",
      "stocks_read",
      "funds_read",
      "pensions_read",
      "customer_information_read"
    ],
    "locking_hash": "v1.36GHPwfDK-UUZGG4fb1jtg"
  }
}
```

**SDK Code**

```python The ticket is spent and the token is yours
import requests

url = "https://clients.infonite.tech/api/executions/t10n"

payload = {
    "execution_id": "6aa3d8b418d1c5dc9a8e3d36",
    "ticket": "ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM"
}
headers = {
    "X-APP-SECRET": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.put(url, json=payload, headers=headers)

print(response.json())
```

```javascript The ticket is spent and the token is yours
const url = 'https://clients.infonite.tech/api/executions/t10n';
const options = {
  method: 'PUT',
  headers: {'X-APP-SECRET': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"execution_id":"6aa3d8b418d1c5dc9a8e3d36","ticket":"ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go The ticket is spent and the token is yours
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://clients.infonite.tech/api/executions/t10n"

	payload := strings.NewReader("{\n  \"execution_id\": \"6aa3d8b418d1c5dc9a8e3d36\",\n  \"ticket\": \"ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM\"\n}")

	req, _ := http.NewRequest("PUT", url, payload)

	req.Header.Add("X-APP-SECRET", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby The ticket is spent and the token is yours
require 'uri'
require 'net/http'

url = URI("https://clients.infonite.tech/api/executions/t10n")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Put.new(url)
request["X-APP-SECRET"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"execution_id\": \"6aa3d8b418d1c5dc9a8e3d36\",\n  \"ticket\": \"ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM\"\n}"

response = http.request(request)
puts response.read_body
```

```java The ticket is spent and the token is yours
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.put("https://clients.infonite.tech/api/executions/t10n")
  .header("X-APP-SECRET", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"execution_id\": \"6aa3d8b418d1c5dc9a8e3d36\",\n  \"ticket\": \"ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM\"\n}")
  .asString();
```

```php The ticket is spent and the token is yours
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('PUT', 'https://clients.infonite.tech/api/executions/t10n', [
  'body' => '{
  "execution_id": "6aa3d8b418d1c5dc9a8e3d36",
  "ticket": "ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-APP-SECRET' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp The ticket is spent and the token is yours
using RestSharp;

var client = new RestClient("https://clients.infonite.tech/api/executions/t10n");
var request = new RestRequest(Method.PUT);
request.AddHeader("X-APP-SECRET", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"execution_id\": \"6aa3d8b418d1c5dc9a8e3d36\",\n  \"ticket\": \"ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift The ticket is spent and the token is yours
import Foundation

let headers = [
  "X-APP-SECRET": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "execution_id": "6aa3d8b418d1c5dc9a8e3d36",
  "ticket": "ibAeHHRvLpc2tKVM1FbXXOkups1zADyRhcsLc5gb3EycOM61j1QzLTD5cWGJ8EDM"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://clients.infonite.tech/api/executions/t10n")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "PUT"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```