Animica Pay docs 2.00% per successful payment

Start here

Quickstart

Accept your first ANM payment in about ten minutes.

Ten minutes, five steps, one webhook. Everything here works in test mode, which never moves real ANM.

1. Create an account and a merchant #

Sign up at pay.animica.dev, then set:

2. Get your keys #

Four key shapes exist, and the split is a safety boundary rather than cosmetics:

PrefixKindWhere it may appear
apk_test_…publishable, testbrowser, HTML, mobile app
apk_live_…publishable, livebrowser, HTML, mobile app
ask_test_…secret, testserver only
ask_live_…secret, liveserver only

A secret key is displayed once. We store only a keyed hash of it, so we cannot show it to you again and a stolen database dump cannot be used to authenticate. If you lose it, rotate it.

3. Create a PaymentIntent #

Server-side, with the secret key. Send an Idempotency-Key — your own order id is the right value.

bash
curl -sS https://pay.animica.dev/api/v1/payment-intents \
  -H "Authorization: Bearer $ANIMICA_PAY_SECRET_KEY" \
  -H "Idempotency-Key: order_19482" \
  -H "Content-Type: application/json" \
  -d '{"amount":"49.99","currency":"USD","merchant_order_id":"19482",
       "description":"Order #19482",
       "success_url":"https://shop.example/thanks",
       "cancel_url":"https://shop.example/cart"}'

The response carries the id, the exact nANM amount and the checkout URL:

json
{
  "id": "pay_X9m2ABC",
  "status": "AWAITING_PAYMENT",
  "anm_amount": "41825250000000",
  "checkout_url": "https://pay.animica.dev/c/pay_X9m2ABC",
  "expires_at": "2026-08-07T15:10:00Z"
}

4. Send the customer to checkout #

Redirect to checkout_url. The hosted page shows the amount, the payment address, the ANMPAY1 reference the wallet must attach, and live status as the transfer confirms.

js
// Node, no SDK required — one fetch and a redirect.
const res = await fetch('https://pay.animica.dev/api/v1/payment-intents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ANIMICA_PAY_SECRET_KEY}`,
    'Idempotency-Key': `order_${order.id}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: order.total.toFixed(2),   // a decimal STRING, never a float on the wire
    currency: 'USD',
    merchant_order_id: String(order.id),
  }),
});
if (!res.ok) throw new Error(`intent failed: ${res.status}`);
const intent = await res.json();
reply.redirect(303, intent.checkout_url);

5. Handle the webhook #

This is the only thing that should mark an order paid. Verify the signature before you look at the body, and make the handler idempotent — a retry is normal traffic, not an exception.

js
const { verifyAnimicaWebhook } = require('./verify-animica-webhook');

// The RAW body is required. Do not let a JSON body parser consume it first:
//   express.raw({ type: 'application/json' })
app.post('/webhooks/animica', express.raw({ type: 'application/json' }), (req, res) => {
  const check = verifyAnimicaWebhook({
    rawBody: req.body.toString('utf8'),
    headers: req.headers,
    secrets: [process.env.ANIMICA_WEBHOOK_SECRET],
  });
  if (!check.ok) return res.status(400).json({ error: check.reason });

  const event = JSON.parse(req.body.toString('utf8'));
  if (alreadyProcessed(event.id)) return res.status(200).json({ ok: true });

  if (event.type === 'payment.confirmed') {
    // Idempotent by construction: only mark unpaid orders paid.
    markOrderPaidOnce(event.data.merchant_order_id, event.data.transaction_hash);
  }
  remember(event.id);
  res.status(200).json({ ok: true });
});

The full verifier is on the Webhooks page, in Node and PHP.

What you should see #

StepEventIntent status
Intent createdpayment.createdAWAITING_PAYMENT
Transfer seen in a blockpayment.detectedPAYMENT_DETECTED
Depth accumulatingpayment.confirmingCONFIRMING
Depth reached, receipt OKpayment.confirmedPAID