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/settings once registered)

1. Install the SDK

npm install @arcora/sdk
# or, for React:
npm install @arcora/sdk-react

2. 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/invoices is 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.swap time.
  • 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.