Doorstep API

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.

Authentication

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.

Create a payment address

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"
  }'
FieldMeaning
intoYour contract. It must have deposit(uint256 amount, address beneficiary) and take the funds with transferFrom (any ERC-4626 vault works).
beneficiaryWho your contract credits.
refundToWhere money goes if it can't be delivered. This wallet can also cancel at any time. Never an exchange deposit address.
amountOptional. Ask for an exact amount (an invoice): nothing is delivered until all of it has arrived.
minimumOptional, instead of amount: deliver once at least this much has arrived. Default 0.
expiresOptional: "45m", "12h", "30d", or unix seconds. Default 30 days. After this, anything waiting is returned.
keeperFeeCapOptional: the most the delivery may cost the payment, in USDC. Default "0.01". Delivery bots skip addresses that can't repay their gas.
referenceOptional: your order or invoice id, up to 31 characters. Shown on the pay page and in every event.
webhookUrlOptional: we POST here when a payment is delivered or returned.
callAdvanced, 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.

Check an address

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

Webhooks

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

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

Health

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