Shieldz API Docs, Accept Crypto Payments (Non-custodial)
Shieldz

Shieldz API docs

Accept crypto, settle to your own wallet. Create an invoice, send your customer to a hosted checkout, and get a signed webhook the moment it's paid. Base URL: https://shieldz.cash

Shieldz payment flow: your server calls POST /api/v1/invoices, Shieldz returns a pay_url, the buyer pays any coin on the hosted checkout, funds settle to your wallet, and an invoice.paid signed webhook (or GET /api/v1/invoices/:id) tells your server it is paid.

Overview

Shieldz is a non-custodial crypto payment API. Funds settle straight to a wallet you control; Shieldz only ever sees a public address, so there is nothing to freeze or skim, and there is no platform fee (network gas only). A full integration is the four steps in the diagram above.

ModeSetupBest for
API keyAn sk_live_ key and signed webhooks. Also unlocks Bitcoin and shielded Zcash.Servers and stores.
KeylessJust a wallet address, no account, no key. Payment links, tip jars, an MCP server for AI agents, or a plain GET https://shieldz.cash/api/v1/tip-jars?to=0x…&title=… URL.Agents, zero-setup links.

This page documents API-key mode. For keyless mode and AI agents, see the agents guide.

Official SDK Node / TypeScript

Prefer a typed client? The official SDK wraps this API with invoice helpers, webhook signature verification, automatic retries, and auto-pagination, zero dependencies, runs on Node 18+, Deno, Bun, and edge runtimes.

npm install @shieldz/sdk
import Shieldz, { constructEvent } from "@shieldz/sdk";

const shieldz = new Shieldz(process.env.SHIELDZ_API_KEY);

// Create an invoice, send the buyer to invoice.pay_url
const invoice = await shieldz.invoices.create({
  amount_usd_cents: 2500,
  memo: "Order #1234",
  metadata: { order_id: "1234" },
});

// Verify a webhook against the raw body, then act on it
const event = await constructEvent(rawBody, signatureHeader, WEBHOOK_SECRET);
if (event.type === "invoice.paid") fulfill(event.data.invoice);

Source, examples & issues: github.com/ShieldZCash/shieldz-sdk. Also: a browsable API reference, the OpenAPI spec, and a Postman collection. Prefer raw HTTP? The full REST reference below works from any language.

1Get an API key

Create one in your dashboard under Developer mode → API keys (merchant.shieldz.cash). Keys are shown once, store them securely.

sk_live_… for production, sk_test_… for test mode (simulated settlement, real webhooks). Authenticate every request with Authorization: Bearer <key>.

2Create an invoice

POST /api/v1/invoices

curl https://shieldz.cash/api/v1/invoices \
  -H "Authorization: Bearer sk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd_cents": 2500,
    "memo": "Order #1234",
    "customer_email": "[email protected]",
    "metadata": { "order_id": "1234" }
  }'

Only amount_usd_cents is required. Optional: memo, customer_email, metadata (any JSON, echoed back on webhooks), expires_in_seconds, and idempotency_key (safely retry creates).

Response:

{
  "id": "R1uykKHSKVXc3Qb5x2T9",
  "object": "invoice",
  "amount_usd_cents": 2500,
  "status": "pending",
  "pay_url": "https://shieldz.cash/pay/R1uykKHSKVXc3Qb5x2T9",
  "expires_at": 1750000000000,
  "mode": "live"
}

3Send your customer to checkout

Redirect the buyer to the pay_url from the response, a hosted page that handles coin selection, the live amount, and payment detection. You build nothing.

https://shieldz.cash/pay/{invoice.id}
The Shieldz hosted checkout page: an amount due, a memo, and a grid of coins (USDC, Ethereum, BNB, Avalanche, Tether) the buyer can pay with, noting 'any coin, your keys, non-custodial and feeless.'
The hosted checkout. The buyer picks any coin; it settles to your wallet in your chosen asset.

4Get notified when it's paid webhooks

