요금GitHub대시보드

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.

HMACX-Signatureerrorsrate limitscurlNode.js

← Public API

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
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Errors

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',
  }),
)

팁

  • 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.