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.
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
| Concept | Detail |
|---|---|
| Algorithm | Ed25519 (EdDSA) |
| Transport | Authorization: APIKey <jwt> (scheme case-insensitive; document as APIKey) |
| JWT expiry | 30 seconds |
| Replay protection | Unique nonce per request |
| Body integrity | SHA-256 hash of request body |
| Method matching | Exact match (GET, POST, etc.) |
| URI matching | Exact match (path + query params) |
| Key expiration | Maximum 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
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:
- Navigate to Settings > API Keys
- Click Create API Key
- Enter a description (e.g. "Production Server")
- Upload your
public_key.pemfile - Set an expiration date (maximum 1 year)
- 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:
| Header | Value |
|---|---|
Authorization | APIKey <signed-jwt> |
The JWT is signed with your private key and contains:
| Claim | Type | Description |
|---|---|---|
sub | string | Required. Your Key ID (raw UUID). |
tenantId | string | Required. Your Workspace ID (raw UUID). |
accountId | string | Account-scoped keys only. Must equal the key's bound account (raw UUID or cst_… TypeID). Omit for workspace-scoped keys (the common case today). |
uri | string | Required. Exact request path + query params (e.g. /api/v2/balances?accountId=123). |
method | string | Required. HTTP method in uppercase (e.g. GET, POST, DELETE). |
nonce | string | Required. A unique value per request (UUID v4 recommended). Prevents replay attacks. |
bodyHash | string | Required. SHA-256 hex digest of the request body. Use the hash of an empty string for GET requests. |
iat | number | Required. Issued at (Unix timestamp). |
exp | number | Required. Expiration (Unix timestamp). Maximum 30 seconds after iat. |
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
}
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.
| Check | Rejection reason |
|---|---|
| API key not found / deleted | Unauthorized |
| API key is frozen | Unauthorized |
| API key has expired | Unauthorized |
| Account API keys disabled (account-scoped keys) | Unauthorized |
| Client IP not in allowlist (when configured) | Unauthorized |
| JWT signature invalid | Unauthorized |
JWT alg is not EdDSA | Unauthorized |
JWT sub does not match key id | Unauthorized |
JWT tenantId missing / mismatch | Unauthorized |
JWT accountId missing / mismatch (account-scoped key) | Unauthorized |
JWT accountId present on a workspace-scoped key | Unauthorized |
| 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 30s | Unauthorized |
JWT uri / method / bodyHash mismatch | Unauthorized |
JWT nonce already used (≈60s window) | Unauthorized |
Key Rotation
You can have multiple active API keys per workspace, enabling zero-downtime rotation:
- Create a new API key with a new key pair in the Dashboard
- Update your application to use the new key
- Verify the new key works in production
- 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:
- Instagram: BlockWyre Instagram
- Twitter: BlockWyre Twitter
- Facebook: BlockWyre Facebook
- LinkedIn: BlockWyre LinkedIn
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!