Set your endpoint URL in Developer mode → Webhooks. Shieldz POSTs a JSON event on every state change:

EventWhen
invoice.paidPayment detected & sufficient, fulfill the order.
invoice.failedPayment failed / refunded, do not fulfill.
invoice.expiredWindow elapsed with no payment.
{
  "type": "invoice.paid",
  "created": 1750000000,
  "data": {
    "invoice": {
      "id": "R1uykKHSKVXc3Qb5x2T9",
      "status": "paid",
      "amount_usd_cents": 2500,
      "metadata": { "order_id": "1234" },
      "paid_after_expiry": false
    }
  }
}

Verify the signature on every webhook, it's how you know it's really from Shieldz:

import { createHmac, timingSafeEqual } from "node:crypto";

// Shieldz signs every webhook:  X-Shieldz-Signature: t=<unix>,v1=<hex>
// where hex = HMAC_SHA256(your_signing_secret, "<t>.<raw_request_body>").
const TOLERANCE_SECONDS = 300; // reject events older than 5 min

function verifyShieldzWebhook(rawBody, header, secret) {
  const parts = header.split(",");
  const t = parts.find(p => p.startsWith("t="))?.slice(2);
  if (!t) return false;

  // 1) Replay guard: t is INSIDE the signed payload (so it can't be forged),  //    reject anything too old to stop a captured event being re-sent later.
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > TOLERANCE_SECONDS) {
    return false;
  }

  // 2) Recompute the HMAC and constant-time compare. During key rotation the
  //    header may carry multiple v1= values, accept any match.
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const sigs = parts.filter(p => p.startsWith("v1=")).map(p => p.slice(3));
  return sigs.some(s =>
    s.length === expected.length &&
    timingSafeEqual(Buffer.from(s), Buffer.from(expected))
  );
}

// IMPORTANT: verify against the RAW request body bytes, before JSON.parse.
// Also dedupe on the X-Shieldz-Delivery header (at-least-once delivery), that
// + the timestamp check together fully close replay.
Always verify against the raw request body before parsing JSON, and treat delivery as at-least-once, dedupe on the X-Shieldz-Delivery header. The X-Shieldz-Event header carries the event type.

Or check a payment yourself polling

Don't want to run a webhook listener? Retrieve any invoice on demand and read its status. Same call powers reconciliation jobs and "did this pay?" checks.

GET /api/v1/invoices/{id}

curl https://shieldz.cash/api/v1/invoices/R1uykKHSKVXc3Qb5x2T9 \
  -H "Authorization: Bearer sk_live_your_key"
{
  "id": "R1uykKHSKVXc3Qb5x2T9",
  "object": "invoice",
  "status": "paid",          // pending | paid | expired | failed
  "paid_at": 1750000000000,  // null until paid
  "amount_usd_cents": 2500,
  "expires_at": 1750000000000
}

status is one of pending, paid, expired, failed; paid_at is set once it settles. Poll politely (every few seconds with backoff) until paid/expired, invoices carry an expires_at so you know when to stop.

Webhooks are real-time and the recommended primary path; polling is the simplest fallback when you can't receive inbound HTTP, and a good belt-and-suspenders for reconciliation.

Chains & assets

Pick where funds settle with settlement on the invoice. Stablecoins work in every mode; Bitcoin and shielded Zcash need an API key.

AssetChainsModes
USDCBase, Arbitrum, Optimism, Polygon, EthereumAll
USDTBase, Arbitrum, Optimism, Polygon, EthereumAll
BTCBitcoinAPI key
ZEC (shielded)ZcashAPI key
Pay with any crypto. The buyer can pay in a coin you do not list; it is swapped and settles to you in your chosen asset. You always receive what you asked for.

Errors & rate limits

Every error returns a JSON body with a stable type and code you can branch on, plus a human message and, for validation errors, the offending param.

