Developer quickstart
Install the SDK, create an invoice, listen for the webhook — ten minutes end-to-end.
This is the developer integration guide. If you're a tester just trying the flow in a browser, the 10-minute tester quickstart is faster.
Prerequisites
- Node.js 20 or higher
- A merchant account on
arcorapay.xyz/m/login(sign in once with your wallet, register your payout token) - An API key (created at
/m/settingsonce registered)
1. Install the SDK
npm install @arcora/sdk
# or, for React:
npm install @arcora/sdk-react2. Create an invoice
import { Arcora } from '@arcora/sdk';
const arcora = new Arcora({ apiKey: process.env.ARCORA_API_KEY });
const invoice = await arcora.createInvoice({
amount: '49.99',
currency: 'EURC',
successUrl: 'https://yourshop.com/order/123/success',
cancelUrl: 'https://yourshop.com/order/123/cancel',
metadata: { orderId: '123' },
idempotencyKey: 'order-123',
});
// Send the customer here:
console.log(invoice.url);
// https://arcorapay.xyz/i/0x4f3a...amount is the gross charge as a decimal string in major units (no floats). currency is the stablecoin you're paid out in (default USDC) — the buyer always locks USDC and Arcora handles the FX. Pass an idempotencyKey so a retried create never duplicates the invoice.
3. Receive the webhook
Configure a webhook URL in /m/settings. Every delivery is dual-signed with the secret you set there: a legacy X-Arcora-Signature and a timestamp-bound X-Arcora-Signature-V2 (paired with X-Arcora-Timestamp) for replay protection. The SDK (≥ 1.3.0) ships an official verifier at @arcora/sdk/webhook that checks the V2 signature in constant time and rejects anything outside a ±300s replay window. It is fail-closed — a missing or mismatched signature returns false rather than throwing (see Webhooks for details).
import { verifyWebhook } from '@arcora/sdk/webhook';
export async function POST(req: Request) {
const rawBody = await req.text();
const ok = verifyWebhook({
body: rawBody, // raw string — never a re-stringified JSON object
signature: req.headers.get('x-arcora-signature-v2') ?? '', // timestamp-bound, replay-protected
timestamp: req.headers.get('x-arcora-timestamp') ?? '',
secret: process.env.ARCORA_WEBHOOK_SECRET!,
});
if (!ok) return new Response('Bad signature', { status: 400 }); // fail-closed
const event = JSON.parse(rawBody);
switch (event.type) {
case 'invoice.paid':
// event.invoice_id, event.paid_by, event.tx_hash
break;
case 'invoice.refunded':
break;
case 'compliance.review_queued':
break;
}
return new Response('ok');
}On a runtime without the SDK, reproduce the same check with any HMAC-SHA256 primitive over <timestamp>.<rawBody> and a constant-time compare — the equivalent node:crypto snippet is in Webhooks.
4. Test it
Open the invoice URL in an incognito window with a different funded wallet. Sign the Permit2 prompt. Within 30 seconds:
- Customer sees "Paid ✓"
- Your webhook endpoint receives
invoice.paid - The settled amount lands in your merchant wallet
Refund flows the same way — trigger it from the invoice row in /m/dashboard. (There's no SDK refund method; refunds are merchant-dashboard or direct-contract operations.)
Common pitfalls
- CORS —
/api/invoicesis CORS-open by design. The hosted checkout doesn't need your origin allowlisted. - Quote expiry — Hosted checkout shows a TTL on the quote; if it expires, the customer has to refresh. SDK quotes are advisory; the actual rate is locked at
kit.swaptime. - Refund window — The custody-escrow gateway holds each settled invoice in per-invoice escrow for 7 days. Refunds drain directly from the escrow — no ERC-20 allowance from the merchant wallet is required. The window is soft: after 7 days anyone can call
claim(globalIds[])to release the matured funds to the merchant payout address, but a refund stays callable until that claim actually lands — whichever transaction confirms first wins. Once claimed, the refund path closes.