curl --request POST \
--url https://api.arcus.xyz/v1/transfer \
--header 'Content-Type: application/json' \
--data '
{
"ethereumAddress": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100000000000",
"nonce": "1730000000000000000",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
}
}
'import requests
url = "https://api.arcus.xyz/v1/transfer"
payload = {
"ethereumAddress": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100000000000",
"nonce": "1730000000000000000",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
}
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
ethereumAddress: '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
fromAccountIndex: 4,
toAccountIndex: 4,
amount: '100000000000',
nonce: '1730000000000000000',
signature: {
r: '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
s: '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
v: '0x1c'
}
})
};
fetch('https://api.arcus.xyz/v1/transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.arcus.xyz/v1/transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'ethereumAddress' => '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
'fromAccountIndex' => 4,
'toAccountIndex' => 4,
'amount' => '100000000000',
'nonce' => '1730000000000000000',
'signature' => [
'r' => '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
's' => '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
'v' => '0x1c'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.arcus.xyz/v1/transfer"
payload := strings.NewReader("{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.arcus.xyz/v1/transfer")
.header("Content-Type", "application/json")
.body("{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.arcus.xyz/v1/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}"
response = http.request(request)
puts response.read_body{
"transferId": "b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"ethereumAddress": "<string>",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100",
"status": "PENDING",
"submittedAt": 123
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "rate limited",
"reason": "account_empty",
"retryAfterMs": 850,
"clientId": "my-order-42"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}Submit internal transfer
Move collateral between two account indexes of the same wallet.
curl --request POST \
--url https://api.arcus.xyz/v1/transfer \
--header 'Content-Type: application/json' \
--data '
{
"ethereumAddress": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100000000000",
"nonce": "1730000000000000000",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
}
}
'import requests
url = "https://api.arcus.xyz/v1/transfer"
payload = {
"ethereumAddress": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100000000000",
"nonce": "1730000000000000000",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
}
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
ethereumAddress: '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
fromAccountIndex: 4,
toAccountIndex: 4,
amount: '100000000000',
nonce: '1730000000000000000',
signature: {
r: '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
s: '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
v: '0x1c'
}
})
};
fetch('https://api.arcus.xyz/v1/transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.arcus.xyz/v1/transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'ethereumAddress' => '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
'fromAccountIndex' => 4,
'toAccountIndex' => 4,
'amount' => '100000000000',
'nonce' => '1730000000000000000',
'signature' => [
'r' => '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
's' => '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
'v' => '0x1c'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.arcus.xyz/v1/transfer"
payload := strings.NewReader("{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.arcus.xyz/v1/transfer")
.header("Content-Type", "application/json")
.body("{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.arcus.xyz/v1/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"ethereumAddress\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"fromAccountIndex\": 4,\n \"toAccountIndex\": 4,\n \"amount\": \"100000000000\",\n \"nonce\": \"1730000000000000000\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n }\n}"
response = http.request(request)
puts response.read_body{
"transferId": "b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"ethereumAddress": "<string>",
"fromAccountIndex": 4,
"toAccountIndex": 4,
"amount": "100",
"status": "PENDING",
"submittedAt": 123
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}{
"error": "rate limited",
"reason": "account_empty",
"retryAfterMs": 850,
"clientId": "my-order-42"
}{
"error": "Invalid request body",
"code": "GEO_RESTRICTED",
"errorSource": "Order",
"errorType": "Tick",
"rejectionReason": "POST_ONLY_WOULD_CROSS"
}X-API-Key / X-Signature headers. In v0 transfers are same-wallet only: both fromAccountIndex and toAccountIndex belong to ethereumAddress, so there is no cross-wallet recipient field. Funds never leave custody, so nothing settles on-chain.
Response behavior
Asynchronous. A202 Accepted means the transfer passed validation and was queued to the matching engine with status: PENDING. The definitive outcome (applied, or rejected for insufficient free collateral) is delivered asynchronously — poll GET /v1/accountTransferUpdates or subscribe to the account transfer update WebSocket channel and correlate on transferId.
Signing the request
Transfers use EIP-712 typed data (eth_signTypedData_v4). Sign this typed data with the wallet that owns ethereumAddress, then split the 65-byte result into {r, s, v} for the signature field. The gateway recovers the signer and rejects mismatches with HTTP 401.
Domain
{
"name": "Arcus Transfer",
"version": "1",
"chainId": <rootchain chain id, e.g. 421614>,
"verifyingContract": "<BridgeVault address for that chain>"
}
Transfer(address ethereumAddress,uint8 fromAccountIndex,uint8 toAccountIndex,uint256 amount,string nonce)
{
"ethereumAddress": "<ethereumAddress>",
"fromAccountIndex": <fromAccountIndex>,
"toAccountIndex": <toAccountIndex>,
"amount": "<amount>",
"nonce": "<nonce>"
}
amount is the integer collateral quote-quantum amount (same value as the amount request field).
Per-environment domain parameters. chainId and verifyingContract are identical to the withdraw domain — see the per-environment table under POST /v1/withdraw. Only the domain name differs ("Arcus Transfer" vs "Arcus Withdraw").
Complete eth_signTypedData_v4 call (ethers v6 / viem)
// ethers v6 — chainId / verifyingContract per the /v1/withdraw table
const domain = {
name: "Arcus Transfer",
version: "1",
chainId, // e.g. 421614 on staging (see /v1/withdraw)
verifyingContract, // BridgeVault for that chain (see /v1/withdraw)
};
const types = {
Transfer: [
{ name: "ethereumAddress", type: "address" },
{ name: "fromAccountIndex", type: "uint8" },
{ name: "toAccountIndex", type: "uint8" },
{ name: "amount", type: "uint256" },
{ name: "nonce", type: "string" }
]
};
const message = {
ethereumAddress: "0xYourAddress",
fromAccountIndex: 0,
toAccountIndex: 1,
amount: BigInt("100000000000"), // quote quantums
nonce: "1730000000000000000"
};
const sig = await signer.signTypedData(domain, types, message);
const r = sig.slice(0, 66);
const s = "0x" + sig.slice(66, 130);
const v = "0x" + sig.slice(130, 132);
// viem
const sig = await walletClient.signTypedData({ domain, types, primaryType: "Transfer", message });
v component must be 0x1b (27) or 0x1c (28); wallets that return 0 / 1 should have 27 added before sending.Body
Master EVM address that owns both the source and destination accounts. In v0 an internal transfer is same-wallet only: the funds move between two account indexes of this one address — there is no cross-wallet recipient field.
^(0x|0X)?[0-9a-fA-F]{40}$"0x742d35cc6634c0532925a3b844bc9e7595f2bd18"
Account index to debit.
0 <= x <= 9Account index to credit. Must differ from fromAccountIndex; a transfer to the same index is rejected with HTTP 400.
0 <= x <= 9Amount to transfer, as a base-10 integer string in quote quantums (1e9 = $1; for example, 1000000000 represents $1). Amounts must be positive and fit in a signed 64-bit integer. Unlike withdrawals, an internal transfer never settles on-chain, so the amount is NOT constrained to collateral-base-unit multiples. Float-style decimals are rejected. The source account must have sufficient free collateral or the transfer is rejected asynchronously with REJECTED_INSUFFICIENT_COLLATERAL.
^[0-9]+$"100000000000"
Replay-protection nonce, single-use per (ethereumAddress, fromAccountIndex, toAccountIndex) — a repeated nonce is rejected with 409. Send the current time as nanoseconds since the Unix epoch (a plain base-10 integer, e.g. 1730000000000000000); the server accepts a timestamp up to 48 hours in the past by default and up to 24 hours in the future by default — both bounds are configurable per environment. A legacy opaque nonce (e.g. a UUID) is still accepted but deprecated. At most 64 characters.
1 - 64"1730000000000000000"
secp256k1 EIP-712 typed-data signature (r, s, v) over the Transfer payload (eth_signTypedData_v4). See the POST /v1/transfer endpoint description for the exact domain, types, and message. Produced by the wallet that owns ethereumAddress; requests whose recovered signer does not match are rejected with HTTP 401.
Show child attributes
Show child attributes
Response
Transfer accepted and queued to the matching engine. The body carries status: PENDING and a transferId; the terminal outcome is delivered on the account transfer update stream.
Server-generated identifier (UUID) correlating this transfer with the matching AccountTransferUpdate on the account transfer update stream.
"b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
20-byte EVM address as hex: optional 0x or 0X prefix and exactly 40 hexadecimal digits. API responses normalize to lowercase a–f after 0x.
^(0x|0X)?[0-9a-fA-F]{40}$Account index (account index, 0–9). Identifies the account for orders, positions, fills, and API keys.
0 <= x <= 9Account index (account index, 0–9). Identifies the account for orders, positions, fills, and API keys.
0 <= x <= 9Transfer amount echoed back as a human-readable decimal in quote currency (quote resolution 1e9), matching how the amount is reported on the account transfer update feed.
"100"
"1.5"
Synchronous status of a submitted transfer. Always PENDING on the 202 response — the transfer has been queued to the matching engine but not yet applied. The terminal outcome (APPLIED, REJECTED_INSUFFICIENT_COLLATERAL, ...) is delivered asynchronously on the account transfer update stream (see AccountTransferUpdate).
PENDING Server receive timestamp (epoch microseconds).
Was this page helpful?