{
  "error": {
    "type": "invalid_request",
    "code": "missing_field",
    "message": "amount_usd_cents is required",
    "param": "amount_usd_cents"
  }
}
StatustypeMeaning
400invalid_requestMalformed or missing parameters.
401auth_errorMissing or invalid API key.
403invalid_requestKey not allowed for this action.
404invalid_requestNo such invoice or resource.
409conflictIdempotency key reused with a different body.
429rate_limitToo many requests, back off and retry.
500internal_errorTransient, retry with backoff.

Send an idempotency_key on POST /api/v1/invoices to make retries safe, the same key returns the original invoice instead of creating a duplicate.

Test mode

Use an sk_test_… key to create test invoices: settlement is simulated, but webhooks fire for real so you can build and verify your integration end-to-end before going live.

Full example Bun

Everything above wired together, create an invoice, redirect to checkout, and verify + handle the webhook (with replay protection and idempotency). Copy-paste runnable:

// Bun, full Shieldz integration. Zero dependencies (node:crypto is built in).
// Run: SHIELDZ_API_KEY=sk_live_... SHIELDZ_WEBHOOK_SECRET=whsec_... bun run server.ts
import { createHmac, timingSafeEqual } from "node:crypto";

const SHIELDZ_API = "https://shieldz.cash";
const API_KEY = process.env.SHIELDZ_API_KEY;          // sk_live_... (or sk_test_)
const WEBHOOK_SECRET = process.env.SHIELDZ_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
const seen = new Set();  // dedupe delivery ids, use Redis/DB in production

Bun.serve({
  port: 3000,
  async fetch(req) {
    const { pathname } = new URL(req.url);

    // 1. Create an invoice, send the buyer to the hosted checkout.
    if (req.method === "POST" && pathname === "/checkout") {
      const resp = await fetch(SHIELDZ_API + "/api/v1/invoices", {
        method: "POST",
        headers: {
          "Authorization": "Bearer " + API_KEY,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          amount_usd_cents: 2500,
          memo: "Order #1234",
          metadata: { order_id: "1234" },   // echoed back on the webhook
        }),
      });
      if (!resp.ok) return new Response("could not create invoice", { status: 502 });
      const invoice = await resp.json();
      return Response.redirect(invoice.pay_url, 303);  // hosted Shieldz checkout
    }

    // 2. Receive webhooks, verify against the RAW body bytes (req.text()).
    if (req.method === "POST" && pathname === "/webhooks/shieldz") {
      const rawBody = await req.text();
      const sig = req.headers.get("X-Shieldz-Signature") ?? "";
      if (!verifyShieldzWebhook(rawBody, sig, WEBHOOK_SECRET)) {
        return new Response("invalid signature", { status: 400 });
      }
      const deliveryId = req.headers.get("X-Shieldz-Delivery");
      if (deliveryId && seen.has(deliveryId)) return new Response("ok");  // replay/dupe
      if (deliveryId) seen.add(deliveryId);

      const event = JSON.parse(rawBody);
      if (event.type === "invoice.paid") {
        fulfillOrder(event.data.invoice.metadata?.order_id);
      }
      return new Response("ok");  // ack fast; do slow work async
    }

    return new Response("not found", { status: 404 });
  },
});

function verifyShieldzWebhook(rawBody, header, secret) {
  const parts = header.split(",");
  const t = parts.find(p => p.startsWith("t="))?.slice(2);
  if (!t) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  const sigs = parts.filter(p => p.startsWith("v1=")).map(p => p.slice(3));
  return sigs.some(s => s.length === expected.length &&
    timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

function fulfillOrder(orderId) {
  // Your logic: mark the order paid, ship it, grant access...
  console.log("fulfilled order", orderId);
}

console.log("Shieldz example listening on :3000");

Reference & SDKs

ResourceLink
Browsable API referenceshieldz.cash/reference
OpenAPI spec/openapi.json
Postman collection/shieldz.postman_collection.json
Official SDKs (Node, Python, Rust, PHP)github.com/ShieldZCash/shieldz-sdk
AI agents & MCPshieldz.cash/agents