Create Arc payment addresses that pay straight into your contract, check them, and get a webhook when money lands. Base URL: https://api.arcdoorstep.duckdns.org · Doorstep contract 0x56BD0FEfdaAF9d053a74107E6dDb9de19D306229 on Arc mainnet.
The API never holds money. It calculates Doorstep addresses (anyone can recompute them from the instructions), registers them on-chain so delivery bots pick payments up, and reports what the chain says. Prefer no server? Use the JavaScript SDK directly.
Send your API key in every /v1 request:
Authorization: Bearer dk_…
Keys are issued by whoever runs this API (node admin.mjs new-key <name>). Keep them on your server, never in a web page.
POST /v1/addresses
curl -X POST https://api.arcdoorstep.duckdns.org/v1/addresses \
-H "Authorization: Bearer $DOORSTEP_KEY" -H "content-type: application/json" \
-d '{
"into": "0xYourVault",
"beneficiary": "0xCustomer",
"refundTo": "0xCustomer",
"amount": "25",
"reference": "inv-1042",
"expires": "7d",
"webhookUrl": "https://yourapp.com/hooks/doorstep"
}'
| Field | Meaning |
|---|---|
into | Your contract. It must have deposit(uint256 amount, address beneficiary) and take the funds with transferFrom (any ERC-4626 vault works). |
beneficiary | Who your contract credits. |
refundTo | Where money goes if it can't be delivered. This wallet can also cancel at any time. Never an exchange deposit address. |
amount | Optional. Ask for an exact amount (an invoice): nothing is delivered until all of it has arrived. |
minimum | Optional, instead of amount: deliver once at least this much has arrived. Default 0. |
expires | Optional: "45m", "12h", "30d", or unix seconds. Default 30 days. After this, anything waiting is returned. |
keeperFeeCap | Optional: the most the delivery may cost the payment, in USDC. Default "0.01". Delivery bots skip addresses that can't repay their gas. |
reference | Optional: your order or invoice id, up to 31 characters. Shown on the pay page and in every event. |
webhookUrl | Optional: we POST here when a payment is delivered or returned. |
call | Advanced, instead of beneficiary: {"abi": [...], "functionName": "topUp", "args": ["AMOUNT", "0x…"]}. "AMOUNT" marks where the arrived amount goes. |
Response 201:
{
"address": "0x6d97…0d58", // show this, or a QR code; payers send USDC on Arc here
"payLink": "https://…/#i=…", // a ready pay page with QR code and live status
"description": "USDC sent to this address is deposited into 0x12…34 for 0x56…78. …",
"reference": "inv-1042",
"expiresAt": "2026-10-19T12:00:00.000Z",
"registered": true, // delivery bots will pick payments up automatically
"registerTxUrl": "https://explorer.arc.io/tx/0x…",
"invoice": { "state": "unpaid", "expected": "25", "received": "0", "remaining": "25", "extra": "0" },
"intent": { … } // the exact on-chain instructions behind the address
}
The same instructions always give the same address; repeating a request returns the existing one.
GET /v1/addresses/{address}
{
"state": "delivered", // waiting | part_paid | below_minimum | delivering | delivered | returned | cancelled | returning | expired
"waiting": "0", // USDC sitting at the address right now
"invoice": { "state": "paid", "received": "25", "remaining": "0", … },
"events": [ { "type": "delivered", "amount": "24.995", "networkFee": "0.003", "txUrl": "https://explorer.arc.io/tx/0x…" } ]
}
GET /v1/addresses?reference=inv-1042 lists your addresses (latest 100).
When a payment to one of your addresses is delivered or returned, we POST JSON to its webhookUrl:
{
"id": "evt_…",
"type": "payment.delivered", // or "payment.returned"
"createdAt": "2026-10-12T20:31:00.000Z",
"data": { "address": "0x…", "reference": "inv-1042", "amount": "24.995", "networkFee": "0.003", "tx": "0x…", "txUrl": "…" }
}
Answer with any 2xx. Failed deliveries are retried after 30 s, 2 min, 10 min, 30 min and 2 h. Always check the signature:
// Node.js
import crypto from 'node:crypto';
function verify(rawBody, headers, secret /* whsec_… */) {
const ts = headers['x-doorstep-timestamp'];
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const given = headers['x-doorstep-signature'] ?? '';
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
Errors look like {"error": {"code": "invalid", "message": "refundTo must be an address"}}. Codes: unauthorized (401), invalid, bad_json, bad_webhook (400), not_found (404), exists (409), rate_limited (429), register_failed (502).
GET /v1/health (no key needed) shows the chain, the Doorstep contract and whether registration is on.
Doorstep is open source and unaudited; use small amounts. github.com/phoenix-2203/doorstep