> ## Documentation Index
> Fetch the complete documentation index at: https://developers.fireblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Exchanges Travel Rule Overview

> Send Travel Rule PII with deposits to and withdrawals from exchange accounts through the Fireblocks API: build the `piiData` payload, encrypt it, and add it to the transaction.

Some exchanges require Travel Rule information, the originator's or beneficiary's personal data (PII), for deposits to and withdrawals from your exchange account. You send it with the transaction itself, as an encrypted `piiData` object in `extraParameters` of [`POST /transactions`](/api-reference/transactions/create-a-new-transaction).

This doesn't go through a Travel Rule provider or your Travel Rule policy. Fireblocks passes the data directly to the exchange, and the exchange applies its own requirements.

Fireblocks doesn't validate `piiData`. The API accepts any payload, so it's up to you to send the fields the exchange requires for each transaction. Requirements differ by venue, direction, relationship, and, for Binance, jurisdiction. When `piiData` is missing or incomplete, the transaction may fail if the exchange requires Travel Rule data.

You can also provide this data in the Console. See [Exchanges Travel Rule Overview](https://support.fireblocks.io/hc/en-us/articles/30614376019484-Exchanges-Travel-Rule-Overview) on the Help Center.

## Supported venues

Each venue has its own guide, with the fields it requires for every direction and scenario.

| Venue | When PII is required | Requirements |
| - | - | - |
| [Binance](/docs/binance-travel-rule) | Withdrawals and deposits | Vary by jurisdiction |
| [Bitstamp](/docs/bitstamp-travel-rule) | Withdrawals and deposits | Fixed field combinations |
| [Bitfinex](/docs/bitfinex-travel-rule) | Withdrawals only | Fixed fields |
| [OKX](/docs/okx-travel-rule) | Withdrawals | Fixed fields |
| [TrustCo](/docs/trustco-travel-rule) | Withdrawals | Fixed fields |

TrustCo is a custodian, not an exchange; it's covered here because its requirements work the same way. It's unrelated to [TRUST](/docs/interact-with-trust), the Travel Rule network.

## How it works

1. Work out which fields to send. See [Find the fields to send](#find-the-fields-to-send).
2. Build the `piiData` payload.
3. Get the public key for encryption.
4. Encrypt every value in `piiData.data`.
5. Create the transaction with the encrypted `piiData` in `extraParameters`.

Never send unencrypted PII in an API call.

## Find the fields to send

Answer these in order, then look up the matching row in the venue's guide:

1. **Venue:** Binance, Bitstamp, Bitfinex, OKX, or TrustCo.
2. **Direction:** withdrawal from the exchange, or deposit to it.
3. **Jurisdiction (Binance only):** the country of your Binance entity, sent as `vaspCountry`.
4. **Relationship:** is the other party you (`FirstParty`) or someone else (`ThirdParty`)?
5. **Counterparty wallet:** a private (unhosted) wallet, or an account at another VASP?
6. **Entity type:** `Individual` or `Business`.

## The piiData payload

```json theme={"system"}
{
  "type": "exchange-service-travel-rule",
  "typeVersion": "1.0.0",
  "data": {
    "beneficiary": { },
    "originator": { },
    "beneficiaryVASP": { },
    "originatingVASP": { },
    "transactionData": { },
  }
}
```

`type` and `typeVersion` stay in plain text. Everything inside `data` is encrypted.

Which party you describe depends on direction:

| Direction | Party fields | VASP fields |
| - | - | - |
| Withdrawal from the exchange | `beneficiary` | `beneficiaryVASP`, and `originatingVASP` where required |
| Deposit to the exchange | `originator` | `originatingVASP`, and `beneficiaryVASP` where required |

### Party fields

| Field | Description |
| - | - |
| `participantRelationshipType` | `FirstParty` if the party is you; `ThirdParty` if it is someone else. |
| `entityType` | `Individual` when the beneficiary or originator is a person; `Business` when it is a legal entity. |
| `names` | Array of objects. For individuals: `primaryName`, `secondaryName`, and `nameType` (for example `Latin`). For Japanese Kana and Kanji: `primaryName` and `nameType` (`Kana` or `Kanji`). |
| `company.name` | For business entities. |
| `nationalIdentification` | National identification: `nationalIdentifierType` (for example `PASSPORT` or `ABN`) and `nationalIdentifier`. |
| `registrationNumber` | A business's registration number, where required. |
| `dateOfBirth` | For individuals, where required. |
| `postalAddress` | Full address: `country` (ISO 2-letter code), `city`, `streetName`, `buildingNumber`, `postalCode`, `subdivision`. |
| `isHosted` | OKX: whether the destination wallet is hosted. |
| `externalReferenceId` | TrustCo: your reference for the party. |

### VASP fields

| Field | Description |
| - | - |
| `vaspCode` | Identifies the counterparty VASP. The format differs by venue: a code such as `BINANCE`, a UUID for Bitstamp, or a DID for Bitfinex and OKX. |
| `vaspName` | The counterparty VASP's name, where the venue identifies VASPs by name. Used for Binance deposits, and instead of `vaspCode` for TrustCo and for VASPs a venue does not list. |
| `vaspRegion` | The VASP's region, where needed. |

### Transaction fields

| Field | Description |
| - | - |
| `transactionData.withdraw.isAddressVerified`, `transactionData.deposit.isAddressVerified` | Binance: whether the withdrawal or deposit address was verified. |
| `transactionData.withdraw.txPurpose`, `transactionData.deposit.txPurpose` | Binance: the transaction's purpose, in jurisdictions that require it. |

## Encryption

Exchanges require RSA-only encryption. Hybrid encryption (RSA with AES) is rejected.

* **Algorithm:** RSA-OAEP with SHA-256.
* **Scope:** encrypt each value in `data` individually, keeping the object structure. The result is the same shape, with every value replaced by a base64-encoded ciphertext.
* **Key:** encrypt with your workspace's exchange public key. This is a workspace-wide key, not tied to any specific exchange. Get it with [Get public key to encrypt exchange credentials](/api-reference/exchange-accounts/get-public-key-to-encrypt-exchange-credentials).

## Example: encrypt the payload and create the transaction

**1. Initialize the Fireblocks SDK.**

```ts theme={"system"}
import { Fireblocks } from '@fireblocks/ts-sdk';

const fireblocksSDK = new Fireblocks({ apiKey, privateKey, basePath: baseUrl });
```

**2. Get your workspace exchange public key.** The same key encrypts exchange credentials and PII values.

```ts theme={"system"}
async function getExchangePublicKey() {
  try {
    const credentialsResponse = await fireblocksSDK.exchangeAccounts
.getExchangeAccountsCredentialsPublicKey();
    return credentialsResponse.publicKey;
  } catch (error) {
    console.error('Failed to get workspace exchange public key:', error);
    throw error;
  }
}
```

**3. Encrypt the `piiData` values with the public key.**

```ts theme={"system"}
/**
 * Cleans PEM headers, footers, and whitespace, then converts it to an ArrayBuffer.
 */
function pemToArrayBuffer(pem: string): ArrayBuffer {
  const cleanBase64 = pem
    .replace(/-----BEGIN PUBLIC KEY-----/g, '')
    .replace(/-----END PUBLIC KEY-----/g, '')
    .replace(/\s+/g, '');

  const binaryString = atob(cleanBase64);
  return Uint8Array.from(binaryString, (char) => char.charCodeAt(0)).buffer;
}

/**
 * Imports an RSA public key from PEM format for the Web Crypto API.
 */
export async function importRsaPublicKey(pemContents: string): Promise<CryptoKey> {
  const binaryDer = pemToArrayBuffer(pemContents);

  return crypto.subtle.importKey(
    'spki',
    binaryDer,
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    true,
    ['encrypt']
  );
}

/**
 * Encrypts a primitive value using an imported CryptoKey (RSA-OAEP with SHA-256)
 * and returns a Base64 string.
 */
async function encryptPrimitiveData(data: string | number | boolean, cryptoKey: CryptoKey): Promise<string> {
  const encodedData = new TextEncoder().encode(String(data));

  const encryptedBuffer = await crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    cryptoKey,
    encodedData
  );

  return Buffer.from(encryptedBuffer).toString('base64');
}

/**
 * Encrypts each PII field individually using RSA-OAEP encryption.
 * Preserves the original object/array structure.
 *
 * @param piiData - PII data object to encrypt field by field
 * @param publicKey - RSA public key in PEM format or JSON string containing credPublicKey
 * @returns Promise<any> - Object with each primitive field encrypted individually in Base64
 */
export async function encryptPiiFieldsIndividually(piiData: any, publicKey: string): Promise<any> {
  // 1. Extract PEM content if JSON string was provided, then import key once
  const pemContents = publicKey.trim().startsWith('{')
    ? JSON.parse(publicKey).credPublicKey
    : publicKey;

  const cryptoKey = await importRsaPublicKey(pemContents);

  // 2. Recursive traversal helper
  async function encryptPrimitives(obj: any): Promise<any> {
    if (obj === null || obj === undefined) {
      return obj;
    }

    if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
      return await encryptPrimitiveData(obj, cryptoKey);
    }

    if (Array.isArray(obj)) {
      return Promise.all(obj.map((item) => encryptPrimitives(item)));
    }

    if (typeof obj === 'object') {
      const result: any = {};
      for (const [key, value] of Object.entries(obj)) {
        result[key] = await encryptPrimitives(value);
      }
      return result;
    }

    return obj;
  }

  // 3. Execute recursive encryption
  return encryptPrimitives(piiData);
}
```

**4. Create the transaction, with `piiData` in `extraParameters`.**

```ts theme={"system"}
async function createExchangeTransaction(fireblocksSDK: Fireblocks, piiData: any) {
  // 2. Get workspace exchange public key from Fireblocks
  const publicKey = await getExchangePublicKey();

  // 3. RSA-only encrypt each PII field individually
  const encryptedPiiData = await encryptPiiFieldsIndividually(piiData, publicKey);

  // 4. Build the Fireblocks transaction payload
  const transactionRequest = {
    operation: "TRANSFER",
    source: { type: "VAULT_ACCOUNT", id: "1" },
    destination: { type: "EXCHANGE_ACCOUNT", id: "exchange_account_id" },
    amount: "100",
    assetId: "BTC",
    extraParameters: {
      piiData: encryptedPiiData
    }
  };

  // 5. Send the transaction
  const result = await fireblocksSDK.transactions.createTransaction({           
    transactionRequest: transactionRequest 
  });
  console.log('Exchange transaction created:', result.id);
  return result;
}

createExchangeTransaction(fireblocksSDK, <PII DATA OBJECT>)
  .then(() => console.log("PII transaction sent successfully to exchange."))
  .catch(console.error);
```

For a complete plaintext `piiData` object for a specific scenario, see the venue's guide.

## Before you go live

* Map your customer data to the `piiData` fields each venue requires.
* Decide the required fields per transaction, following [Find the fields to send](#find-the-fields-to-send).
* Check that the plaintext PII matches the KYC records you hold.
* Test in a sandbox workspace.

## On the Help Center

To provide Travel Rule data for exchange transactions in the Console, see [Exchanges Travel Rule Overview](https://support.fireblocks.io/hc/en-us/articles/30614376019484-Exchanges-Travel-Rule-Overview).
