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
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.
| Mode | Setup | Best for |
|---|---|---|
| API key | An sk_live_ key and signed webhooks. Also unlocks Bitcoin and shielded Zcash. | Servers and stores. |
| Keyless | Just 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}
4Get notified when it's paid webhooks
Set your endpoint URL in Developer mode → Webhooks. Shieldz POSTs a JSON event on every state change:
| Event | When |
|---|---|
invoice.paid | Payment detected & sufficient, fulfill the order. |
invoice.failed | Payment failed / refunded, do not fulfill. |
invoice.expired | Window 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.
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.
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.
| Object | What it is |
|---|---|
prod_… product | Something you sell: name, description, image. |
price_… price | What 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 link | A 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. |
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
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.
| Asset | Chains | Modes |
|---|---|---|
USDC | Base, Arbitrum, Optimism, Polygon, Ethereum | All |
USDT | Base, Arbitrum, Optimism, Polygon, Ethereum | All |
BTC | Bitcoin | API key |
ZEC (shielded) | Zcash | API key |
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"
}
}
| Status | type | Meaning |
|---|---|---|
400 | invalid_request | Malformed or missing parameters. |
401 | auth_error | Missing or invalid API key. |
403 | invalid_request | Key not allowed for this action. |
404 | invalid_request | No such invoice or resource. |
409 | conflict | Idempotency key reused with a different body. |
429 | rate_limit | Too many requests, back off and retry. |
500 | internal_error | Transient, 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
| Resource | Link |
|---|---|
| Browsable API reference | shieldz.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 & MCP | shieldz.cash/agents |