Payment Gateway
The payment-gateway module is the integration layer with Amdigipay (brand name of Bisabiller). FE uses two read-only endpoints: list payment channels (VA bank, QRIS, e-wallet, retail) and detail of one PaymentOrder. Payment initiation itself is done via the payments module (see document 17). This module also has a callback endpoint from Amdigipay → Moria (vendor webhook) that is not called by FE — documented in the last slide for awareness.
| Property | Value |
|---|---|
| Base URL | {HOST}/v1 |
| Auth | Bearer JWT (header Authorization) or cookie access_token |
| Content-Type | application/json |
| Error envelope | { "message": string | string[], "statusCode": number, "error": string } |
| Validation | Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true |
| Related modules | payments, accounts, saving-goals, charitable-cause, commodity-financing |
| Document version | v1 · 2026-05-20 |
| Audience | Internal FE devs (mobile + web) |
Summary
Section titled “Summary”FE typically uses GET /channels to populate the payment method picker, then uses the endpoint in the payments module to initiate payment. After the payment is created, FE polls status with GET /orders/:id while waiting for the Amdigipay callback to trigger the final state transition on the server.
| Method | Path | Summary |
|---|---|---|
| GET | /v1/payment-gateway/channels | List active payment channels (cached 5 minutes) |
| GET | /v1/payment-gateway/orders/:id | User’s PaymentOrder detail |
| POST | /v1/payment-gateway/callback | Vendor webhook (NOT called by FE) |
GET /v1/payment-gateway/channels bearer
Section titled “GET /v1/payment-gateway/channels ”List active payment channels from Amdigipay (VA bank, QRIS, e-wallet, retail), each with a fee model and minimum amount. Cached server-side for 5 minutes. Use this response to populate the payment method picker in FE.
bearerResponse — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "Channels fetched", "data": { "channels": [ { "payment_id": 22, "name": "VA BCA", "type_fee": "nominal", "minimum_amount": 4000, "admin_fee": 4000 }, { "payment_id": 33, "name": "QRIS", "type_fee": "percent", "minimum_amount": 1500, "admin_fee": 1 } ] }}Field mapping: payment_id → use as payment_id in POST /v1/payments/gateway. type_fee: 'nominal' means admin_fee is a flat IDR amount, 'percent' means percentage points (e.g. 1 = 1%). minimum_amount is the minimum amount that Amdigipay accepts on that channel.
Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
401 Unauthorized | Bearer/cookie invalid |
GET /v1/payment-gateway/orders/:id bearer
Section titled “GET /v1/payment-gateway/orders/:id ”Detail of a single PaymentOrder belonging to the logged-in user. Orders that do not belong to the user return 404 (indistinguishable from non-existent orders). FE can render the entire payment status screen from this response without additional fetches.
bearerPath params
Section titled “Path params”| Param | Type | Notes |
|---|---|---|
id | UUID | PaymentOrder UUID (the id column, not order_number). Validated by ParseUUIDPipe. |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "Payment order fetched", "data": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "order_number": "PAY-1ce9ab3d-9b1c-4d2f-9b6a-1d2c3a4f5e6b", "status": "waiting_payment", "bisabiller_id": 18293012, "bisabiller_status_id": 4, "bisabiller_status": "Completed", "reference_type": "account_top_up", "reference_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "organization_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "payment_id": 22, "payment_name": "VA BCA", "nominal": "200000.0000", "internal_admin_fee": "0.0000", "payment_gateway_fee": "4500.0000", "total_amount": "204500.0000", "expired_date": "2026-04-18 23:59:59", "payment_link": null, "payment_code": "8808081234567890", "qr_code": null, "customer_name": "John Doe", "customer_email": "john@example.com", "customer_number": "+628123456789", "item_details": [ { "item_id": "bisabiller-admin-fee", "item_name": "Payment gateway admin fee", "item_price": 4500, "item_quantity": 1, "item_total_price": 4500 } ], "last_callback_at": "2026-04-17T14:35:12.000Z", "created_at": "2026-04-17T14:30:00.000Z", "updated_at": "2026-04-17T14:30:00.000Z" }}status lifecycle: PENDING → WAITING_PAYMENT → COMPLETED | FAILED | CANCELLED | REFUNDED. States ON_HOLD and PROCESSING are intermediate. Money fields (nominal, total_amount, etc.) are decimal strings.
Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | id is not a UUID |
401 Unauthorized | Bearer/cookie invalid |
404 Not Found | Order missing or not owned by user (intentionally indistinguishable) |
Vendor callback (not called from FE)
Section titled “Vendor callback (not called from FE)”POST /v1/payment-gateway/callback public
Section titled “POST /v1/payment-gateway/callback ”Inbound webhook from Amdigipay. Called automatically every time a payment changes state (pending → completed / failed / refunded). Always returns 200 to prevent Amdigipay retries and for anti-leak signature.
publicRequest body — BisabillerCallbackDto
Section titled “Request body — BisabillerCallbackDto”| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | ✓ | Amdigipay internal transaction id |
transaction_id | string | ✓ | Transaction id created when the payment was created (PAY-<uuid>) |
transaction_total | string | ✓ | Total paid (nominal + admin_fee), serialized as string |
signature | string | ✓ | HMAC signature for payload verification |
payment_id | integer | ✓ | Channel id (e.g. 22=VA BCA, 23=VA MANDIRI, 33=QRIS, 37=GOPAY) |
payment | string | ✓ | Human-readable channel name |
status_id | integer | ✓ | 4=Completed, 2=On processing, 14=Failed, 6=Refund |
status | string | ✓ | Human-readable status label |
expired_date | string | optional | YYYY-MM-DD HH:mm:ss (only appears on some callbacks) |
Response — 200 OK
Section titled “Response — 200 OK”{ "error": false, "message": "received" }Anti-leak behavior
Section titled “Anti-leak behavior”- Signature verification failure → return 200 silently
- Duplicate event (already-processed
transaction_id + status_id) → return 200 silently - Only a malformed body (DTO validation failed) returns 400
Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | DTO validation failed (missing required field, wrong type) — global ValidationPipe |
Side effects
Section titled “Side effects”- Updates
PaymentOrder.status,bisabiller_status_id,bisabiller_status,last_callback_at. - Emits an event to the payment handler listener (account top-up, saving goal contribution, etc.) to apply the final domain effect.
Reference
Section titled “Reference”Enum: PaymentOrderStatus
Section titled “Enum: PaymentOrderStatus”pending— local row saved, Amdigipay not yet calledwaiting_payment— waiting for user to pay VA / scan QRprocessing— Amdigipay is validatingcompleted— terminal: successcancelled— terminal: cancelledrefunded— terminal: refundon_hold— funds held, manual intervention requiredfailed— terminal: failed
Common Amdigipay status_id
Section titled “Common Amdigipay status_id”2— On processing4— Completed6— Refund14— Failed
Standard error envelope
Section titled “Standard error envelope”{ "message": "Payment order f47ac10b-58cc-4372-a567-0e02b2c3d479 not found", "statusCode": 404, "error": "Not Found"}Common HTTP codes
Section titled “Common HTTP codes”400validation (invalid UUID, unknown channel id)401Bearer invalid404order missing / not owned by user500internal — generic toast
Polling status
Section titled “Polling status”- FE polls
GET /orders/:idevery 3-5 seconds while the state is not terminal. - Stop polling when
status∈{completed, failed, cancelled, refunded}. - The
last_callback_atfield indicates the latest Amdigipay callback has been processed.