Request Signing
Sign Operator API requests with your Ed25519 key
Every Operator API request must be signed with your Ed25519 private key. Phoenix verifies the signature with the public key registered for your operator.
Headers
| Header | Description |
|---|---|
X-Operator-Code | Your Phoenix Prediction operator code |
X-Operator-Environment | Target environment: sandbox or prod |
X-Signature-Timestamp | Unix seconds when the request was signed |
X-Signature | Base64url Ed25519 signature |
Requests outside the timestamp replay window are rejected.
Canonical Payload
Sign this exact string:
operator_code + "\n" +
environment + "\n" +
timestamp + "\n" +
METHOD + "\n" +
path + "\n" +
sha256_hex(raw_body)The environment is the same value sent in X-Operator-Environment (sandbox or prod).
Example for a bodyless GET /operator/api/settings:
acme
sandbox
1779100000
GET
/operator/api/settings
e3b0c44298fc1c149afbf4c8996fb924...The path does not include scheme, host, or query string. Bodyless requests use the SHA-256 hash of an empty string.
Node.js Example
import { createPrivateKey, createHash, sign } from 'node:crypto';
function sha256Hex(input: string) {
return createHash('sha256').update(input).digest('hex');
}
function signOperatorRequest(input: {
operatorCode: string;
environment: 'sandbox' | 'prod';
method: string;
path: string;
body?: string;
privateKeyPem: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = input.body ?? '';
const payload = [
input.operatorCode,
input.environment,
timestamp,
input.method.toUpperCase(),
input.path,
sha256Hex(body),
].join('\n');
const key = createPrivateKey(input.privateKeyPem);
const signature = sign(null, Buffer.from(payload), key).toString('base64url');
return {
'content-type': 'application/json',
'x-operator-code': input.operatorCode,
'x-operator-environment': input.environment,
'x-signature-timestamp': timestamp,
'x-signature': signature,
};
}For Ed25519, Node's sign algorithm argument is null.
Failure Behavior
Authentication failures return HTTP 401 with:
{ "error": "unauthorized" }Phoenix intentionally does not reveal which verification step failed.