Vaultody API Authentication
Vaultody REST API uses HMAC-SHA256 (Hash-based Message Authentication Code) to authenticate and authorize every request. You must sign each API call to verify your identity and ensure requests have not been tampered with in transit.
Overview
To interact with the Vaultody API securely, you need to:
- Generate an API key pair (Key, Secret, Passphrase) in the Dashboard.
- Sign each request using the HMAC-SHA256 algorithm.
- Include the required headers with every request.
Required Credentials
Generate your credentials in the Dashboard under Developers → API Keys → Create New API Key.
- API Key (
x-api-key) — the public identifier of your application. Safe to log. - API Secret — used to compute the signature. Never share or expose this value.
- Passphrase (
x-api-passphrase) — an additional user-defined secret for added security.
Security: Store your API Secret and Passphrase in environment variables or a secrets manager. Never commit them to source code or include them in client-side applications.
Signature Generation
The x-api-sign header is generated by signing a message string with your API Secret using
HMAC-SHA256.
Step 1 — Build the message string
Concatenate these five elements in order with no separator:
{timestamp}{HTTP_METHOD}{request_path}{body}{query_params}
Rules:
timestamp— current UNIX timestamp in seconds (integer as string)HTTP_METHOD— uppercase:GET,POST,PUTrequest_path— the URL path only, starting with/(e.g./vaults/main)body— the raw JSON request body string. Use{}for GET requests and requests with no bodyquery_params— query parameters as a JSON object string. Use{}if none
Examples:
GET /vaults/main with no body and no query params:
1715709672GET/vaults/main{}{}
POST /vaults/{vaultId}/vault-account with body:
{
"context": "yourExampleString",
"data": {
"item": {
"color": "#00C7E6",
"isHiddenInDashboard": false,
"name": "User Alice"
}
}
}
Important: The JSON body must be minified (no extra whitespace or newlines) before signing. Whitespace differences will cause a signature mismatch.
Step 2 — Decode the API Secret
Your API Secret is Base64-encoded. Decode it to raw bytes before signing:
decoded_secret = base64.b64decode(api_secret)
Step 3 — Compute the HMAC-SHA256 signature
signature = hmac.new(decoded_secret, message.encode('utf-8'), hashlib.sha256).digest()
Step 4 — Base64-encode the signature
signature_b64 = base64.b64encode(signature).decode()
This final value is your x-api-sign header.
Required Headers
| Header | Required | Description |
|---|---|---|
x-api-key |
Yes | Your public API key |
x-api-sign |
Yes | Base64-encoded HMAC-SHA256 signature |
x-api-timestamp |
Yes | Current UNIX timestamp in seconds |
x-api-passphrase |
Yes | Your passphrase |
Content-Type |
Yes | Must be application/json on all requests |
x-api-version |
No | Pins this request to a specific API version, e.g. 2026-03-20. Takes precedence over the version pinned to your API key in the Dashboard. An unknown value returns 409 invalid_api_version listing the accepted values |
Code Examples
Python
import time, hmac, hashlib, base64, requests, json
api_key = 'your_api_key'
api_secret = 'your_api_secret'
passphrase = 'your_passphrase'
def signed_request(method, path, body=None, params=None):
timestamp = str(int(time.time()))
body_str = json.dumps(body, separators=(',', ':')) if body else '{}'
query_str = json.dumps(params, separators=(',', ':')) if params else '{}'
message = timestamp + method.upper() + path + body_str + query_str
decoded_secret = base64.b64decode(api_secret)
signature = hmac.new(decoded_secret, message.encode('utf-8'), hashlib.sha256).digest()
signature_b64 = base64.b64encode(signature).decode()
headers = {
'x-api-key': api_key,
'x-api-sign': signature_b64,
'x-api-timestamp': timestamp,
'x-api-passphrase': passphrase,
'Content-Type': 'application/json'
}
url = 'https://rest.vaultody.com' + path
if method.upper() == 'GET':
return requests.get(url, headers=headers, params=params)
elif method.upper() == 'POST':
return requests.post(url, headers=headers, json=body)
elif method.upper() == 'PUT':
return requests.put(url, headers=headers, json=body)
# Example: list vaults
response = signed_request('GET', '/vaults/main')
print(response.json())
Node.js
const crypto = require('crypto');
const axios = require('axios');
const apiKey = 'your_api_key';
const apiSecret = 'your_api_secret';
const passphrase = 'your_passphrase';
function signedRequest(method, path, body = null, params = null) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyStr = body ? JSON.stringify(body) : '{}';
const queryStr = params ? JSON.stringify(params) : '{}';
const message = timestamp + method.toUpperCase() + path + bodyStr + queryStr;
const hmac = crypto.createHmac('sha256', Buffer.from(apiSecret, 'base64'));
hmac.update(message);
const signature = hmac.digest('base64');
const headers = {
'x-api-key': apiKey,
'x-api-sign': signature,
'x-api-timestamp': timestamp,
'x-api-passphrase': passphrase,
'Content-Type': 'application/json',
};
const url = 'https://rest.vaultody.com' + path;
if (method.toUpperCase() === 'GET') return axios.get(url, { headers, params });
if (method.toUpperCase() === 'POST') return axios.post(url, body, { headers });
if (method.toUpperCase() === 'PUT') return axios.put(url, body, { headers });
}
// Example: list vaults
signedRequest('GET', '/vaults/main')
.then(res => console.log(res.data))
.catch(err => console.error(err.response.data));
PHP
<?php
$api_key = 'your_api_key';
$api_secret = 'your_api_secret';
$passphrase = 'your_passphrase';
function signed_request(string $method, string $path, array $body = [], array $params = []): array {
global $api_key, $api_secret, $passphrase;
$timestamp = (string) time();
$body_str = empty($body) ? '{}' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$query_str = empty($params) ? '{}' : json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$message = $timestamp . strtoupper($method) . $path . $body_str . $query_str;
$decoded_secret = base64_decode($api_secret);
$signature = base64_encode(hash_hmac('sha256', $message, $decoded_secret, true));
$headers = [
'x-api-key: ' . $api_key,
'x-api-sign: ' . $signature,
'x-api-timestamp: ' . $timestamp,
'x-api-passphrase: ' . $passphrase,
'Content-Type: application/json',
];
$url = 'https://rest.vaultody.com' . $path;
if (!empty($params)) $url .= '?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if (strtoupper($method) === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body_str);
} elseif (strtoupper($method) === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body_str);
}
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Example: list vaults
$result = signed_request('GET', '/vaults/main');
print_r($result);
Postman (Pre-request Script)
var CryptoJS = require("crypto-js");
var secretKey = pm.variables.get("x-api-key");
var passphrase = pm.variables.get("x-api-passphrase");
var secret = pm.variables.get("x-api-secret");
var timestamp = Math.floor(Date.now() / 1000).toString();
var bodyStr = (pm.request.method === 'GET')
? JSON.stringify({})
: pm.request.body.raw.replace(/\n(?=(?:[^"]*"[^"]*")*[^"]*$)/g, '').replace(/(\".*?\"|\s+)/g, '$1');
var queryObj = pm.request.url.query.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});
var messageToSign = timestamp + pm.request.method.toUpperCase()
+ pm.request.url.getPath()
+ bodyStr
+ JSON.stringify(queryObj);
var key = CryptoJS.enc.Base64.parse(secret);
var signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(messageToSign, key));
pm.request.headers.add({ key: "x-api-timestamp", value: timestamp });
pm.request.headers.add({ key: "x-api-sign", value: signature });
pm.request.headers.add({ key: "x-api-key", value: secretKey.toString() });
pm.request.headers.add({ key: "x-api-passphrase", value: passphrase });
pm.request.headers.add({ key: "Content-Type", value: "application/json" });
Security Best Practices
- Never expose your API Secret — treat it like a password. Store it in environment variables.
- Use IP whitelisting — restrict API key access to known server IP addresses in the Dashboard.
- Rotate keys regularly — regenerate API keys periodically and immediately if you suspect a compromise.
- Scope your keys — create separate API keys with minimal required permissions for each service.
- Generate a fresh timestamp per request — never reuse or cache a timestamp across calls.
Troubleshooting
| HTTP | Error code | Cause | Fix |
|---|---|---|---|
| 401 | missing_authorization_header |
One of the four authentication headers was not sent | Send x-api-key, x-api-sign, x-api-timestamp and x-api-passphrase on every request |
| 401 | invalid_api_key |
Unknown, deleted or not-yet-active API key, or wrong passphrase | Check the key and passphrase, or generate a new key in the Dashboard |
| 401 | invalid_api_sign |
Signature mismatch | Check body minification, query serialisation, and that the secret is Base64-decoded before signing |
| 401 | invalid_api_timestamp |
x-api-timestamp is outside the accepted window |
Generate a fresh timestamp per request and keep your server clock in sync |
| 403 | ip_address_not_whitelisted |
The request came from an IP that is not on the key's allowlist | Add the IP in the Dashboard, or clear the allowlist |
| 403 | endpoint_not_allowed_for_api_key |
The key lacks permission for this endpoint | Create a key with the correct scopes |
| 403 | endpoint_not_allowed_for_plan |
The endpoint is not part of your subscription | Upgrade your plan |
| 403 | feature_mainnets_not_allowed_for_plan |
Mainnet call on a plan without mainnet access | Upgrade your plan |
| 403 | mainnet_not_allowed_for_key |
Test API key used against a mainnet network | Use a main API key |
| 403 | testnet_not_allowed_for_key |
Main API key used against a testnet network | Use a test API key |
| 403 | api_key_not_allowed_for_wallet |
The key is scoped to specific vaults and this is not one of them | Use a key scoped to that vault |
| 409 | missing_required_attributes |
A required path, query or body attribute is absent | Add the attribute |
| 409 | extra_body_attributes / extra_query_attributes |
An unknown attribute was sent | Send only documented attributes |
| 409 | invalid_api_version |
Unknown x-api-version value |
Use one of the versions listed in the error message |
| 415 | unsupported_media_type |
Missing or wrong Content-Type |
Send Content-Type: application/json |
| 422 | invalid_request_body_structure |
Body not wrapped in { "data": { "item": {} } } |
Wrap all POST and PUT bodies in the envelope |
| 429 | request_limit_reached |
Rate limit exceeded | Back off and retry — see Data Flow Limiting |
Common Signature Failure Causes
- Timestamp drift — the most frequent cause of 401 errors, returned as
invalid_api_timestamp. The window is asymmetric: a timestamp may be at most 30 seconds in the past and at most 10 seconds in the future relative to Vaultody's server time, so a clock that runs fast fails sooner than one that runs slow. Always calltime()/Date.now()inside your signing function, never outside it, and keep your server on NTP. - Body whitespace — extra spaces or newlines in the JSON body produce a different signature than
the server expects. Minify your body string before signing (use
separators=(',', ':')in Python,JSON.stringifyin JavaScript). - Wrong query format — query parameters must be serialised as a JSON object string
(
{"key":"value"}), not a URL query string (key=value). If there are no query parameters, use{}. - Attribute order — the signed body and query strings are compared as serialised JSON, so send the attributes in the same order you signed them.