Public API
Authentication
Every Public API request is authenticated with an API key id and an HMAC-SHA256 signature over a canonical string. The signature covers the method, the path with its query string, and the body, so none of them can be changed on the way. A request whose time differs from the server by more than 300 seconds is rejected.
Headers
X-Api-Key — the id of the key (a UUID) from the cabinet API keys page.
X-Timestamp — Unix time in seconds (integer). It must be within ±300 seconds of the server time.
X-Signature — lowercase hex HMAC-SHA256 of the canonical string, keyed with the secret of the key (sk_live_…, shown once when the key is created).
Requests with a body are sent as JSON with Content-Type: application/json (otherwise 415 public_api.unsupported_media_type).
Canonical string
Join four lines with a single newline (\n) between them. There is no newline after the last line.
METHOD is uppercase (GET, POST, PUT, PATCH, DELETE).
path is the request target exactly as the API server receives it: the whole path, which starts with /v1/…, and the query string with its "?" when there is one. The query IS signed (a different query gives a different signature), so build the query string once and use the same string in the signature and in the URL.
body is the raw body bytes as sent. With no body, hash the empty string: sha256_hex("") is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Formula
{timestamp}\n{METHOD}\n{path}\n{sha256_hex(body)}
X-Signature = hex(hmac_sha256(secret, canonical))
# GET https://api.rpcnode.dev/v1/usage?days=30
1760270400
GET
/v1/usage?days=30
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Errors
An error answer is JSON: ok: false, error_code (a stable machine-readable code), error (a message), and for a body that cannot be accepted also errors — one entry for every field that is wrong (field, error_code, error).
A request whose X-Timestamp is too old or too new is answered as an invalid signature (public_api.signature_invalid).
Authentication and request errors
401 public_api.key_missing X-Api-Key is missing
401 public_api.key_invalid no such key, or it was deleted
403 public_api.ip_not_allowed the key has an IP allowlist and the request comes from another address
401 public_api.timestamp_invalid X-Timestamp is missing or not a number
401 public_api.signature_missing X-Signature is missing
401 public_api.signature_invalid the signature does not match, or the time is more than 300 s off
403 public_api.user_inactive the account of the key is blocked
429 public_api.rate_limited a limit below was reached (see Retry-After)
429 public_api.too_many_failures too many failed sign-ins from this address (see Retry-After)
413 public_api.payload_too_large the body is larger than 1 MiB
400 public_api.body_too_deep the JSON is nested deeper than 16 levels
415 public_api.unsupported_media_type the body is not sent as application/json
404 public_api.method_not_found there is no such route
400 common.validation_failed the body or a parameter is wrong; see "errors"Validation errors
When a body or a parameter is wrong, the answer is 400 with every field that failed, not only the first. With a single failed field the top-level error_code is that field’s code (for example payment.order_id_required); with several it is common.validation_failed.
A value of the wrong type is named by field ("amount must be a string (write it in quotes)"). A body that is not JSON is reported as the field "body". A field that the method does not have is also reported (a typo is never dropped silently): "token_contrakt is not a field of this request."
A value outside its bounds is refused with the field and the reason, not cut to the bound: page 1–10000, per_page 1–100, days 7 / 30 / 90, text up to 255 characters, ids as UUIDs.
A 400 with several fields
{
"ok": false,
"error_code": "common.validation_failed",
"error": "Validation failed",
"errors": [
{ "field": "order_id", "error_code": "payment.order_id_required", "error": "order_id is required." },
{ "field": "amount", "error_code": "payment.amount_required", "error": "Provide amount_raw, or amount." }
]
}Rate limits
Three limits protect the API.
Per client address: 30 requests per second, whatever the request is (with a key, without one, with a wrong one). Over the limit: 429 public_api.rate_limited, Retry-After: 1.
Failed sign-ins per client address: 20 per minute (an unknown key, an address that is not allowed, a wrong signature or time). After that the address waits for the next minute: 429 public_api.too_many_failures, with Retry-After until the end of the minute — even for a correct signature.
Per account: 100 requests per minute, for all keys of the account together, on every plan. Over the limit: 429 public_api.rate_limited, Retry-After until the start of the next minute. A request over this limit is not counted as a failed sign-in.
The plan rps_limit is not related to this API: it is the limit of JSON-RPC traffic on endpoint URLs. API requests and RPC requests do not take from each other.
Notes
If the key has an IP allowlist, it applies to the address the request comes from; a request from any other address is refused with 403 public_api.ip_not_allowed.
The body is limited to 1 MiB and to a JSON depth of 16.
Shell example: GET account, GET usage with a query, POST an address
Shell
# Prerequisites: openssl, curl. Replace API_KEY (UUID) and SECRET.
BASE=https://api.rpcnode.dev
API_KEY=7c9e6679-7425-40de-944b-e07fc1f90ae7
SECRET=sk_live_your_secret
# --- GET /v1/account (empty body) ---
REQ_PATH=/v1/account
METHOD=GET
BODY=
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$REQ_PATH" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -sS "$BASE$REQ_PATH" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"
# --- GET /v1/usage?days=30 (the query is signed with the path) ---
REQ_PATH='/v1/usage?days=30'
METHOD=GET
BODY=
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$REQ_PATH" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -sS "$BASE$REQ_PATH" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"
# --- POST /v1/address-watches ---
REQ_PATH=/v1/address-watches
METHOD=POST
BODY='{"network_slug":"ethereum","address":"0x0000000000000000000000000000000000000000"}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$REQ_PATH" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -sS -X POST "$BASE$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d "$BODY"Node.js example: GET account, GET usage with a query, POST an address
JavaScript
import crypto from 'node:crypto'
const API_KEY = process.env.PUBLIC_API_KEY // UUID from the cabinet
const SECRET = process.env.PUBLIC_API_SECRET // shown once at key creation
const BASE = process.env.PUBLIC_API_BASE || 'https://api.rpcnode.dev'
function sha256Hex(body) {
return crypto.createHash('sha256').update(body, 'utf8').digest('hex')
}
// path is the request target as the server receives it: the whole path and the query, if any
function sign({ method, path, body = '' }) {
const timestamp = Math.floor(Date.now() / 1000)
const canonical = [String(timestamp), method.toUpperCase(), path, sha256Hex(body)].join('\n')
const signature = crypto.createHmac('sha256', SECRET).update(canonical, 'utf8').digest('hex')
return { timestamp, signature }
}
async function publicApi(method, path, bodyObj) {
// Sign the exact string you send: serialize once, send that string.
const body = bodyObj === undefined ? '' : JSON.stringify(bodyObj)
const { timestamp, signature } = sign({ method, path, body })
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': API_KEY,
'X-Timestamp': String(timestamp),
'X-Signature': signature,
},
body: body === '' ? undefined : body,
})
return res.json()
}
// GET account: plan and usage (empty body → sha256 of "")
console.log(await publicApi('GET', '/v1/account'))
// GET usage series: the query string IS part of the signed path
console.log(await publicApi('GET', '/v1/usage?days=30'))
// POST add an address to watch
console.log(
await publicApi('POST', '/v1/address-watches', {
network_slug: 'ethereum',
address: '0x0000000000000000000000000000000000000000',
}),
)Consejos
- The host is https://api.rpcnode.dev and the routes live under /v1. The path you sign starts with /v1/ — the host is not part of the canonical string.
- Sign the exact body string you send. Pretty-printing or re-serializing the JSON after signing breaks the signature.
- Sign the query string too, in the same order and with the same encoding as in the URL.
- The secret stays on your server. Never put it in a browser or a mobile app.
- Outbound webhooks and WebSocket events are documented under Webhooks and WebSockets.