Skip to main content

Authentication

BlockWyre API uses Ed25519 JWT per-request signing for authentication. You generate an Ed25519 key pair locally, upload the public key through the BlockWyre Dashboard, and sign every API request with your private key. Your private key never leaves your server.

Breaking change (v3)

v2 used X-API-Key + Authorization: Bearer <jwt>. v3 uses a single header:

Authorization: APIKey <signed-jwt>

The Key ID is the JWT sub claim (raw UUID). Do not send X-API-Key. A JWT signed for API-key auth sent as Bearer is treated as a session token and will fail.

Overview​

ConceptDetail
AlgorithmEd25519 (EdDSA)
TransportAuthorization: APIKey <jwt> (scheme case-insensitive; document as APIKey)
JWT expiry30 seconds
Replay protectionUnique nonce per request
Body integritySHA-256 hash of request body
Method matchingExact match (GET, POST, etc.)
URI matchingExact match (path + query params)
Key expirationMaximum 1 year, mandatory

Step 1: Generate an Ed25519 Key Pair​

Generate your key pair locally using OpenSSL:

# Generate private key
openssl genpkey -algorithm Ed25519 -out private_key.pem

# Extract public key
openssl pkey -in private_key.pem -pubout -out public_key.pem
Keep your private key safe

Your private key (private_key.pem) must never be shared, uploaded, or exposed. Only the public key is uploaded to BlockWyre.

Step 2: Upload the Public Key​

Upload your public key (public_key.pem) through the BlockWyre Dashboard:

  1. Navigate to Settings > API Keys
  2. Click Create API Key
  3. Enter a description (e.g. "Production Server")
  4. Upload your public_key.pem file
  5. Set an expiration date (maximum 1 year)
  6. Click Create

After creation, the dashboard displays your Key ID (a UUID, e.g. 550e8400-e29b-41d4-a716-446655440000) and your Workspace ID. Copy and store both values — you'll include them in every API request JWT (sub and tenantId).

Step 3: Sign Every API Request​

Each request must include:

HeaderValue
AuthorizationAPIKey <signed-jwt>

The JWT is signed with your private key and contains:

ClaimTypeDescription
substringRequired. Your Key ID (raw UUID).
tenantIdstringRequired. Your Workspace ID (raw UUID).
accountIdstringAccount-scoped keys only. Must equal the key's bound account (raw UUID or cst_… TypeID). Omit for workspace-scoped keys (the common case today).
uristringRequired. Exact request path + query params (e.g. /api/v2/balances?accountId=123).
methodstringRequired. HTTP method in uppercase (e.g. GET, POST, DELETE).
noncestringRequired. A unique value per request (UUID v4 recommended). Prevents replay attacks.
bodyHashstringRequired. SHA-256 hex digest of the request body. Use the hash of an empty string for GET requests.
iatnumberRequired. Issued at (Unix timestamp).
expnumberRequired. Expiration (Unix timestamp). Maximum 30 seconds after iat.
Workspace vs account-scoped keys

A workspace-scoped key (fk_account_id null) behaves like a tenant member: leave accountId out of the JWT; requests without an accountId see the whole workspace, and you may pass accountId in the path/query to narrow to one account. An account-scoped key behaves like a customer account: the JWT requires a matching accountId claim, requests without an accountId are automatically confined to the bound account, naming a foreign account in the path or query is rejected with 403, and a foreign resource id reads as 404. Body fields may reference other accounts where the operation legitimately needs a counterparty (e.g. a transfer target).

JWT Structure​

Header:

{
"alg": "EdDSA",
"typ": "JWT"
}

Payload:

{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"tenantId": "770a9c20-1234-5678-9abc-def012345678",
"uri": "/api/v2/balances?accountId=123&coin=btc",
"method": "GET",
"nonce": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"bodyHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"iat": 1708300000,
"exp": 1708300030
}
Empty body hash

For requests without a body (GET, DELETE), bodyHash must be the SHA-256 of an empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Complete Node.js Example​

Installation​

npm install jose

Client Implementation​

import crypto from "node:crypto";
import fs from "node:fs";
import { SignJWT, importPKCS8 } from "jose";

const API_KEY_ID = "550e8400-e29b-41d4-a716-446655440000";
const TENANT_ID = "770a9c20-1234-5678-9abc-def012345678";
const BASE_URL = "https://api.blockwyre.com";

// Load your private key once at startup
const privateKeyPem = fs.readFileSync("private_key.pem", "utf-8");
const privateKey = await importPKCS8(privateKeyPem, "EdDSA");

/**
* Signs and sends an authenticated API request.
*/
async function apiRequest(method, path, body = null) {
const bodyString = body ? JSON.stringify(body) : "";
const bodyHash = crypto
.createHash("sha256")
.update(bodyString)
.digest("hex");

const now = Math.floor(Date.now() / 1000);

const jwt = await new SignJWT({
tenantId: TENANT_ID,
uri: path,
method: method.toUpperCase(),
nonce: crypto.randomUUID(),
bodyHash,
})
.setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
.setSubject(API_KEY_ID)
.setIssuedAt(now)
.setExpirationTime(now + 30)
.sign(privateKey);

const response = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
Authorization: `APIKey ${jwt}`,
...(body && { "Content-Type": "application/json" }),
},
...(body && { body: bodyString }),
});

if (!response.ok) {
const error = await response.text();
throw new Error(`API error ${response.status}: ${error}`);
}

return response.json();
}

Usage Examples​

// GET request
const balances = await apiRequest("GET", "/v3/balances?accountId=123");
console.log(balances);

// POST request with body
const transfer = await apiRequest("POST", "/v3/transfers", {
fromAccountId: "account-uuid",
toAccountId: "account-uuid",
amount: "100.00",
coin: "USDC",
});
console.log(transfer);

// DELETE request
await apiRequest("DELETE", "/v3/webhooks/webhook-uuid");

Verification Rules​

The server validates every request against these rules. If any check fails, the request is rejected with 401 Unauthorized.

CheckRejection reason
API key not found / deletedUnauthorized
API key is frozenUnauthorized
API key has expiredUnauthorized
Account API keys disabled (account-scoped keys)Unauthorized
Client IP not in allowlist (when configured)Unauthorized
JWT signature invalidUnauthorized
JWT alg is not EdDSAUnauthorized
JWT sub does not match key idUnauthorized
JWT tenantId missing / mismatchUnauthorized
JWT accountId missing / mismatch (account-scoped key)Unauthorized
JWT accountId present on a workspace-scoped keyUnauthorized
Path / query names a foreign account (account-scoped key)Forbidden
Resource id belongs to a foreign account (account-scoped key)Not Found
JWT exp in the past (5s leeway)Unauthorized
JWT TTL longer than 30sUnauthorized
JWT uri / method / bodyHash mismatchUnauthorized
JWT nonce already used (≈60s window)Unauthorized

Key Rotation​

You can have multiple active API keys per workspace, enabling zero-downtime rotation:

  1. Create a new API key with a new key pair in the Dashboard
  2. Update your application to use the new key
  3. Verify the new key works in production
  4. Deactivate or delete the old key from the Dashboard

Support and Resources​

If you need assistance or have any questions, our support team is here to help. You can contact our support team at support@blockwyre.com.

Stay Updated​

Stay up-to-date with the latest news, updates, and features from BlockWyre by following us on social media:

We are excited to have you on board and look forward to seeing how you leverage BlockWyre's powerful tools to enhance your financial operations. Happy integrating!