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). The one exception: a buyer who pays through the widget in a coin you don't settle in pays a 0.15% swap fee on top, and you still receive the full amount. 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" },
  return_url: "https://yourstore.com/orders/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": "buyer@example.com",
    "metadata": { "order_id": "1234" },
    "return_url": "https://yourstore.com/orders/1234"
  }'

Send amount_usd_cents, or line_items (price IDs or inline price_data) and Shieldz adds them up. Optional: memo, customer_email, metadata (any JSON, echoed back on webhooks), expires_in_seconds, idempotency_key (safely retry creates), and return_url (must be a public https URL — the paid checkout page shows a "Return to merchant" button and auto-redirects there after 4 seconds).

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.

Sell from a catalog products · payment links · buy button

No backend? Do it the way Stripe does: you set prices in Shieldz, and your website only says what is being bought. A buyer's browser never sets the price, so editing the page can't change what they pay.

ObjectWhat it is
prod_… productSomething you sell: name, description, image.
price_… priceWhat a product costs, fixed or a range the customer picks from (tips, donations). Prices never change: a new price replaces the old one, and your payment links move to it.
plink_… payment linkA hosted page at https://shieldz.cash/l/plink_… with one or more prices, optional adjustable quantity, email and shipping address, and a thank-you note or redirect.
Two kinds of payment link. These catalog links (plink_…, /api/v1/payment_links) belong to your shop: they sell your products, show in your dashboard and count sales. The keyless POST /api/v1/links on the agent page is a different, simpler thing: a one-off link that pays a wallet address, with no account or API key.

Build a payment link

Make products and links in the dashboard (Products, Payment links) or with your secret key. Set one up here: the code changes as you type, and the preview is the real page your buyers see.

Product

To upload a file instead, use Products in the dashboard.

Price

Quantity

Checkout

After payment

yourstore.com/products/

Press Buy now: this is the popup your buyers get.

Your code, as you set it up

# A product with its price
curl https://shieldz.cash/api/v1/products \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"name":"Blue hoodie","default_price_data":{"unit_amount_usd_cents":4999}}'

# A payment link for it (buyer picks 1-5, we ask for a shipping address)
curl https://shieldz.cash/api/v1/payment_links \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"line_items":[{"product":"prod_...","quantity":1,
        "adjustable_quantity":{"enabled":true,"minimum":1,"maximum":5}}],
       "collect_shipping_address":true}'

The builder only makes code: nothing is created until you run it with your key. Products and payment links have no test mode: a link takes real payments whichever key made it. To try a priced checkout without real money, create an invoice with line_items and an sk_test_… key (see Test mode). A checkout's total must be from $1 to $100,000.

Put it on your site

Buy button. Paste this where the product is, with the link's id. The checkout opens on your page. Or just share the link's url.

<script src="https://shieldz.cash/widget.js" defer></script>
<button data-shieldz-link="plink_...">Buy now</button>

Your own JS cart. Turn on carts of price IDs in the dashboard (Quick start → Widget), then start a checkout with your publishable shop key. Only your own active prices are accepted:

// <script src="https://shieldz.cash/widget.js" data-key="pk_live_..." defer></script>
Shieldz.checkout({
  lineItems: [{ price: "price_...", quantity: 2 }],
  order: "A-1043", // your cart or order id → client_reference_id
});

Widget fee: when a buyer pays a buy button, inline checkout or JS cart in a coin you don't settle in, the swap includes a 0.15% Shieldz fee. It's added to the buyer's total, so you still receive the full amount. Paying in your own settlement coin has no fee, and neither do invoices you create with your secret key or links you share as a URL.

Your own server. Send line_items when you create the invoice; price_data works for anything not in your catalog:

curl https://shieldz.cash/api/v1/invoices \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"line_items":[{"price":"price_...","quantity":2},
                     {"price_data":{"name":"Gift wrap","unit_amount_usd_cents":300}}],
       "client_reference_id":"A-1043"}'

Change, archive and look up

# Change the price. Prices never change, so this adds a new price, archives
# the old one and moves your payment links to it. Link URLs stay the same.
curl https://shieldz.cash/api/v1/products/prod_... \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"default_price_data":{"unit_amount_usd_cents":3999}}'

# Stop selling: archive the product, or turn off just the link
curl https://shieldz.cash/api/v1/products/prod_... -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" -d '{"active":false}'
curl https://shieldz.cash/api/v1/payment_links/plink_... -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" -d '{"active":false}'

# Look things up: each link comes back with paid_count and paid_usd_cents
curl https://shieldz.cash/api/v1/payment_links -H "Authorization: Bearer sk_live_..."
curl "https://shieldz.cash/api/v1/products?active=true" -H "Authorization: Bearer sk_live_..."

Product images: over the API, image_url takes a public https URL. To upload a file, use Products in the dashboard. Every field is described in the API reference, and the Postman collection has a ready request for each.

Invoices from any of these carry line_items, payment_link, client_reference_id and, when the link asked for it, shipping (in webhooks and to your API key; never on the public checkout). Fulfil from the invoice.paid webhook.

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.

To settle a pending test invoice, simulate the payment with the same test key (or press Simulate payment on the invoice in the dashboard). The invoice flips to paid and a signed invoice.paid webhook goes to your endpoint within about 10 seconds, flagged "simulated": true. No chain, no funds. A webhook endpoint must be configured first.

curl -X POST https://shieldz.cash/api/v1/invoices/{invoice.id}/simulate_payment \
  -H "Authorization: Bearer sk_test_..."

Check what was delivered with GET /api/v1/webhook_deliveries (attempt, status and your endpoint's HTTP response code per delivery). Replay one with POST /api/v1/webhook_deliveries/{id}/retry.

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
          return_url: "https://yourstore.com/orders/1234",
        }),
      });
      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