> ## Documentation Index
> Fetch the complete documentation index at: https://orderly.network/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# API Authentication

> Implement secure request signing for the Orderly API using the ed25519 standard.

## Generate Request Signature

Orderly uses the [`ed25519` elliptic curve standard](https://ed25519.cr.yp.to/) for request authentication via signature verification.

<Steps>
  <Step title="Orderly Account ID">
    Register your account and obtain your account ID. The registration steps are provided [here](/build-on-omnichain/user-flows/accounts).
    Add your account ID to the request header as `orderly-account-id`.
  </Step>

  <Step title="Orderly Key">
    Add your Orderly public key to the request header as `orderly-key`. To generate and add a new Orderly Key, see [the documentation](/build-on-omnichain/user-flows/wallet-authentication#orderly-key).
    You can also obtain Orderly keys from frontend builders like [WOOFi Pro](https://pro.woofi.com).
  </Step>

  <Step title="Timestamp">
    Take the current timestamp in milliseconds and add it as `orderly-timestamp` to the request header.
  </Step>

  <Step title="Normalize request content">
    Normalize the message to a string by concatenating the following:

    1. Current timestamp in milliseconds, e.g. `1649920583000`
    2. HTTP method in uppercase, e.g. `POST`
    3. Request path **including** query parameters (without base URL), e.g. `/v1/orders?symbol=PERP_BTC_USDC`
    4. (Optional) If the request has a body, JSON stringify it and append

    Example result:

    ```
    1649920583000POST/v1/order{"symbol": "PERP_ETH_USDC", "order_type": "LIMIT", "order_price": 1521.03, "order_quantity": 2.11, "side": "BUY"}
    ```
  </Step>

  <Step title="Generate signature">
    Sign the normalized content using the `ed25519` algorithm, encode the signature in base64 url-safe format, and add the result to the request header as `orderly-signature`.
  </Step>

  <Step title="Content type">
    Set the `Content-Type` header:

    * `GET` and `DELETE`: `application/x-www-form-urlencoded`
    * `POST` and `PUT`: `application/json`
  </Step>

  <Step title="Send the request">
    The final request should have the following headers:

    | Header               | Description                   |
    | -------------------- | ----------------------------- |
    | `Content-Type`       | Request content type          |
    | `orderly-account-id` | Your Orderly account ID       |
    | `orderly-key`        | Your Orderly public key       |
    | `orderly-signature`  | ed25519 signature (base64url) |
    | `orderly-timestamp`  | Request timestamp (ms)        |
  </Step>
</Steps>

<Note>
  The Orderly Key should be used without the `ed25519:` prefix when used in code samples below.
</Note>

### Minimal Example

This is a concise, single-file TypeScript example that demonstrates how to sign and send a request to the Orderly API using the `@noble/ed25519` library.

```ts theme={null}
import { signAsync, getPublicKeyAsync } from "@noble/ed25519";
import bs58 from "bs58";

async function main() {
  const orderlyAccountId = "YOUR_ACCOUNT_ID";
  const orderlySecret = "YOUR_ORDERLY_SECRET"; // base58 encoded
  const baseUrl = "https://testnet-api.orderly.org";
  const url = new URL(`${baseUrl}/v1/order`);
  const method = "POST";
  const body = JSON.stringify({
    symbol: "PERP_ETH_USDC",
    order_type: "MARKET",
    order_quantity: 0.01,
    side: "BUY"
  });
  const timestamp = Date.now();

  const privateKey = bs58.decode(orderlySecret);
  const message = `${timestamp}${method}${url.pathname}${url.search}${body}`;
  const signature = await signAsync(new TextEncoder().encode(message), privateKey);

  const response = await fetch(url, {
    method,
    body,
    headers: {
      "Content-Type": "application/json",
      "orderly-account-id": orderlyAccountId,
      "orderly-key": `ed25519:${bs58.encode(await getPublicKeyAsync(privateKey))}`,
      "orderly-timestamp": String(timestamp),
      "orderly-signature": Buffer.from(signature).toString("base64url")
    }
  });

  const data = await response.json();
  console.log(data);
}

main();
```

### Full Example

<Tabs>
  <Tab title="Java">
    <CodeGroup>
      ```Java AuthenticationExample.java theme={null}
      import io.github.cdimascio.dotenv.Dotenv;
      import net.i2p.crypto.eddsa.EdDSAPrivateKey;
      import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
      import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
      import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
      import okhttp3.OkHttpClient;
      import okhttp3.Request;
      import okhttp3.Response;

      import org.bitcoinj.base.Base58;
      import org.json.JSONObject;

      public class AuthenticationExample {
      public static void main(String[] args) throws Exception {
      String baseUrl = "https://testnet-api.orderly.org";
      String orderlyAccountId = "<orderly-account-id>";

            Dotenv dotenv = Dotenv.load();
            OkHttpClient client = new OkHttpClient();

            String key = dotenv.get("ORDERLY_SECRET");
            EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
            EdDSAPrivateKeySpec encoded = new EdDSAPrivateKeySpec(Base58.decode(key), spec);
            EdDSAPrivateKey orderlyKey = new EdDSAPrivateKey(encoded);

            Signer signer = new Signer(baseUrl, orderlyAccountId, orderlyKey);

            JSONObject json = new JSONObject();
            json.put("symbol", "PERP_ETH_USDC");
            json.put("order_type", "MARKET");
            json.put("order_quantity", 0.01);
            json.put("side", "BUY");
            Request req = signer.createSignedRequest("/v1/order", "POST", json);
            String res;
            try (Response response = client.newCall(req).execute()) {
               res = response.body().string();
            }
            JSONObject obj = new JSONObject(res);

      }
      }

      ```

      ```Java Signer.java theme={null}
      import java.io.UnsupportedEncodingException;
      import java.security.*;
      import java.time.Instant;
      import java.util.Base64;

      import org.json.JSONObject;

      import net.i2p.crypto.eddsa.EdDSAEngine;
      import net.i2p.crypto.eddsa.EdDSAPrivateKey;
      import okhttp3.MediaType;
      import okhttp3.Request;
      import okhttp3.RequestBody;

      public class Signer {
         public final String baseUrl;

         private String accountId;
         private EdDSAPrivateKey privateKey;

         public Signer(String baseUrl, String accountId, EdDSAPrivateKey privateKey) {
            this.baseUrl = baseUrl;
            this.accountId = accountId;
            this.privateKey = privateKey;
         }

         public Request createSignedRequest(String url)
               throws SignatureException, UnsupportedEncodingException, InvalidKeyException, OrderlyClientException {
            checkIsInitialized();
            return createSignedRequest(url, "GET", null);
         }

         public Request createSignedRequest(String url, String method, JSONObject json)
               throws OrderlyClientException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
            checkIsInitialized();
            if (method == "GET" || method == "DELETE") {
               return createSignedFormRequest(url, method);
            } else if ((method == "POST" || method == "PUT") && json != null) {
               return createSignedJsonRequest(url, method, json);
            } else {
               throw new IllegalArgumentException();
            }
         }

         private void checkIsInitialized() throws OrderlyClientException {
            if (accountId == null) {
               throw OrderlyClientExceptionReason.ACCOUNT_UNINITIALIZED.asException();
            }
            if (privateKey == null) {
               throw OrderlyClientExceptionReason.KEYPAIR_UNINITIALIZED.asException();
            }
         }

         private Request createSignedFormRequest(String url, String method)
               throws SignatureException, UnsupportedEncodingException, InvalidKeyException {
            long timestamp = Instant.now().toEpochMilli();
            String message = "" + timestamp + method + url;

            EdDSAEngine signature = new EdDSAEngine();
            signature.initSign(privateKey);
            byte[] orderlySignature = signature.signOneShot(message.getBytes("UTF-8"));

            return new Request.Builder()
                  .url(baseUrl + url)
                  .addHeader("Content-Type", "x-www-form-urlencoded")
                  .addHeader("orderly-timestamp", "" + timestamp)
                  .addHeader("orderly-account-id", accountId)
                  .addHeader("orderly-key", Util.encodePublicKey(privateKey))
                  .addHeader("orderly-signature", Base64.getUrlEncoder().encodeToString(orderlySignature))
                  .get()
                  .build();
         }

         private Request createSignedJsonRequest(String url, String method, JSONObject json)
               throws SignatureException, UnsupportedEncodingException, InvalidKeyException {
            RequestBody body = RequestBody.create(json.toString(), MediaType.parse("application/json"));

            long timestamp = Instant.now().toEpochMilli();
            String message = "" + timestamp + method + url + json.toString();

            EdDSAEngine signature = new EdDSAEngine();
            signature.initSign(privateKey);
            byte[] orderlySignature = signature.signOneShot(message.getBytes("UTF-8"));

            return new Request.Builder()
                  .url(baseUrl + url)
                  .addHeader("Content-Type", "application/json")
                  .addHeader("orderly-timestamp", "" + timestamp)
                  .addHeader("orderly-account-id", accountId)
                  .addHeader("orderly-key", Util.encodePublicKey(privateKey))
                  .addHeader("orderly-signature", Base64.getUrlEncoder().encodeToString(orderlySignature))
                  .post(body)
                  .build();
         }
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Python">
    <CodeGroup>
      ```py authentication_example.py theme={null}
      import json
      import os
      from base58 import b58decode
      from requests import Request, Session

      from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

      from signer import Signer

      base_url = "https://testnet-api.orderly.org"
      orderly_account_id = "<orderly-account-id>"

      key = b58decode(os.environ.get("ORDERLY_SECRET"))
      orderly_key = Ed25519PrivateKey.from_private_bytes(key)

      session = Session()
      signer = Signer(orderly_account_id, orderly_key)

      req = signer.sign_request(
      Request(
      "POST",
      "%s/v1/order" % base_url,
      json={
      "symbol": "PERP_ETH_USDC",
      "order_type": "MARKET",
      "order_quantity": 0.01,
      "side": "BUY",
      },
      )
      )
      res = session.send(req)
      response = json.loads(res.text)

      ```

      ```py signer.py theme={null}
      from base58 import b58encode
      from base64 import urlsafe_b64encode
      from datetime import datetime
      import json
      import math
      from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
      from requests import PreparedRequest, Request
      import urllib

      class Signer(object):
          def __init__(
              self,
              account_id: str,
              private_key: Ed25519PrivateKey,
          ) -> None:
              self._account_id = account_id
              self._private_key = private_key

          def sign_request(self, req: Request) -> PreparedRequest:
              d = datetime.utcnow()
              epoch = datetime(1970, 1, 1)
              timestamp = math.trunc((d - epoch).total_seconds() * 1_000)

              json_str = ""
              if req.json is not None:
                  json_str = json.dumps(req.json)

              url = urllib.parse.urlparse(req.url)
              message = str(timestamp) + req.method + url.path + json_str
              if len(url.query) > 0:
                  message += "?" + url.query

              orderly_signature = urlsafe_b64encode(
                  self._private_key.sign(message.encode())
              ).decode("utf-8")

              req.headers = {
                  "orderly-timestamp": str(timestamp),
                  "orderly-account-id": self._account_id,
                  "orderly-key": encode_key(
                      self._private_key.public_key().public_bytes_raw()
                  ),
                  "orderly-signature": orderly_signature,
              }
              if req.method == "GET" or req.method == "DELETE":
                  req.headers["Content-Type"] = "application/x-www-form-urlencoded"
              elif req.method == "POST" or req.method == "PUT":
                  req.headers["Content-Type"] = "application/json"

              return req.prepare()

      def encode_key(key: bytes):
          return "ed25519:%s" % b58encode(key).decode("utf-8")
      ```
    </CodeGroup>
  </Tab>

  <Tab title="TypeScript">
    <CodeGroup>
      ```ts authenticationExample.ts theme={null}
      import bs58 from 'bs58';
      import { config } from 'dotenv';
      import { webcrypto } from 'node:crypto';

      import { signAndSendRequest } from "./signer";

      // this is only necessary in Node.js to make `@noble/ed25519` dependency work
      if (!globalThis.crypto) globalThis.crypto = webcrypto as any;

      config();

      async function main() {
      const baseUrl = 'https://testnet-api.orderly.org';
      const orderlyAccountId = '<orderly-account-id>';

      const orderlyKey = bs58.decode(process.env.ORDERLY_SECRET!);

      const res = await signAndSendRequest(orderlyAccountId, orderlyKey, `${baseUrl}/v1/order`, {
      method: 'POST',
      body: JSON.stringify({
      symbol: 'PERP_ETH_USDC',
      order_type: 'MARKET',
      order_quantity: 0.01,
      side: 'BUY'
      })
      });
      const response = await res.json();
      console.log(response);
      }

      main();

      ```

      ```ts signer.ts theme={null}
      import { getPublicKeyAsync, signAsync } from '@noble/ed25519';
      import { encodeBase58 } from 'ethers';

      export async function signAndSendRequest(
        orderlyAccountId: string,
        privateKey: Uint8Array | string,
        input: URL | string,
        init?: RequestInit | undefined
      ): Promise<Response> {
        const timestamp = Date.now();
        const encoder = new TextEncoder();

        const url = new URL(input);
        let message = `${String(timestamp)}${init?.method ?? 'GET'}${url.pathname}${url.search}`;
        if (init?.body) {
          message += init.body;
        }
        const orderlySignature = await signAsync(encoder.encode(message), privateKey);

        return fetch(input, {
          headers: {
            'Content-Type':
              init?.method !== 'GET' && init?.method !== 'DELETE'
                ? 'application/json'
                : 'application/x-www-form-urlencoded',
            'orderly-timestamp': String(timestamp),
            'orderly-account-id': orderlyAccountId,
            'orderly-key': `ed25519:${encodeBase58(await getPublicKeyAsync(privateKey))}`,
            'orderly-signature': Buffer.from(orderlySignature).toString('base64url'),
            ...(init?.headers ?? {})
          },
          ...(init ?? {})
        });
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Shell">
    ```shell cURL theme={null}
    curl --request POST \
      --url https://api.orderly.org/v1/order \
      --header 'Content-Type: application/json' \
      --header 'orderly-account-id: <orderly-account-id>' \
      --header 'orderly-key: ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX' \
      --header 'orderly-signature: dG4bkKiqG0dUYLzViRZkvbI6Sy239JxAdNMIBxFZ4w030Jofr0ORV06GHtvXZkaZaWUXE+XAU3fnzKN/5fDeBQ==' \
      --header 'orderly-timestamp: 1649920583000' \
      --data '{
      "symbol": "<symbol>",
      "order_type": "<order_type>",
      "side": "<side>",
      "reduce_only": false
    }'
    ```
  </Tab>
</Tabs>

## Security

Orderly validates every request through three checks. A request must pass all three to be accepted.

### Request Timestamp

The request is rejected if the `orderly-timestamp` header differs from the API server time by more than 300 seconds.

### Signature Verification

The `orderly-signature` header must be a valid ed25519 signature generated from the normalized request content and signed with your Orderly secret key.

### Orderly Key Validity

The `orderly-key` header must reference a key that has been added to the network, is associated with the account, and has not expired.
