curl --request POST \
--url https://api.arcus.xyz/v1/createApiKey \
--header 'Content-Type: application/json' \
--data '
{
"address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"publicKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"apiWalletName": "Arcus",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
},
"validUntil": 2,
"nonce": "a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901"
}
'import requests
url = "https://api.arcus.xyz/v1/createApiKey"
payload = {
"address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"publicKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"apiWalletName": "Arcus",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
},
"validUntil": 2,
"nonce": "a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901"
}
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({
address: '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
publicKey: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
apiWalletName: 'Arcus',
signature: {
r: '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
s: '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
v: '0x1c'
},
validUntil: 2,
nonce: 'a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901'
})
};
fetch('https://api.arcus.xyz/v1/createApiKey', 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/createApiKey",
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([
'address' => '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
'publicKey' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
'apiWalletName' => 'Arcus',
'signature' => [
'r' => '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
's' => '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
'v' => '0x1c'
],
'validUntil' => 2,
'nonce' => 'a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901'
]),
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/createApiKey"
payload := strings.NewReader("{\n \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\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/createApiKey")
.header("Content-Type", "application/json")
.body("{\n \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.arcus.xyz/v1/createApiKey")
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 \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\n}"
response = http.request(request)
puts response.read_body{
"apiKey": "<string>",
"address": "<string>",
"createdAt": 123,
"allSubaccounts": true,
"accountIndex": 4,
"validUntil": 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": "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"
}Create API key
Create an API key for the ethereum address.
curl --request POST \
--url https://api.arcus.xyz/v1/createApiKey \
--header 'Content-Type: application/json' \
--data '
{
"address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"publicKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"apiWalletName": "Arcus",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
},
"validUntil": 2,
"nonce": "a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901"
}
'import requests
url = "https://api.arcus.xyz/v1/createApiKey"
payload = {
"address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
"publicKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"apiWalletName": "Arcus",
"signature": {
"r": "0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3",
"s": "0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf",
"v": "0x1c"
},
"validUntil": 2,
"nonce": "a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901"
}
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({
address: '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
publicKey: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
apiWalletName: 'Arcus',
signature: {
r: '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
s: '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
v: '0x1c'
},
validUntil: 2,
nonce: 'a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901'
})
};
fetch('https://api.arcus.xyz/v1/createApiKey', 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/createApiKey",
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([
'address' => '0x742d35cc6634c0532925a3b844bc9e7595f2bd18',
'publicKey' => 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
'apiWalletName' => 'Arcus',
'signature' => [
'r' => '0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3',
's' => '0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf',
'v' => '0x1c'
],
'validUntil' => 2,
'nonce' => 'a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901'
]),
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/createApiKey"
payload := strings.NewReader("{\n \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\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/createApiKey")
.header("Content-Type", "application/json")
.body("{\n \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.arcus.xyz/v1/createApiKey")
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 \"address\": \"0x742d35cc6634c0532925a3b844bc9e7595f2bd18\",\n \"publicKey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n \"apiWalletName\": \"Arcus\",\n \"signature\": {\n \"r\": \"0x69e7308419769592ec8363d002e3077e803e403b4e1920fc30e485b63d76e2a3\",\n \"s\": \"0x0980d0742c9e65037af9d54e559704ae2c2285f3112a9d89544191a740f103cf\",\n \"v\": \"0x1c\"\n },\n \"validUntil\": 2,\n \"nonce\": \"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901\"\n}"
response = http.request(request)
puts response.read_body{
"apiKey": "<string>",
"address": "<string>",
"createdAt": 123,
"allSubaccounts": true,
"accountIndex": 4,
"validUntil": 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": "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"
}Signing the request
The request is authenticated by a secp256k1 EIP-712 typed-data signature (eth_signTypedData_v4) produced by the wallet that owns address, so wallets render each field and institutional / MPC signers can whitelist on the domain. Sign the typed data below, then split the 65-byte result into {r, s, v} for the signature field. The gateway recovers the signer and rejects the request with HTTP 401 if the recovered address does not equal address.
Domain
API keys are pure off-chain authentication, so the domain has no verifyingContract (the field is optional per EIP-712):
{
"name": "Arcus API Key",
"version": "1",
"chainId": <rootchain chain id, e.g. 421614>
}
0–9 for a single subaccount, or 255 to scope the key to all of the address’s subaccounts (the default for web/mobile trading keys):
CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil,uint8 accountIndex)
accountIndex 0 you may instead sign the original index-less type (backward compatibility, no deadline); a non-zero accountIndex (including the 255 all-subaccounts sentinel) MUST use the type above:
CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil)
nonce in the request body, sign the combined type that binds both replay protection and subaccount scope:
CreateApiKey(string apiWalletName,string apiWalletPublicKey,uint256 validUntil,string nonce,uint8 accountIndex)
nonce and accountIndex in the message must match the request body. For accountIndex 0 the server also accepts the nonce-only type without accountIndex in the signed payload (backward compatibility).
Message
{
"apiWalletName": "<apiWalletName>",
"apiWalletPublicKey": "<publicKey>",
"validUntil": <validUntil>,
"accountIndex": <accountIndex>
}
apiWalletPublicKey is the request’s publicKey field (64 hex chars, no 0x); validUntil is epoch ms, the same value as the request field. Always send validUntil explicitly and sign that value — if the body omits it, the server verifies against its own default (now + 14 days), which will not match what you signed. accountIndex is the key’s subaccount scope — 0–9 for a single subaccount, or 255 for all subaccounts — and must match the request field; omit it from the message only when signing the index-less type for index 0. The legacy EIP-191 personal_sign fallback is index-0 only.
Per-environment domain parameters
| Environment | chainId |
|---|---|
| staging | 421614 |
| testnet | 46630 |
| production | 4663 |
eth_signTypedData_v4 call (ethers v6 / viem)
// ethers v6
const domain = {
name: "Arcus API Key",
version: "1",
chainId: 421614 // staging
};
const types = {
CreateApiKey: [
{ name: "apiWalletName", type: "string" },
{ name: "apiWalletPublicKey", type: "string" },
{ name: "validUntil", type: "uint256" }
]
};
const message = {
apiWalletName: "Arcus",
apiWalletPublicKey: pubKeyHex, // 64 hex chars, no 0x
validUntil: validUntil // epoch ms
};
const sig = await signer.signTypedData(domain, types, message);
// Split into r / s / v for the request body:
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: "CreateApiKey", message });
Legacy EIP-191 signatures (deprecated)
This endpoint previously accepted an EIP-191personal_sign signature over the canonical JSON message
{"apiWalletName":"<apiWalletName>","apiWalletPublicKey":"<publicKey>","validUntil":<validUntil>}
Public-key ownership
A public key is a globally unique credential: auth resolves the owning account fromapi_keys_by_key. createApiKey rejects registering a public key already owned by a different account with HTTP 409 — including keys that have expired but were never revoked. Re-registering a key the caller already owns (renew / rewrite validUntil) is allowed.Body
Ethereum address to associate with the account.
^(0x|0X)?[0-9a-fA-F]{40}$"0x742d35cc6634c0532925a3b844bc9e7595f2bd18"
Hex-encoded Ed25519 public key. Becomes the API key.
64^[0-9a-fA-F]{64}$"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
Name of the API wallet (included in the signing message).
1 - 64"Arcus"
secp256k1 EIP-712 typed-data signature (r, s, v) over the CreateApiKey payload (eth_signTypedData_v4), produced by the wallet that owns address. See the POST /v1/createApiKey endpoint description for the exact domain, types, and message. During the migration window the deprecated legacy EIP-191 personal_sign scheme is also accepted. Requests where the recovered signer does not equal address are rejected with HTTP 401.
Show child attributes
Show child attributes
Subaccount scope for this key. 0–9 locks the key to that single subaccount; 255 scopes it to all of the address's subaccounts (the default for web/mobile trading keys). Defaults to 0 when omitted. The value is bound into the signed payload: any non-zero value (including 255) MUST use the accountIndex-bearing EIP-712 CreateApiKey type (see the endpoint description); index 0 additionally accepts the legacy index-less type/signature for backward compatibility.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255 Expiration timestamp (epoch ms). Must be between 1 day and 180 days from the server's current time (inclusive). If omitted, defaults to 14 days from now. Explicit values outside the [now+1d, now+180d] window are rejected with HTTP 400.
x >= 1Optional replay-protection nonce, single-use per (address, accountIndex) when included in the signed EIP-712 payload. Send the current time as nanoseconds since the Unix epoch (a plain base-10 integer); the server accepts a timestamp up to 48 hours in the past and 24 hours in the future by default. A legacy opaque nonce (e.g. a UUID) is also accepted. Omitting nonce (or signing without it) skips replay protection during the migration window.
64"a1b2c3d4-5f60-7182-93a4-b5c6d7e8f901"
Response
API key creation accepted. The request has been accepted but the key is not yet usable for authenticated requests; the caller must wait briefly for it to be ingested before the returned api_key resolves.
Newly generated API key (hex string).
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}$Creation timestamp (epoch microseconds).
True when the key trades every subaccount of address rather than a single one. Populated on every response; use it as the scope discriminator (optional in schema for deploy-order compatibility).
The single subaccount (0–9) the key trades. Present only when allSubaccounts is false; omitted for an all-subaccounts key.
0 <= x <= 9Expiration timestamp (epoch ms). Echoes the client-supplied expiry, so it stays in milliseconds. Absent if no expiry.
Was this page helpful?