Request Signing
You must include a digital signature in all write requests (POST, PUT, PATCH, DELETE). BLOX uses RFC 9421 HTTP Message Signatures. The signature gives integrity, authenticity, and non-repudiation for each request.
How it Works
RFC 9421 signatures use asymmetric cryptography. You sign requests with your Private Key. BLOX verifies them with the Signing Public Key that you gave when BLOX created your API key.
| Aspect | Description |
|---|---|
| Key Type | A public/private key pair |
| Security | Your private key stays on your server |
| Algorithm | Ed25519 / ECDSA |
To sign a request:
- Canonicalize the request body and hash it to make the Content-Digest.
- Make a Signature Base String that contains the request metadata (method, path) and the headers.
- Sign the base string with your private key.
- Send the signature and the metadata in the
SignatureandSignature-Inputheaders.
Key Pair Generation
Before you send signed requests, generate a cryptographic key pair. Register the public key when BLOX creates your API key.
Supported Algorithms
| Algorithm | Identifier | Key Format | Signed data | Best For |
|---|---|---|---|---|
| Ed25519 | ed25519 | PEM | The signature base string | Recommended for most integrations |
| ECDSA P-256 | ecdsa-p256-sha256 | PEM | The signature base string | Standard ECDSA |
| ECDSA secp256k1 | ecdsa-secp256k1-sha256 | EVM address | An EIP-712 typed message. Refer to its section. | Signatures from an Ethereum wallet |
Ed25519 and ECDSA P-256 sign the same bytes and use the same helper below. secp256k1 uses a different scheme. Read its section before you select it.
Generating Ed25519 Keys (Recommended)
# Generate private key
openssl genpkey -algorithm Ed25519 -out private_key.pem
# Extract public key
openssl pkey -in private_key.pem -pubout -out public_key.pemGenerating ECDSA P-256 Keys
# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
# Extract public key
openssl ec -in private_key.pem -pubout -out public_key.pemUsing an Ethereum Wallet (secp256k1)
To sign with an EVM wallet, give your Ethereum address as the public key. BLOX verifies each signature against this address.
Required Headers
All signed requests must have these headers and also blox-api-key:
| Header | Format | Description |
|---|---|---|
Content-Digest | sha-256=:BASE64: | The Base64-encoded SHA-256 hash of the canonicalized request body. Do not send it if the request has no body. |
Signature-Input | sig1=(...);created=...;keyid=...;alg=... | Metadata that describes the signature components. |
Signature | sig1=:BASE64: | The cryptographic signature of the base string. |
Content-Digest
Calculate the SHA-256 hash of the canonicalized body (keys in alphabetical order). Encode the hash in Base64:
Content-Digest: sha-256=:X48E9qOokqqrvDts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:Signature-Input
This header defines the signed components. The necessary components are @method, @path, content-digest, and content-type. If a request has no body, sign only @method and @path. The pages of the endpoints that take no body tell you this.
Signature-Input: sig1=(@method @path content-digest content-type);created=1705900000;keyid="your_key_id";alg="ed25519"Signature
The signature, with a colon before it and a colon after it:
Signature: sig1=:w7SdqL8L...:Signature Base String
The signature base string shows the request in a deterministic format:
"@method": POST
"@path": /v1/wallet/token/withdrawals
"content-digest": sha-256=:X48E9qOokqqrvDts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"content-type": application/json
"@signature-params": (@method @path content-digest content-type);created=1705900000;keyid="your_key_id";alg="ed25519"Implementation Examples
The examples use Ed25519 and RFC 8785 JSON canonicalization. Set BLOX_KEY_ID and BLOX_PRIVATE_KEY (the path to your PKCS#8 PEM private key). Then use the helper again with the examples on the endpoint pages.
Algorithm-Specific Notes
Ed25519
- Sign the signature base directly. A prehash is not necessary.
- BLOX recommends this algorithm because it is simple and secure.
- Use
alg="ed25519"in Signature-Input.
ECDSA P-256
- Use SHA-256 as the hash function.
- Use
alg="ecdsa-p256-sha256"in Signature-Input.
ECDSA secp256k1 (EVM Wallet)
- The
keyidis your Ethereum address (for example,0x742d35Cc...). It is also a field in the signed message. - Use
alg="ecdsa-secp256k1-sha256"inSignature-Input. - The
Signatureheader contains the base64 of the raw 65-byter || s || v. Do not use hex. - The address that the signature recovers to must be the same as your registered address.
Use exactly this typed data:
const domain = { name: "Blox API", version: "1" };
const types = {
HttpRequest: [
{ name: "method", type: "string" },
{ name: "path", type: "string" },
{ name: "contentDigest", type: "string" },
{ name: "contentType", type: "string" },
{ name: "created", type: "uint256" },
{ name: "keyid", type: "address" },
],
};The domain has no chainId and no verifyingContract. It has only the two fields above.
This is a complete signer that uses viem:
// npm install viem
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { privateKeyToAccount } from "viem/accounts";
const wallet = privateKeyToAccount(process.env.BLOX_EVM_PRIVATE_KEY);
const domain = { name: "Blox API", version: "1" };
const types = {
HttpRequest: [
{ name: "method", type: "string" },
{ name: "path", type: "string" },
{ name: "contentDigest", type: "string" },
{ name: "contentType", type: "string" },
{ name: "created", type: "uint256" },
{ name: "keyid", type: "address" },
],
};
export async function signBloxEvm(method, path, body) {
const digest = createHash("sha256")
.update(canonicalize(body))
.digest("base64");
const contentDigest = `sha-256=:${digest}:`;
const created = Math.floor(Date.now() / 1000);
const signatureHex = await wallet.signTypedData({
domain,
types,
primaryType: "HttpRequest",
message: {
method: method.toUpperCase(),
path,
contentDigest,
contentType: "application/json",
created: BigInt(created),
// Your address is both the keyid and a signed field.
keyid: wallet.address,
},
});
// 65 raw bytes (r || s || v), base64 — not the 0x hex string.
const signature = Buffer.from(signatureHex.slice(2), "hex").toString(
"base64",
);
const params =
`(@method @path content-digest content-type);created=${created};` +
`keyid="${wallet.address}";alg="ecdsa-secp256k1-sha256"`;
return {
"Content-Digest": contentDigest,
"Signature-Input": `sig1=${params}`,
Signature: `sig1=:${signature}:`,
};
}Signature-Input also lists (@method @path content-digest content-type). These components and the header values must agree with the typed message that you signed. If they do not agree, the verification fails.
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Invalid signature | The signature verification failed | Make sure that your signature base is exactly the same as the format above |
Missing Signature-Input header | The header is not in the request | Include the Signature-Input header |
Missing Content-Digest header for request with body | The body hash is not in the request | Include Content-Digest each time that you send a body |
Clock drift detected | created is outside the permitted window | Wallet / Checkout: in the range 30s in the past to 5s in the future. Payout: in the range ±300s |
Signature has already been used | BLOX received the same signature two times | Make a new signature with a new created value for each attempt, also for retries |
Common Issues
- Key order: Canonicalize the JSON (keys in alphabetical order) before you hash it.
- Clock skew: Keep
createdin the window of the product (refer to the table above). - Encoding: Use UTF-8 for all string operations.
- Line endings: Use
\n(LF), not\r\n(CRLF), in the signature base. - Replay protection: BLOX accepts each signature one time only, on all APIs. Make a new signature for each retry. If the endpoint takes an
Idempotency-Key, use the same key again.
Payout differences
The wire format is the same as for Wallet and Onramp. The freshness policy is different. The policy applies to the route, not to the path prefix. All routes are under /v1:
| Policy | Wallet / Onramp / Checkout | Payout |
|---|---|---|
| Routes | All other routes under /v1, including /v1/wallet/* | /v1/payouts/* and /v1/payout/prefund/balance |
created= window | 30s in the past / 5s in the future | ±300 seconds |
| BLOX accepts each signature one time only | Yes | Yes |
The two APIs both have trigger addresses. Each trigger address route uses the window of its API. /v1/wallet/bank-accounts/{bankAccountId}/address uses the narrow window. /v1/payouts/beneficiaries/{beneficiaryId}/address uses the payout window. If you sign requests for the two routes, use a new created value for each request.
BLOX accepts a signature one time only on each API. Thus, to retry a request, make a new signature with a new created value. This control is different from the Idempotency-Key. With a new signature, BLOX accepts the retry. The Idempotency-Key prevents a second payout from the retry.
Webhook signatures use HMAC, not RFC 9421. Refer to Webhooks.
Security Best Practices
- Never share your private key: Keep it only on your server.
- Rotate keys periodically: Create new keys and deactivate the old keys.
- Use environment variables: Do not hard-code keys in source code.
- Monitor API key usage: Examine the dashboard for unusual activity.