API Authentication with Signed Requests
SideShift.ai authenticates API requests with an Ed25519 keypair that you generate yourself. You register the public key with your account and sign each request with the secret key. SideShift only stores the public key, so the secret key never leaves your server.
New integrations should use signed requests. The legacy account secret (x-sideshift-secret) still works during a migration period. If you use it, migrate to signed requests.
1. Generate a Keypair
Generate the keypair offline, once. Both keys are 32 bytes, hex encoded (64 characters).
import { ed25519 } from '@noble/curves/ed25519.js';
import { bytesToHex } from '@noble/curves/utils.js';
const secretKey = crypto.getRandomValues(new Uint8Array(32));
const pubKey = bytesToHex(ed25519.getPublicKey(secretKey));
const secretKeyHex = bytesToHex(secretKey); // store this
from nacl.signing import SigningKey
signing_key = SigningKey.generate()
secret_key = signing_key.encode().hex()
pub_key = signing_key.verify_key.encode().hex()
Store the secret key like a password. Anyone who has it can act as your account, and SideShift cannot recover it for you.
2. Create an Account with Your Public Key
Send the public key to POST /v2/create-account:
curl -L 'https://sideshift.ai/api/v2/create-account' \
-H 'Content-Type: application/json' \
-d '{ "pubKey": "PUBLIC_KEY" }'
{
"id": "YQMi62XMb",
"pubKey": "PUBLIC_KEY"
}
Use the returned id as your affiliateId. An account created this way has no secret: the keypair is its only credential.
Using the Private Key from the Account Page
If you created your account on the site, open the account page, click Show Private Key and copy the key. Check its length:
-
64 hex characters: this is your Ed25519 secret key. It is already registered to your account, so you can sign requests with it straight away. It is not your legacy account secret, even though it is shown under the same button. Your public key is derived from it:
const pubKey = bytesToHex(ed25519.getPublicKey(hexToBytes(secretKeyHex))); -
32 characters: this is a legacy account secret. Send it as
x-sideshift-secret, and migrate to signed requests when you can.
3. Sign Each Request
Build the message by joining these four values, with no separators and no hashing:
timestamp + METHOD + path + body
| Part | Value |
|---|---|
timestamp | Current Unix time in milliseconds, as a string. The same value goes in x-sideshift-timestamp. |
METHOD | HTTP method in uppercase, e.g. GET, POST. |
path | Everything after the host, including /api and the query string, e.g. /api/v2/shifts/variable. |
body | The exact request body string you send. Use an empty string for requests without a body, such as GET. |
Sign the message with your secret key and send these three headers:
| Header | Value |
|---|---|
x-sideshift-key | Your public key, hex (a 0x prefix is optional) |
x-sideshift-timestamp | The timestamp used in the message |
x-sideshift-signature | The Ed25519 signature, hex (128 characters) |
import { ed25519 } from '@noble/curves/ed25519.js';
import { bytesToHex, hexToBytes } from '@noble/curves/utils.js';
const signRequest = ({ method, path, body = '' }, secretKeyHex, pubKey) => {
const timestamp = Date.now().toString();
const message = new TextEncoder().encode(timestamp + method.toUpperCase() + path + body);
const signature = bytesToHex(ed25519.sign(message, hexToBytes(secretKeyHex)));
return {
'x-sideshift-key': pubKey,
'x-sideshift-timestamp': timestamp,
'x-sideshift-signature': signature,
};
};
const path = '/api/v2/shifts/variable';
const body = JSON.stringify({
settleAddress: '0xde2642b2120fd3011fe9659688f76e9E4676F472',
affiliateId: 'YQMi62XMb',
depositCoin: 'btc',
depositNetwork: 'bitcoin',
settleCoin: 'eth',
settleNetwork: 'mainnet',
});
const response = await fetch(`https://sideshift.ai${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-ip': END_USER_IP,
...signRequest({ method: 'POST', path, body }, secretKeyHex, pubKey),
},
body,
});
import time
from nacl.signing import SigningKey
def sign_request(method, path, body, secret_key):
signing_key = SigningKey(bytes.fromhex(secret_key))
timestamp = str(int(time.time() * 1000))
message = (timestamp + method.upper() + path + body).encode()
signature = signing_key.sign(message).signature.hex()
return {
"x-sideshift-key": signing_key.verify_key.encode().hex(),
"x-sideshift-timestamp": timestamp,
"x-sideshift-signature": signature,
}
Signing Rules
- Sign the exact bytes you send. Serialize the body once, sign that string, and send that same string. Re-serializing after signing (different key order or spacing) breaks the signature.
- Send
Content-Type: application/jsonon requests with a body. Without it, the body is not included when the signature is checked, and the request fails withInvalid authorisation signature. - The timestamp must be within 60 seconds of server time. If it is not, the error response includes
serverTimeso you can correct your clock offset. - Sign again on every retry. Generate a new timestamp and signature for each attempt.
- A signature takes priority over a secret. If
x-sideshift-signatureis present, the request is authenticated by signature only, and an invalid signature fails with401even on endpoints that do not require authentication.
Signed requests work on every endpoint that accepts x-sideshift-secret.
Errors
Signature failures return 401 with code INVALID_SIGNATURE. See Error Codes for every message and how to fix it.
If your clock is off by more than 60 seconds, the error includes serverTime. Use it to correct your clock offset:
{
"error": {
"message": "Timestamp outside of valid 60s range",
"serverTime": 1792743050123,
"code": "INVALID_SIGNATURE"
}
}
Managing Keys
An account can have up to 5 active keys. Use these endpoints to rotate a key without creating a new account:
GET /v2/account/auth-keys: list your keys.POST /v2/account/auth-keys: register another key.DELETE /v2/account/auth-keys/{id}: disable a key.
To rotate a key:
- Generate a new keypair.
- Register it with
POST /v2/account/auth-keys, authenticated with your current key. To prove you own the new key, sign your account ID string with the new secret key and send it assignature. A proof signed over anything else, or with the current key, fails with400 Invalid signature. - Switch your integration to the new key.
- Disable the old key with
DELETE /v2/account/auth-keys/{id}.
const signature = bytesToHex(
ed25519.sign(new TextEncoder().encode(accountId), hexToBytes(newSecretKeyHex))
);
// body: { "pubKey": newPubKey, "signature": signature }
You cannot disable the last active key of an account that has no secret, because the account would be locked.
Legacy Account Secret
Older accounts, and accounts created with POST /v2/create-account without a pubKey, have a 32 character account secret. Send it as the x-sideshift-secret header:
curl -L 'https://sideshift.ai/api/v2/account' \
-H 'x-sideshift-secret: YOUR_ACCOUNT_SECRET'
The secret works during a migration period.
Migrate to Signed Requests
You keep the same account and account ID. You only add a key to it.
-
Sign your account ID string with the new secret key. This proves you own the key. See the proof of ownership example.
-
Register the public key with
POST /v2/account/auth-keys. Authenticate this request with your account secret:curl -L -X POST 'https://sideshift.ai/api/v2/account/auth-keys' \-H 'Content-Type: application/json' \-H 'x-sideshift-secret: YOUR_ACCOUNT_SECRET' \-d '{"pubKey": "PUBLIC_KEY","signature": "ACCOUNT_ID_SIGNED_WITH_NEW_KEY"}' -
Sign your requests with the new key and stop sending
x-sideshift-secret.
Your secret keeps working after you register a key, so you can switch at your own pace.