Skip to content

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.

PropertyValue
Base URL{HOST}/v1
AuthBearer JWT (header Authorization) or cookie access_token
Content-Typeapplication/json
Error envelope{ "message": string | string[], "statusCode": number, "error": string }
ValidationGlobal ValidationPipe · whitelist: true, forbidNonWhitelisted: true
Related modulespayments, accounts, saving-goals, charitable-cause, commodity-financing
Document versionv1 · 2026-05-20
AudienceInternal FE devs (mobile + web)

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.

MethodPathSummary
GET/v1/payment-gateway/channelsList active payment channels (cached 5 minutes)
GET/v1/payment-gateway/orders/:idUser’s PaymentOrder detail
POST/v1/payment-gateway/callbackVendor 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.

bearer
{
"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.

StatusWhen it occurs
401 UnauthorizedBearer/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.

bearer
ParamTypeNotes
idUUIDPaymentOrder UUID (the id column, not order_number). Validated by ParseUUIDPipe.
{
"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.

StatusWhen it occurs
400 Bad Requestid is not a UUID
401 UnauthorizedBearer/cookie invalid
404 Not FoundOrder missing or not owned by user (intentionally indistinguishable)

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.

public
FieldTypeRequiredNotes
idintegerAmdigipay internal transaction id
transaction_idstringTransaction id created when the payment was created (PAY-<uuid>)
transaction_totalstringTotal paid (nominal + admin_fee), serialized as string
signaturestringHMAC signature for payload verification
payment_idintegerChannel id (e.g. 22=VA BCA, 23=VA MANDIRI, 33=QRIS, 37=GOPAY)
paymentstringHuman-readable channel name
status_idinteger4=Completed, 2=On processing, 14=Failed, 6=Refund
statusstringHuman-readable status label
expired_datestringoptionalYYYY-MM-DD HH:mm:ss (only appears on some callbacks)
{ "error": false, "message": "received" }
  • 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
StatusWhen it occurs
400 Bad RequestDTO validation failed (missing required field, wrong type) — global ValidationPipe
  • 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.

  • pending — local row saved, Amdigipay not yet called
  • waiting_payment — waiting for user to pay VA / scan QR
  • processing — Amdigipay is validating
  • completed — terminal: success
  • cancelled — terminal: cancelled
  • refunded — terminal: refund
  • on_hold — funds held, manual intervention required
  • failed — terminal: failed
  • 2 — On processing
  • 4 — Completed
  • 6 — Refund
  • 14 — Failed
{
"message": "Payment order f47ac10b-58cc-4372-a567-0e02b2c3d479 not found",
"statusCode": 404,
"error": "Not Found"
}
  • 400 validation (invalid UUID, unknown channel id)
  • 401 Bearer invalid
  • 404 order missing / not owned by user
  • 500 internal — generic toast
  • FE polls GET /orders/:id every 3-5 seconds while the state is not terminal.
  • Stop polling when status{completed, failed, cancelled, refunded}.
  • The last_callback_at field indicates the latest Amdigipay callback has been processed.