Skip to main content
POST
Submit internal transfer
The response body shown here is a static example, not live data. After you click Send, your live result appears in a separate panel headed 200 OK. The panel under the status-code tabs is a fixed sample from the spec — its field values (fees, prices, sizes, IDs, timestamps) are placeholders. Use Send, or call the endpoint, for current values.
Move collateral between two account indexes of the same wallet. 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. Two mutually exclusive authentication modes are supported, selected by the presence of the X-API-Key header:
  • Wallet-signed (default): the request is self-authenticated by a secp256k1 EIP-712 typed-data signature in the body’s signature field — no X-API-Key / X-Signature headers.
  • API-key-signed: the request carries the X-API-Key / X-Timestamp / X-Signature header triple with an Ed25519 signature over the TransferV1 typed canonical payload (see below); the body signature field is then omitted. The key must be bound to ethereumAddress, and a key scoped to a single subaccount may set only that subaccount as fromAccountIndex (a key scoped to all subaccounts of its wallet may use any). No special permission is required — moving funds between a wallet’s own subaccounts carries the same authority as trading, which every valid key already has for its scope. The destination is another subaccount of the same wallet, so no separate recipient authorization applies.

Response behavior

Asynchronous. A 202 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 (wallet mode)

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
Types
Message
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)
The v component must be 0x1b (27) or 0x1c (28); wallets that return 0 / 1 should have 27 added before sending.

Signing the request (API-key mode)

The X-Signature header carries a lowercase-hex Ed25519 signature over the TransferV1 typed canonical payload — the same compact key-sorted-JSON convention as placeOrder. The payload is built from parsed request values (never raw body bytes):
Keys are strictly alphabetical (ad, af, at, ct, n, op, q, v) with no whitespace. ct must equal the X-Timestamp header value (nanosecond epoch), binding the signature to the drift window; op is fixed at 6 (transfer — a signature for any other operation can never authorize a transfer, and a transfer signature can never authorize any other operation); both af and at are signed, so the destination cannot be swapped after signing; q is the integer quote-quantum amount (same value as the amount field, unquoted); n is the body nonce signed verbatim. Example (payload → signature):
Replay protection is two-layered on this mode: the X-Timestamp drift window bounds how long a captured signature stays valid, and the nonce is single-use per (ethereumAddress, fromAccountIndex, toAccountIndex) exactly as on the wallet-signed mode (409 on reuse).

Authorizations

X-API-Key
string
header
required

Hex-encoded Ed25519 public key (64 chars). The public key IS the API key — register it via POST /createApiKey. Required on every authenticated request, both read-only and signed.

X-Timestamp
string
header
required

Unix time in nanoseconds as a decimal string (e.g. "1713825891591000000"). Millisecond or second epochs are rejected with 401 Unauthorized. Must be within ±30,000 ms of server wall-clock, or the request is rejected with 401 Unauthorized. Required on all mutating / credential-creating endpoints. This same value must appear as the ct field in the ordersign typed canonical payload (single-order endpoints) or in each element's ct field (batch endpoints).

X-Signature
string
header
required

Lowercase hex-encoded Ed25519 signature (128 chars).

Single-order endpoints (placeOrder, cancelOrder, modifyOrder, and other non-batch mutating routes) sign over the ordersign typed canonical payload — a compact, key-sorted JSON object built from parsed request fields using engine-native integer values:

ct must equal the X-Timestamp header value. Keys in brackets are conditional (omitted when empty). op values: 1=place, 2=cancel, 3=modify. See the ordersign package for field definitions and reference signing code.

Other signed routes (e.g. createApiKey) still use the legacy scheme: signing_message = X-Timestamp + ACTION + canonicalJSON(body), where ACTION is the camelCase final path segment.

Batch endpoints (batchPlaceOrders, batchCancelOrders, batchModifyOrders) do NOT use this header. They authenticate with per-element typed ordersign signatures embedded in the request body (see the global auth description and the per-field signature descriptions on OrderRequest / CancelOrderRequest / ModifyOrderRequest).

Read endpoints are authenticated by ?address= (and optionally X-API-Key) only — no signature is required, except for the signed affiliate reads (see the Referral tag), which require the full header triple; with no body their signing message is X-Timestamp + ACTION. canonicalJSON(body) is the JSON body with object keys sorted lexicographically at every level and no whitespace; the server canonicalizes the received body before verifying, so only the bytes signed over must be canonical. Required on all mutating / credential-creating endpoints.

Body

application/json
ethereumAddress
string
required

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.

Pattern: ^(0x|0X)?[0-9a-fA-F]{40}$
Example:

"0x742d35cc6634c0532925a3b844bc9e7595f2bd18"

fromAccountIndex
integer
required

Account index to debit.

Required range: 0 <= x <= 9
toAccountIndex
integer
required

Account index to credit. Must differ from fromAccountIndex; a transfer to the same index is rejected with HTTP 400.

Required range: 0 <= x <= 9
amount
string
required

Amount 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.

Pattern: ^[0-9]+$
Example:

"100000000000"

nonce
string
required

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.

Required string length: 1 - 64
Example:

"1730000000000000000"

signature
object

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. Required on the default wallet-signed mode; omit it when authenticating with the X-API-Key / X-Timestamp / X-Signature header triple (the header signature is the proof on that mode).

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.

transferId
string
required

Server-generated identifier (UUID) correlating this transfer with the matching AccountTransferUpdate on the account transfer update stream.

Example:

"b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"

ethereumAddress
string
required

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.

Pattern: ^(0x|0X)?[0-9a-fA-F]{40}$
fromAccountIndex
integer
required

Account index (account index, 0–9). Identifies the account for orders, positions, fills, and API keys.

Required range: 0 <= x <= 9
toAccountIndex
integer
required

Account index (account index, 0–9). Identifies the account for orders, positions, fills, and API keys.

Required range: 0 <= x <= 9
amount
string
required

Transfer 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.

Examples:

"100"

"1.5"

status
enum<string>
required

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).

Available options:
PENDING
submittedAt
integer<int64>
required

Server receive timestamp (epoch microseconds).