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({
amountUsdc: 49.99,
payInToken: 'EURC',
successUrl: 'https://yourshop.com/order/123/success',
cancelUrl: 'https://yourshop.com/order/123/cancel',
metadata: { orderId: '123' },
});
// Send the customer here:
console.log(invoice.url);
// https://arcorapay.xyz/i/0x4f3a...amountUsdc is the gross amount your customer will pay, denominated in your payout-token's USD-equivalent. The pay-in token is what the customer pays with — Arcora handles the FX.
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. Both header values carry a sha256= prefix — compare against the whole value. Prefer V2 when present and reject deliveries outside a ±300s window. Verification is plain node:crypto — deliberately not an SDK method, so it works in any runtime (see Webhooks for details).
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300; // ±5 min replay window
function safeEqual(a: string, b: string) {
const ab = Buffer.from(a), bb = Buffer.from(b);
return ab.length === bb.length && timingSafeEqual(ab, bb);
}
function verifyWebhook(headers: Headers, rawBody: string, secret: string) {
const sigV2 = headers.get('x-arcora-signature-v2');
const ts = headers.get('x-arcora-timestamp');
if (sigV2 && ts) { // prefer V2: timestamp-bound, replay-protected
const n = Number(ts);
if (!Number.isFinite(n) || Math.abs(Date.now() / 1000 - n) > TOLERANCE_SECONDS) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
return safeEqual(expected, sigV2);
}
const sig = headers.get('x-arcora-signature') ?? ''; // legacy fallback (no replay protection)
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
return safeEqual(expected, sig);
}
export async function POST(req: Request) {
const rawBody = await req.text();
if (!verifyWebhook(req.headers, rawBody, process.env.ARCORA_WEBHOOK_SECRET!)) {
return new Response('Bad signature', { status: 400 });
}
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');
}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.