Skip to main content

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

JavaScript
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
Python (PyNaCl)
from nacl.signing import SigningKey

signing_key = SigningKey.generate()

secret_key = signing_key.encode().hex()
pub_key = signing_key.verify_key.encode().hex()
warning

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" }'
Response
{
"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
PartValue
timestampCurrent Unix time in milliseconds, as a string. The same value goes in x-sideshift-timestamp.
METHODHTTP method in uppercase, e.g. GET, POST.
pathEverything after the host, including /api and the query string, e.g. /api/v2/shifts/variable.
bodyThe 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:

HeaderValue
x-sideshift-keyYour public key, hex (a 0x prefix is optional)
x-sideshift-timestampThe timestamp used in the message
x-sideshift-signatureThe Ed25519 signature, hex (128 characters)
JavaScript
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,
});
Python
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/json on requests with a body. Without it, the body is not included when the signature is checked, and the request fails with Invalid authorisation signature.
  • The timestamp must be within 60 seconds of server time. If it is not, the error response includes serverTime so 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-signature is present, the request is authenticated by signature only, and an invalid signature fails with 401 even 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:

Example
{
"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:

To rotate a key:

  1. Generate a new keypair.
  2. 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 as signature. A proof signed over anything else, or with the current key, fails with 400 Invalid signature.
  3. Switch your integration to the new key.
  4. Disable the old key with DELETE /v2/account/auth-keys/{id}.
Proof of ownership for a new key
const signature = bytesToHex(
ed25519.sign(new TextEncoder().encode(accountId), hexToBytes(newSecretKeyHex))
);
// body: { "pubKey": newPubKey, "signature": signature }
info

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.

  1. Generate a keypair.

  2. Sign your account ID string with the new secret key. This proves you own the key. See the proof of ownership example.

  3. 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"
    }'
  4. 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.