Sign a trade request
Build the exact Ed25519 message, sign the body bytes, and retry without creating a second order.
Every request to an operation with the trade API-key scope needs an Ed25519 signature. The signature proves that your client holds the private key registered when the trade key was created.
This request signature does not replace a Canton Party's signature over a ledger transaction. See the order flow for that second step.
Required headers
| Header | Value |
|---|---|
X-API-Key | Your customer trade key. |
X-API-Timestamp | Unix time in milliseconds, exactly 13 digits. |
X-API-Signature | Ed25519 signature in unpadded base64url, exactly 86 characters. |
Idempotency-Key | A stable identifier for this logical write. Required on order prepare and hosted submission; include it on all signed POST requests. |
X-API-Recv-Window | Optional validity window in milliseconds; defaults to 5000, maximum 60000. |
Canonical message
Join these six lines with a single \n. Do not add a trailing newline.
<timestamp>
<receive window, or 5000 when the header is absent>
<uppercase HTTP method>
<path and query exactly as received by the API>
<Idempotency-Key, or an empty line when absent>
<lowercase hex SHA-256 of the body bytes>Sign the UTF-8 bytes of this message with the Ed25519 private key. The body hash covers the bytes received by the API after content decoding. For no body, hash the empty string. Serialize a JSON body once, hash it, and send those same bytes.
A proxy must preserve the path and query. Reordering query parameters, changing escaping, or merging slashes after signing changes the message the server verifies.
Node.js example
Save this helper as signed-post.mjs. It uses Node.js built-ins and a PKCS#8 PEM Ed25519 private key from the environment. The caller supplies the idempotency key so it can be reused for a retry of the same operation.
import { createHash, createPrivateKey, sign } from "node:crypto";
const API = "https://launch-api.supanova.app";
const apiKey = process.env.SUPANOVA_API_KEY;
const signingKeyPem = process.env.SUPANOVA_SIGNING_KEY_PEM;
if (!apiKey || !signingKeyPem) throw new Error("Missing API credentials");
const privateKey = createPrivateKey(signingKeyPem);
export async function signedPost(path, payload, idempotencyKey) {
if (!path.startsWith("/api/v1/")) throw new Error("Unexpected API path");
if (!idempotencyKey) throw new Error("Missing idempotency key");
const body = payload === undefined ? "" : JSON.stringify(payload);
const timestamp = String(Date.now());
const digest = createHash("sha256").update(body, "utf8").digest("hex");
const message = [timestamp, "5000", "POST", path, idempotencyKey, digest].join("\n");
const signature = sign(null, Buffer.from(message, "utf8"), privateKey)
.toString("base64url");
const response = await fetch(`${API}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
"X-API-Timestamp": timestamp,
"X-API-Signature": signature,
"Idempotency-Key": idempotencyKey,
},
body: body || undefined,
signal: AbortSignal.timeout(15_000),
});
const result = await response.json();
if (!response.ok) {
const error = new Error(result.error?.message ?? `HTTP ${response.status}`);
Object.assign(error, {
status: response.status,
code: result.error?.code,
requestId: result.requestId,
retryAfter: response.headers.get("retry-after"),
});
throw error;
}
return result;
}For a new logical operation, use randomUUID() from node:crypto. Persist that ID alongside the payload before sending an order. Do not generate a new ID just because the connection timed out.
Clock and replay protection
The timestamp may be at most one second ahead of the server, and no older than the signed receive window. An out-of-window request returns API_KEY_TIMESTAMP_OUT_OF_WINDOW; details.serverTime helps diagnose clock drift. Keep the host clock synchronized.
The server remembers a signature for 61 seconds. Reusing a signature returns API_KEY_SIGNATURE_INVALID, even if the receive window has not expired. On a retry, use a fresh timestamp and signature while preserving the original idempotency key and body for that logical write.
Failed signature attempts consume the trade key's write budget. Fix a signing error before retrying it in a loop.
Signature failures to check
- Use Ed25519, not ECDSA or an HMAC.
- Use unpadded base64url, not standard base64. The signature must be 86 characters.
- Hash exactly the body you send. Whitespace and key order change JSON bytes.
- Include the same receive window and idempotency key in the message and headers.
- Preserve the path, query string, and uppercase method.
- Check the timestamp and avoid reusing a signature.
Continue with quoting and submitting orders.