This guide explains how to transfer ERC20 tokens from a Safe wallet to another address using the TRANSFER_TOKEN operation. This is a generic token transfer operation that works with any ERC20 token on supported blockchains.
Before executing a token transfer, you need to prepare the operation. The API will validate the token address, check if it exists and is active on the target chain, and prepare the transaction.
Endpoint : POST /v1.2/safes/operations
Headers :
Authorization: Bearer <access_token> (required)Body (JSON) :
safeAddress (string, required) : Safe address performing the transferchainId (number, optional) : Target chain ID. If omitted, uses the default chainoperations (array, required) : Array containing an operation of type TRANSFER_TOKENTRANSFER_TOKEN Operation :
type (string, required) : "TRANSFER_TOKEN"tokenAddress (string, required) : EVM address (0x format) of the ERC20 token contractto (string, required) : EVM address (0x format) of the recipientamount (string, required) : Amount in human-readable decimal format (e.g., "1.23")Request Example :
POST /v1.2/safes/operations
Content-Type: application/json
Authorization: Bearer <access_token>
{
"safeAddress": "0xd676c6188195372EC269E9C2cAf815C56436A679",
"chainId": 421614,
"operations": [
{
"type": "TRANSFER_TOKEN",
"tokenAddress": "0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430",
"to": "0xDEST000000000000000000000000000000000000",
"amount": "1.23"
}
]
}
Response (200 OK) :
{
"credentialRequestOptions": {
"challenge": "<base64url_encoded_userOpHash>",
"rpId": "foo.domain",
"userVerification": "required",
"allowCredentials": [
{
"id": "<base64url_encoded_credential_id>",
"type": "public-key"
}
],
"timeout": 60000
}
}
credentialRequestOptions : WebAuthn options required for signing the operationThe API validates tokens by querying BCReader:
GET /api/v1.2/config/tokens/address/:address?blockchainId=<chainId>active=truedecimals is used to convert the human-readable amount into minimal units (wei)The API automatically checks the Safe's balance before preparing the operation. If the balance is insufficient, the request will be rejected with a 400 error.
After receiving credentialRequestOptions, you must sign the operation using WebAuthn and submit the credential to finalize the transfer.
credentialRequestOptions, you must:
navigator.credentials.get() with these options to obtain a credentialPUT /v1.2/safes/operations request with the credential to finalize the transferUse WebAuthn to sign the operation:
const credential = await navigator.credentials.get({
publicKey: credentialRequestOptions
});
Endpoint : PUT /v1.2/safes/operations
Body (JSON) :
credential (object, required) : WebAuthn credential obtained with the options from the previous stepRequest Example :
PUT /v1.2/safes/operations
Content-Type: application/json
Authorization: Bearer <access_token>
{
"credential": {
"id": "AVZs0qRCBSmfThZWu37g...",
"rawId": "AVZs0qRCBSmfThZWu37g...",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ...",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoi...",
"signature": "MEUCIQC..."
}
}
}
Response (200 OK) :
{
"userOpHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
userOpHash : User operation hash. Use it to track the operation statusOnce the operation is submitted, you can track its status. Possible statuses are:
CREATED : Operation created, awaiting signatureSIGNED : Operation signed, ready to be executedEXECUTED : Operation executed on the blockchain, awaiting confirmationCONFIRMED : Operation confirmed with sufficient blocksFAILED : Operation failedEndpoint to check status : GET /v1.2/safes/operations/{userOpHash}
Request Example :
GET /v1.2/safes/operations/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef Authorization: Bearer <access_token>
Response (200 OK) :
{
"userOpHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"safeAddress": "0xd676c6188195372EC269E9C2cAf815C56436A679",
"chainId": 421614,
"status": "CONFIRMED",
"transactionHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:35:00.000Z",
"operations": [
{ "index": 0, "type": "TRANSFER_TOKEN" }
]
}
If the token address is not found on the specified chain, you will receive a 404 error:
{
"error": "Token contract not found on chain 421614: from=0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430, to=0xDEST000000000000000000000000000000000000"
}
If the token exists but is inactive, you will receive a 400 error:
{
"error": "Token contract inactive on chain 421614: from=0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430, to=0xDEST000000000000000000000000000000000000 (active=false)"
}
If the Safe does not have sufficient tokens, you will receive a 400 error:
{
"error": "Insufficient balance: wallet 0xd676c6188195372EC269E9C2cAf815C56436A679 has 0.5 tokens, required 1.23"
}
If the recipient address is invalid, you will receive a 400 error:
{
"error": "Invalid recipient address: 0xINVALID"
}
POST /v1.2/safes/operations with TRANSFER_TOKEN operationnavigator.credentials.get() with the credentialRequestOptionsPUT /v1.2/safes/operations with the credentialGET /v1.2/safes/operations/{userOpHash}status = "CONFIRMED", the transfer is completeCONFIRMED status indicates that the transfer is complete and confirmed on the blockchainThis section provides detailed technical information for AI systems integrating the IBEX token transfer flow, including architecture patterns, data models, and implementation details.
POST /v1.2/safes/operations
TRANSFER_TOKENtokenAddress, to, amounttransfer(address to, uint256 amount) callcredentialRequestOptions with challenge = userOpHashSafeOperation record with status CREATEDToken Validation Flow:
GET /api/v1.2/config/tokens/address/:address?blockchainId=<chainId>GET /api/v1.2/config/tokens (searches all tokens)active=true, decimals availableBalance Verification:
GET /v1.1/balances/{safeAddress}?blockchainId=<chainId>PUT /v1.2/safes/operations
SIGNEDCREATED → SIGNED → EXECUTED → CONFIRMEDStatus Flow:
CREATED : Operation created, awaiting passkey signatureSIGNED : Passkey signature received, operation ready for bundlerEXECUTED : Bundler submitted userOp to blockchain, awaiting confirmationCONFIRMED : Sufficient block confirmations received (typically 1-2 blocks)FAILED : Operation failed (insufficient funds, invalid token, etc.)Status Polling:
GET /v1.2/safes/operations/{userOpHash} to check statusCONFIRMED or FAILEDtransactionHash when confirmederror (error details) when failedsrc/routes/v1/safesOperations.ts : TRANSFER_TOKEN operation handlersrc/clients/BCReader.ts : BCReader API client for token validationprisma/schema.prisma : SafeOperation and Operation model definitionsPOST /v1.2/safes/operations with { type: "TRANSFER_TOKEN", tokenAddress, to, amount }PUT /v1.2/safes/operations with credentialGET /v1.2/safes/operations/{userOpHash} until CONFIRMED