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({
  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/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.