---
name: mayar-id-payments
description: "Add Mayar.id (Indonesian payment gateway) payments to a SaaS: invoice checkout for any quantity, payment.received webhook, and self-managed monthly renewals in IDR. Use when the user mentions Mayar, mayar.id, mayar.club, Indonesian payments, QRIS, virtual account or e-wallet billing, or selling seats/slots/plans monthly in Rupiah."
---

# Mayar.id payments for a SaaS

Mayar is an Indonesian payment gateway: prices in Rupiah, QRIS, bank virtual accounts and e-wallets. Full guide: https://ahmadrosid.com/blog/integrating-mayar-id-payments

## When to use

- Monthly paid access in IDR (seats, workspaces, plans, credit packs) through Mayar.
- Wiring Mayar's `payment.received` webhook into what a customer can use.

## When not to use

- One-off sales with nothing to unlock later: a Mayar payment link is enough.

## Non-negotiables

1. Use `POST /invoices/create` (plural) with `items[{quantity, rate, description}]`. Avoid Mayar Membership when a customer may buy more than one: it allows one registration per email per tier and has no quantity.
2. Send the customer's real email, unmodified.
3. Insert pending seats and a `billing_orders` row before calling Mayar. If Mayar errors, mark the order failed and its seats inactive.
4. Save `id` (invoice), `transactionId` and `link` from the response. The webhook's `data.transactionId` equals that `transactionId`.
5. Unlock only after `payment.received` with `data.status: "SUCCESS"` AND `GET /invoices/{invoiceId}` reporting `paid`. Never on the redirect back.
6. Apply an order exactly once with a conditional `pending|expired → paid` update.
7. Check the webhook's `?token=` shared secret; Mayar does not sign requests.
8. `mobile` is required (at least 10 characters). Ask at checkout only if the account has no phone; store it on the order for renewals.
9. Set `redirectUrl` to the app's dashboard with a "payment received" message, and `extraData.orderId` to the local order id.
10. Run one background sweep every ~10 minutes on a single instance: poll pending orders from the last 48 h, expire overdue orders, expire lapsed seats, then send one renewal invoice per customer for seats ending within 7 days.

## Environment

| Variable | Purpose |
|---|---|
| `MAYAR_API_KEY` | Read & Write key; needed by the web app and the background job |
| `MAYAR_API_BASE` | `https://api.mayar.club/hl/v2` (sandbox) or `https://api.mayar.id/hl/v2` (production) |
| `MAYAR_WEBHOOK_TOKEN` | Shared secret in the webhook URL's `?token=` |
| `APP_URL` | Base for `redirectUrl` |

## Build order

1. Schema: `seats` (`pending|active|inactive`, `current_period_end`) and `billing_orders` (`kind`, `quantity`, `seat_ids`, customer email/name/mobile, Mayar invoice and transaction ids, `invoice_url`, `invoice_expires_at`, `status`, `paid_at`).
2. Client: `mayarFetch` (Bearer auth, unwrap `data`, surface `data[].message` on validation errors), `createInvoice`, `getInvoice`.
3. Checkout: quantity picker with a live total, phone field when needed, redirect to `link`.
4. Webhook route: token check → find order by transaction id → `getInvoice` → apply once → 200.
5. Background sweep with the four steps above, settling payments before expiring anything.
6. Dashboard banner for a pending renewal, linking to `invoice_url`.
7. Tests: N seats in one order, Mayar error releases seats, duplicate webhook, fake unpaid webhook, renewal paid early and late, one renewal per customer, expiry.

## API shapes

```http
POST {MAYAR_API_BASE}/invoices/create
Authorization: Bearer {MAYAR_API_KEY}

{
  "name": "...", "email": "...", "mobile": "081234567890",
  "description": "...", "expiredAt": "2026-10-19T04:22:45.000Z",
  "redirectUrl": "https://app.example/dashboard?billing=success",
  "items": [{ "quantity": 3, "rate": 100000, "description": "Seat (1 month)" }],
  "extraData": { "orderId": "..." }
}
→ data: { id, transactionId, link, expiredAt, extraData }

GET {MAYAR_API_BASE}/invoices/{id}
→ data: { status: "unpaid" | "paid", transactionId, ... }
```

Webhook `payment.received` for an invoice: `data.transactionId` (same as create), `data.productId` (invoice id), `data.productType: "invoice"`, `data.status: "SUCCESS"`, `data.extraData`.

## Output expectations

1. Propose a file map for this repo first (client, checkout, webhook route, schema/migration, background job, UI).
2. Use the repo's existing stack and keep Mayar code in one billing module.
3. Finish with the env vars to set for each process and the exact webhook URL to register.
