Qora by Laayo
Get API keys

Create a payment

POST /v1/payments — creates a payment and mints a Fonepay dynamic QR against your merchant account.

Parameters

NameTypeDescription
amount required integer Paisa. 25000 is रु 250.00. Minimum 1000 (रु 10.00). Decimals are rejected.
currencystringOnly NPR. Defaults to NPR.
referencestringYour own order or invoice number. Searchable, and returned on the payment.
descriptionstringHuman-readable note for your dashboard.
remarks1stringShown in the payer's bank app. Defaults to your reference. Max 50 characters.
remarks2stringSecond remark line. Defaults to your business name.
metadataobjectUp to 20 string key/value pairs, returned on the payment and on every webhook.

Amounts are integer paisa. Sending 250 when you mean रु 250.00 charges रु 2.50. Sending 250.00 is rejected outright rather than silently rounded — we would rather fail your request than take the wrong amount from a customer.

Request

curl
curl https://qora.laayo.com.np/v1/payments \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Idempotency-Key: order-1042" \
  -d amount=25000 \
  -d "reference=Order #1042" \
  -d "metadata[table]=4"
PHP
$response = $client->post('https://qora.laayo.com.np/v1/payments', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('QORA_SECRET_KEY'),
        'Idempotency-Key' => 'order-' . $order->id,
    ],
    'json' => [
        'amount' => $order->total_paisa,
        'reference' => 'Order #' . $order->id,
        'metadata' => ['table' => $order->table],
    ],
]);

$payment = json_decode($response->getBody(), class="tok-key">true);
Node
const res = await fetch('https://qora.laayo.com.np/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.QORA_SECRET_KEY}`,
    'Idempotency-Key': `order-${order.id}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: order.totalPaisa,        // integer paisa
    reference: `Order #${order.id}`,
  }),
});

const payment = await res.json();

Response

201 Created
{
  "id": "pay_7Fh2kQ9mXwRt3vNbLp6sDc",
  "object": "payment",
  "mode": "test",
  "amount": 25000,
  "amount_formatted": "250.00",
  "currency": "NPR",
  "status": "requires_payment",
  "prn": "Q260907091500A3KD9WQZ",
  "description": null,
  "reference": "Order #1042",
  "remarks1": "Order 1042",
  "remarks2": "Himalayan Coffee House",
  "metadata": { "table": "4" },
  "qr": {
    "payload": "00020101021230590016A00555555555550208...",
    "expires_at": "2026-09-07T09:20:00+05:45"
  },
  "fonepay": { "trace_id": null, "status": null, "verified_at": null },
  "payer": {},
  "fee": 200,
  "succeeded_at": null,
  "created_at": "2026-09-07T09:15:00+05:45"
}

Statuses

StatusMeaning
requires_paymentQR minted, waiting for the customer to scan and pay.
processingA bank event arrived but is not yet final. Transient.
succeededConfirmed and final. The money is on its way to your bank.
failedThe QR could not be minted, or the payment was declined. Terminal.
expiredThe QR was never paid before it expired. Terminal.
canceledYou canceled it. Terminal.

succeeded is set only after Qora has independently verified the payment. An event arriving is treated as a signal to go and check, never as proof. That is why this field is worth trusting — and why nothing earlier in the chain is.

Errors

StatusCodeMeaning
422parameter_invalidA parameter failed validation; see error.errors.
422credentials_missingNo usable Fonepay credentials for this mode.
502gateway_unavailableFonepay declined or did not answer. Safe to retry with the same idempotency key.
429rate_limit_exceededToo many QR requests this minute.