SX Users

Transfers

Move funds between wallets of the same customer: create a transfer, confirm it with a verification code, resend the code, cancel it, and list or fetch transfers.

Move funds from one wallet to another wallet of the same customer in two steps: create the transfer to put it in PENDING, then confirm it with the verification code your backend receives by webhook. The ledger moves the funds the moment you confirm, so a transfer settles at once. Every call returns the same transfer object.

Authentication: every endpoint on this page accepts your Owner API Key (x-api-key) or a user token (Authorization: Bearer). A user token only reaches its own user's wallets; any other wallet or transfer answers 404, so ids can't be probed.

Authorization: Bearer USER_TOKEN

Conventions

Responses use the standard envelope and error format. Amounts are decimal strings and dates are UTC without milliseconds. Send an x-idempotency-key on every POST so retries never create a second transfer.


The transfer object

Create, confirm, resend, cancel, get and list all return this object.

FieldTypeDescription
idstring (uuid)Transfer id. Use it in every path on this page.
objectstringAlways transfer.
statusstringPENDING, COMPLETED, FAILED, CANCELLED, EXPIRED or BLOCKED. See Status lifecycle.
status_reasonobject | null{ code, message } for FAILED, CANCELLED, EXPIRED and BLOCKED. null otherwise.
typestringPAYIN or PAYOUT, as sent.
purposestringPurpose, as sent.
descriptionstringDescription, as sent.
user_idstring (uuid)Owner of the source wallet.
customer_idstring | nullYour platform customer id.
wallet_idstring (uuid)Source wallet.
destination_wallet_idstring (uuid)Destination wallet.
currencystringAsset code in uppercase, e.g. USDC.
amountstringAmount at the asset's scale, e.g. "25000.000000".
feeobject | nullAlways null: an internal transfer has no network fee.
networknullAlways null: no on-chain leg.
tx_hashnullAlways null: no on-chain leg.
idempotency_keystring | nullThe x-idempotency-key you sent on create.
created_atstringCreation time (UTC).
updated_atstringLast change (UTC).
completed_atstring | nullSettlement time (UTC). null until COMPLETED.
created_bystring | nullCredential or user token that created it.
updated_bystring | nullCredential or user token that last changed it.
verificationobjectOnly on create and resend: { expires_at, attempts_remaining }. The code itself is never in the response.

Status lifecycle

StatusMeaningstatus_reason.code
PENDINGCreated. A verification code went out and waits for confirmation.—
COMPLETEDConfirmed. The funds moved between the wallets.—
FAILEDThe ledger couldn't move the funds; nothing was debited.INSUFFICIENT_BALANCE or LEDGER_ERROR
BLOCKEDThree wrong codes. Only cancel or expiry follow.VERIFICATION_BLOCKED
EXPIREDThe code lapsed before confirmation. It reads EXPIRED as soon as expires_at passes.VERIFICATION_CODE_EXPIRED
CANCELLEDCancelled before confirmation.CANCELLED_BY_USER

A transfer never shows PROCESSING: confirm moves the funds synchronously. Any change outside this diagram answers 422 INVALID_STATE_TRANSITION.

Verification code delivery

The verification code is a 6-digit number, valid for 5 minutes, with 3 attempts. It's never returned by the API: Sovera delivers it to your backend in the user.transfer.verification_code_issued webhook event, on create and on resend. Configure your endpoint in Customer webhooks.

Event fieldTypeDescription
codestringThe 6-digit verification code.
transfer_idstring (uuid)Transfer id.
user_idstring (uuid)Owner of the source wallet.
expires_atstringWhen the code stops working (UTC).
triggerstringissued on create, resent on resend.
attempts_remainingintegerAttempts left with this code (3).

Create Transfer

POSThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers

Create a PENDING transfer from wallet_id and issue a verification code. Both wallets must be active, belong to the same customer and be different, and the source must hold enough available balance. With a user token the source must be one of that user's wallets; the destination can be any active wallet of the same customer, including another user's. Nothing moves until you confirm.

