Jump to
Ctrl
+
/

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:

  1. Generate an API key pair (Key, Secret, Passphrase) in the Dashboard.
  2. Sign each request using the HMAC-SHA256 algorithm.
  3. 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, PUT
  • request_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 body
  • query_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 call time() / 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.stringify in 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.
Was this page helpful?
Yes
No
Powered by