Accept Fonepay payments with one API call
Create a payment from your server, send your customer to the hosted checkout (or show the QR yourself), and get a signed webhook when the money lands in your Fonepay account.
- Create a payment with your API key — you get a
checkoutUrland a QR payload. - Show the checkout to your customer. They scan with any Fonepay-enabled app.
- Receive a
payment.paidwebhook once AstraCloud Pay has verified the payment with Fonepay.
Base URL: https://pay.astracloud.com.np. All amounts are integers in paisa (Rs 1 = 100 paisa), so Rs 500 is 50000. Times are ISO 8601 in UTC.
Authentication
Every server-to-server request carries your secret API key in the X-Api-Key header. Find it in Dashboard → Developers. Keep it on your server — anyone with the key can create payments on your account. If it leaks, rotate it from the same page; the old key stops working immediately.
X-Api-Key: ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Create a payment
/api/payments| Field | Type | Description |
|---|---|---|
amountPaisa | integer | Required. Amount in paisa. Maximum 100,000,000 (Rs 10,00,000). |
description | string | Shown to the customer on the checkout. Up to 255 characters. |
customerName | string | For your records and search. Up to 160 characters. |
customerPhone | string | For your records and search. Up to 32 characters. |
curl -X POST https://pay.astracloud.com.np/api/payments \
-H "X-Api-Key: $ASTRAPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amountPaisa": 50000,
"description": "Order #1042 — 2 momo plates",
"customerName": "Sita Sharma",
"customerPhone": "9800000000"
}'{
"publicRef": "ORDER-7Q2KX9MB",
"internalReference": "PRN-4H8K2M9QX7TW3A",
"qrPayload": "…",
"amountPaisa": 50000,
"expiresAt": "2026-09-24T10:05:00.000Z",
"status": "QR_CREATED",
"checkoutUrl": "https://pay.astracloud.com.np/pay/?ref=PRN-4H8K2M9QX7TW3A"
}The customer has 5 minutes to pay. After that the payment becomes EXPIRED and nothing can be charged — create a new one.
Retrieve a payment
/api/payments/{internalReference}Returns the payment with its customer details and the full verification timeline. Only payments on your own account are returned.
curl https://pay.astracloud.com.np/api/payments/PRN-4H8K2M9QX7TW3A -H "X-Api-Key: $ASTRAPAY_KEY"
{
"internal_reference": "PRN-4H8K2M9QX7TW3A",
"fonepay_reference": "FP-88213377",
"status": "PAID",
"amount_paisa": 50000,
"public_ref": "ORDER-7Q2KX9MB",
"description": "Order #1042 — 2 momo plates",
"customer_name": "Sita Sharma",
"customer_phone": "9800000000",
"paid_at": "2026-09-24T10:02:41.000Z",
"verified_at": "2026-09-24T10:02:41.000Z",
"timeline": [
{ "action": "create_qr", "ok": 1, "detail": "provider=live", "created_at": "…" },
{ "action": "verify", "ok": 1, "detail": "provider says PAID", "created_at": "…" }
]
}Checkout page
Redirect or link the customer to checkoutUrl. The hosted page shows your business name, the amount, the QR, a live countdown and the result — on any phone, with no login. It updates by itself when the payment is confirmed.
Prefer your own UI? Render qrPayload as a QR code yourself and poll GET /api/payments/{ref}/status (public, read-only) — but always treat the webhook or an authenticated retrieve as the source of truth before you ship goods.
Payment statuses
| Status | Meaning | Final? |
|---|---|---|
QR_CREATED | QR is ready; waiting for the customer to scan. | No |
PROCESSING | Fonepay reported a payment; we’re confirming amount and reference. | No |
PAID | Verified with Fonepay. The money is in your account. Safe to fulfil. | Yes |
EXPIRED | Not paid within 5 minutes. Nothing was charged. | Yes |
FAILED | The payment was declined or couldn’t be verified. | Yes |
A payment only becomes PAID after backend verification of the amount and Fonepay reference. A screenshot or a client-side redirect never marks a payment paid.
Webhooks
Add an HTTPS endpoint in Dashboard → Developers. When a payment is verified we send a POST with a JSON body. Respond with any 2xx within 8 seconds. Each payment is settled once, so you receive payment.paid once per payment — still, make your handler idempotent on data.reference.
POST /webhooks/astrapay
X-AstraPay-Event: payment.paid
X-AstraPay-Signature: t=1790244161,v1=5f2c…e91a
{
"id": "evt_9b1c0f4a2e7d3b6c8a10",
"type": "payment.paid",
"created": "2026-09-24T10:02:41.512Z",
"data": {
"reference": "PRN-4H8K2M9QX7TW3A",
"order": "ORDER-7Q2KX9MB",
"amountPaisa": 50000,
"currency": "NPR",
"description": "Order #1042 — 2 momo plates",
"customer": { "name": "Sita Sharma", "phone": "9800000000" },
"fonepayReference": "FP-88213377",
"paidAt": "2026-09-24T10:02:41.000Z"
}
}Use Send test event on the Developers page to receive a webhook.test event. Every delivery, with its HTTP result, is listed there.
Verify signatures
The signature is HMAC-SHA256(secret, "<t>.<raw body>") in hex, where secret is your webhook signing secret (whsec_…). Compute it over the raw request body, compare in constant time, and reject timestamps older than 5 minutes.
import crypto from 'node:crypto';
import express from 'express';
app.post('/webhooks/astrapay', express.raw({ type: 'application/json' }), (req, res) => {
const parts = Object.fromEntries(req.get('x-astrapay-signature').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.ASTRAPAY_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid = expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!fresh || !valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === 'payment.paid') {
// look up event.data.reference and mark the order paid (idempotently)
}
res.sendStatus(200);
});$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_ASTRAPAY_SIGNATURE']), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, getenv('ASTRAPAY_WEBHOOK_SECRET'));
if (abs(time() - (int)$sig['t']) > 300 || !hash_equals($expected, $sig['v1'])) {
http_response_code(400); exit;
}
$event = json_decode($body, true);Errors
Errors return JSON with a single readable error message.
| Code | When |
|---|---|
400 | A field is missing or invalid — the message names the field. |
401 | Missing or wrong X-Api-Key. |
404 | The payment doesn’t exist on your account. |
429 | Too many sign-in attempts — wait and retry. |
500 | Something failed on our side. Safe to retry creating a payment. |
{ "error": "amountPaisa: Number must be greater than 0" }Sandbox testing
Until your account is connected to a live Fonepay gateway, payments run in sandbox: QR codes can’t be paid with a real app, and no money moves. Simulate a successful scan to exercise your whole flow — status changes, wallet credit and the payment.paid webhook all behave exactly as in live mode.
/api/sandbox/mark-paid/{internalReference}curl -X POST https://pay.astracloud.com.np/api/sandbox/mark-paid/PRN-4H8K2M9QX7TW3A
You can also click Simulate payment in a payment’s details in the dashboard. This endpoint doesn’t exist in live mode.