Path Parameters

ParameterTypeRequiredDescription
wallet_idstring (uuid)YesSource wallet.

Request Body

FieldTypeRequiredDescription
destination_wallet_idstring (uuid)YesWallet that receives the funds.
typestringYesPAYIN or PAYOUT.
purposestringYesReason for the transfer (max 255).
descriptionstringYesHuman-readable description (max 500).
currencystringYesAsset code, e.g. USDC (max 10).
amountstringYesPositive decimal string, e.g. "25000.000000". A JSON number still works but is deprecated.
{
  "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
  "type": "PAYOUT",
  "purpose": "Payment goods",
  "description": "Transfer to wallet 1",
  "currency": "USDC",
  "amount": "25000.000000"
}

Success Response (201 Created):

{
  "success": true,
  "data": {
    "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
    "object": "transfer",
    "status": "PENDING",
    "status_reason": null,
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "currency": "USDC",
    "amount": "25000.000000",
    "fee": null,
    "network": null,
    "tx_hash": null,
    "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:03:11Z",
    "completed_at": null,
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "verification": {
      "expires_at": "2026-10-12T14:08:11Z",
      "attempts_remaining": 3
    }
  },
  "meta": {
    "timestamp": "2026-10-12T14:03:11Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer USER_TOKEN" \
  -H "x-idempotency-key: 6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81" \
  -d '{
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "currency": "USDC",
    "amount": "25000.000000"
  }'

Errors: 400 INSUFFICIENT_BALANCE · 400 SAME_WALLET_TRANSFER source and destination are the same · 400 CROSS_CUSTOMER_TRANSFER wallets of different customers · 401 UNAUTHORIZED · 404 WALLET_NOT_FOUND source or destination missing, inactive or not yours · 409 IDEMPOTENCY_IN_PROGRESS · 422 VALIDATION_ERROR missing or invalid field (source.pointer names it) · 422 IDEMPOTENCY_KEY_REUSED · 429 RATE_LIMIT_EXCEEDED.


Confirm Transfer

POSThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers/:transfer_id/confirm

Confirm a PENDING transfer with its verification code. On a valid code the funds move at once and the transfer comes back COMPLETED. If the ledger can't move the funds, the transfer turns FAILED and the call answers an error (400 INSUFFICIENT_BALANCE, or 500 INTERNAL_SERVER_ERROR with status_reason LEDGER_ERROR).

Path Parameters

ParameterTypeRequiredDescription
wallet_idstring (uuid)YesSource wallet.
transfer_idstring (uuid)YesTransfer id. Ids with the old txn- prefix are accepted too.

Request Body

FieldTypeRequiredDescription
codestringYesThe 6-digit code from user.transfer.verification_code_issued.
otpstringNoDeprecated alias of code. Send one or the other, never both.
{
  "code": "602391"
}

Success Response (200 OK):

{
  "success": true,
  "data": {
    "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
    "object": "transfer",
    "status": "COMPLETED",
    "status_reason": null,
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "currency": "USDC",
    "amount": "25000.000000",
    "fee": null,
    "network": null,
    "tx_hash": null,
    "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:04:30Z",
    "completed_at": "2026-10-12T14:04:30Z",
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3"
  },
  "meta": {
    "timestamp": "2026-10-12T14:04:30Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}/confirm" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer USER_TOKEN" \
  -H "x-idempotency-key: 2d7b9f1e-4c6a-4b8d-a3e5-7f1c9b2d4e60" \
  -d '{
    "code": "602391"
  }'

Errors: 400 INVALID_VERIFICATION_CODE wrong code (the message says how many attempts remain) · 400 VERIFICATION_CODE_EXPIRED · 400 INSUFFICIENT_BALANCE the balance dropped before confirm; the transfer is now FAILED · 401 UNAUTHORIZED · 403 VERIFICATION_BLOCKED too many wrong codes · 403 ACCOUNT_FROZEN transfers are disabled while the account is frozen; the transfer stays PENDING (the code is checked first, and a ledger.transfer.failed event with account_frozen is sent) · 404 WALLET_NOT_FOUND · 404 TRANSFER_NOT_FOUND · 422 VALIDATION_ERROR no code, or both code and otp · 422 INVALID_STATE_TRANSITION the transfer isn't PENDING · 429 RATE_LIMIT_EXCEEDED.


Resend Verification Code

POSThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers/:transfer_id/verification-codes

Issue a new verification code for a PENDING transfer. The previous code stops working, the attempts reset to 3 and a new user.transfer.verification_code_issued event goes out with trigger: "resent". Send no body. A BLOCKED, EXPIRED or final transfer can't get a new code.

POST https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}/verification-codes

Success Response (200 OK):

{
  "success": true,
  "data": {
    "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
    "object": "transfer",
    "status": "PENDING",
    "status_reason": null,
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "currency": "USDC",
    "amount": "25000.000000",
    "fee": null,
    "network": null,
    "tx_hash": null,
    "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:06:40Z",
    "completed_at": null,
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "verification": {
      "expires_at": "2026-10-12T14:11:40Z",
      "attempts_remaining": 3
    }
  },
  "meta": {
    "timestamp": "2026-10-12T14:06:40Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}/verification-codes" \
  -H "Authorization: Bearer USER_TOKEN"

Errors: 401 UNAUTHORIZED · 404 WALLET_NOT_FOUND · 404 TRANSFER_NOT_FOUND · 422 INVALID_STATE_TRANSITION the transfer isn't PENDING · 429 RATE_LIMIT_EXCEEDED.

Deprecated

POST …/transfers/:transfer_id/verification/codes still works with the same behavior, and a type field in the body is tolerated and ignored. Both are removed in the next release: move to …/verification-codes with no body.


Cancel Transfer

POSThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers/:transfer_id/cancel

Cancel a PENDING, BLOCKED or EXPIRED transfer. Nothing has moved for any of them. The transfer turns CANCELLED with status_reason.code CANCELLED_BY_USER. Send no body.

POST https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}/cancel

Success Response (200 OK):

{
  "success": true,
  "data": {
    "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
    "object": "transfer",
    "status": "CANCELLED",
    "status_reason": {
      "code": "CANCELLED_BY_USER",
      "message": "The transfer was cancelled before confirmation."
    },
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "currency": "USDC",
    "amount": "25000.000000",
    "fee": null,
    "network": null,
    "tx_hash": null,
    "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:05:27Z",
    "completed_at": null,
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3"
  },
  "meta": {
    "timestamp": "2026-10-12T14:05:27Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}/cancel" \
  -H "Authorization: Bearer USER_TOKEN"

Errors: 401 UNAUTHORIZED · 404 WALLET_NOT_FOUND · 404 TRANSFER_NOT_FOUND · 422 INVALID_STATE_TRANSITION the transfer is COMPLETED, FAILED or already CANCELLED, or a confirm is in progress · 429 RATE_LIMIT_EXCEEDED.

Deprecated

DELETE …/transfers/:transfer_id still cancels and answers 204 No Content with no body. It's removed in the next release: move to POST …/cancel.


Get Transfer

GEThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers/:transfer_id

Return one transfer when wallet_id is its source or its destination, so both sides can read it.

GET https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}

Success Response (200 OK):

{
  "success": true,
  "data": {
    "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
    "object": "transfer",
    "status": "COMPLETED",
    "status_reason": null,
    "type": "PAYOUT",
    "purpose": "Payment goods",
    "description": "Transfer to wallet 1",
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
    "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
    "currency": "USDC",
    "amount": "25000.000000",
    "fee": null,
    "network": null,
    "tx_hash": null,
    "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:04:30Z",
    "completed_at": "2026-10-12T14:04:30Z",
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3"
  },
  "meta": {
    "timestamp": "2026-10-12T14:30:02Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
curl -X GET "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers/{{transferID}}" \
  -H "Authorization: Bearer USER_TOKEN"

Errors: 401 UNAUTHORIZED · 404 WALLET_NOT_FOUND · 404 TRANSFER_NOT_FOUND · 429 RATE_LIMIT_EXCEEDED.


List Transfers

GEThttps://api.sandbox.sovera.io/sovx/v1/wallets/:wallet_id/transfers

List a wallet's transfers, newest first, with cursor pagination. By default you get the transfers the wallet sent; pass direction INCOMING for the ones it received. An empty result is 200 with data: [].

Query Parameters

ParameterTypeRequiredDescription
filter[limit]integerNoPage size, 1 to 100 (default 25).
cursorstringNoOpaque token from meta.pagination.navigation. Echo it back unchanged.
filter[order]stringNocreated_at or updated_at, plus ASC or DESC. Default created_at DESC; a field without a direction sorts ASC.
filter[where][direction][eq]stringNoOUTGOING (default: wallet_id is the source) or INCOMING (wallet_id is the destination).
filter[where][status][eq|inq]stringNoAny transfer status.
filter[where][currency][eq|inq]stringNoAsset code.
filter[where][tx_hash][eq]stringNoAccepted for parity with other lists; internal transfers have no hash.
filter[where][idempotency_key][eq]stringNoKey sent on create. Finds a transfer after a timeout.
filter[where][created_at][gte|lte|between]stringNoUTC date-time ending in Z.

Up to 6 conditions per request. Any other field, operator or filter[skip] answers 422 with source.parameter.

GET https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers?filter[limit]=25&filter[where][direction][eq]=OUTGOING

Success Response (200 OK):

{
  "success": true,
  "data": [
    {
      "id": "7c9e6f2a-1b3d-4e8f-9a2c-5d6e7f8a9b0c",
      "object": "transfer",
      "status": "COMPLETED",
      "status_reason": null,
      "type": "PAYOUT",
      "purpose": "Payment goods",
      "description": "Transfer to wallet 1",
      "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
      "customer_id": "corp-redundant-firewall",
      "wallet_id": "23c2bada-5134-5b83-848e-4bae9c3d4a8f",
      "destination_wallet_id": "fbe38fa1-7794-565d-953f-73bbb8c51979",
      "currency": "USDC",
      "amount": "25000.000000",
      "fee": null,
      "network": null,
      "tx_hash": null,
      "idempotency_key": "6f1c2a9e-8b4d-4e3a-b7c5-0d9e2f4a6b81",
      "created_at": "2026-10-12T14:03:11Z",
      "updated_at": "2026-10-12T14:04:30Z",
      "completed_at": "2026-10-12T14:04:30Z",
      "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
      "updated_by": "12015942-4d29-5733-a774-14ee16e31da3"
    }
  ],
  "meta": {
    "timestamp": "2026-10-12T14:30:02Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
    "pagination": {
      "records": { "limit": 25, "has_next": true, "has_previous": false },
      "navigation": {
        "next": "/v1/wallets/23c2bada-5134-5b83-848e-4bae9c3d4a8f/transfers?filter[limit]=25&filter[where][direction][eq]=OUTGOING&cursor=UvbIo6YDi8JMM-JsfQXbOHFs",
        "previous": null
      }
    }
  }
}
curl -G "https://api.sandbox.sovera.io/sovx/v1/wallets/{{walletID}}/transfers" \
  -H "Authorization: Bearer USER_TOKEN" \
  --data-urlencode "filter[limit]=25" \
  --data-urlencode "filter[where][direction][eq]=OUTGOING"

Errors: 401 UNAUTHORIZED · 404 WALLET_NOT_FOUND · 422 VALIDATION_ERROR unsupported filter, order, limit or cursor (source.parameter names it) · 429 RATE_LIMIT_EXCEEDED.

On this page