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:
- your payout address — a bech32m
anim1…address you control. It is validated against the chain rules and a0x1002SPHINCS+ address is rejected, because those addresses cannot spend; - your confirmation depth — default 12, which is the depth this chain’s own payment watchers use.
2. Get your keys #
Four key shapes exist, and the split is a safety boundary rather than cosmetics:
| Prefix | Kind | Where it may appear |
|---|---|---|
apk_test_… | publishable, test | browser, HTML, mobile app |
apk_live_… | publishable, live | browser, HTML, mobile app |
ask_test_… | secret, test | server only |
ask_live_… | secret, live | server 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.
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:
{
"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.
// 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.
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 #
| Step | Event | Intent status |
|---|---|---|
| Intent created | payment.created | AWAITING_PAYMENT |
| Transfer seen in a block | payment.detected | PAYMENT_DETECTED |
| Depth accumulating | payment.confirming | CONFIRMING |
| Depth reached, receipt OK | payment.confirmed | PAID |