PPOB integration for partner companies — check Poin, top up, and purchase products.
Download Postman CollectionThe Open API lets partner companies integrate with the MBT PPOB platform. Every product purchase is funded by your company's Poin (prepaid balance), which you can top up and monitor through these endpoints.
https://tokoatlas.com/api/partner
Authenticate every request with your API Key and Secret, sent as HTTP headers. You can find and regenerate these in your company dashboard under Poin Perusahaan → Kredensial Open API.
| Header | Description |
|---|---|
X-Api-Key | Your company API key. |
X-Api-Secret | Your company API secret. |
curl https://tokoatlas.com/api/partner/saldo \ -H "X-Api-Key: mbtk_xxxxxxxxxxxxxxxx" \ -H "X-Api-Secret: mbts_xxxxxxxxxxxxxxxx"
Responses use standard HTTP status codes and a consistent JSON shape:
{
"success": false,
"message": "Invalid API credentials."
}
| Code | Meaning |
|---|---|
| 200 / 201 | Success. |
| 401 | Missing or invalid API credentials. |
| 402 | Insufficient Poin. |
| 403 | Account not allowed to use the API (e.g. MBT). |
| 404 | Resource not found. |
| 422 | Validation error. |
Test every endpoint directly from this page. Credentials are stored only in your browser and sent straight to the API (same origin) — they never pass through a third party. Pick an endpoint, fill in the fields, then Send Request.
GET /saldo
Returns your company's current balance.
curl https://tokoatlas.com/api/partner/saldo \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": {
"company": "Partner Co",
"balance": 1500000
}
}
POST /saldo/topup
Creates a bank-transfer deposit ticket that adds balance to your company's
Poin once the transfer is received. Settlement is asynchronous — poll
Topup Status below or listen for the
topup.success webhook.
| Field | Type | Description | |
|---|---|---|---|
amount |
number | required | Amount to add. Minimum 10,000. |
reference_code |
string | required | Your own reference for this topup. Must be unique — reusing a value returns 409 and does not create a second ticket. Echoed back in every response. |
curl -X POST https://tokoatlas.com/api/partner/saldo/topup \
-H "X-Api-Key: ..." -H "X-Api-Secret: ..." \
-H "Content-Type: application/json" \
-d '{"amount": 500000, "reference_code": "TOPUP-1001"}'
Response
{
"success": true,
"message": "Tiket deposit berhasil dibuat. Selesaikan transfer sebelum batas waktu.",
"data": {
"topup_code": "TOP2205260000001123",
"reference_code": "TOPUP-1001",
"amount": 500000,
"actual_amount": 500328,
"instruction_text": "TOPLINK: Mhn transfer Rp. 500.328 (HRS SAMA) ke ...",
"parsed_instruction": { "account_holder": "...", "banks": [ ... ], "hours": "09.00 - 18.00" },
"status": "awaiting_transfer",
"expired_at": "2026-05-22T21:00:00+07:00"
}
}
Response (409 — duplicate reference_code)
{
"success": false,
"message": "reference_code already used. It must be unique.",
"data": {
"reference_code": "TOPUP-1001",
"topup_code": "TOP2205260000001123",
"status": "awaiting_transfer"
}
}
GET /saldo/topup/{code}
Returns the current status of a topup request.
{code} accepts either our
topup_code or your own
reference_code.
curl https://tokoatlas.com/api/partner/saldo/topup/TOPUP-1001 \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": {
"topup_code": "TOP2205260000001123",
"reference_code": "TOPUP-1001",
"amount": 500000,
"actual_amount": 500328,
"status": "success",
"channel": "partner_api",
"instruction_text": "TOPLINK: Mhn transfer Rp. 500.328 (HRS SAMA) ke ...",
"parsed_instruction": { "account_holder": "...", "banks": [ ... ], "hours": "09.00 - 18.00" },
"failure_reason": null,
"paid_at": "2026-05-22T12:05:00+07:00",
"expired_at": "2026-05-22T21:00:00+07:00",
"created_at": "2026-05-22T12:00:00+07:00"
}
}
Statuses: pending,
awaiting_transfer,
success,
failed,
expired. Returns
404 for an unknown code.
GET /saldo/topup-history
Returns a paginated list of your company's topup requests, newest first.
| Query | Type | Description | |
|---|---|---|---|
status | string | optional | Filter by status: pending, awaiting_transfer, success, failed, expired. |
page | number | optional | Page number (default 1). |
rows | number | optional | Rows per page (default 20, max 100). |
curl "https://tokoatlas.com/api/partner/saldo/topup-history?status=success&rows=20" \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": [
{
"topup_code": "TOP2205260000001123",
"reference_code": "TOPUP-1001",
"amount": 500000,
"actual_amount": 500328,
"status": "success",
"channel": "partner_api",
"paid_at": "2026-05-22T12:05:00+07:00",
"expired_at": "2026-05-22T13:00:00+07:00",
"created_at": "2026-05-22T12:00:00+07:00"
}
],
"pagination": { "page": 1, "rows": 20, "total": 1, "last_page": 1 }
}
GET /saldo/history
Returns a paginated ledger of every change to your company's Poin (top-ups and PPOB purchases), newest first.
| Query | Type | Description | |
|---|---|---|---|
type | string | optional | Filter by mutation type: credit (top-up) or debit (purchase). |
page | number | optional | Page number (default 1). |
rows | number | optional | Rows per page (default 20, max 100). |
curl "https://tokoatlas.com/api/partner/saldo/history?type=debit&rows=20" \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": [
{
"type": "debit",
"source": "ppob_transaction",
"amount": 5900,
"balance_before": 2000000,
"balance_after": 1994100,
"description": "PPOB Three Reguler 5.000 · Tujuan 0895347740321",
"reference_type": "App\\Models\\Transaction",
"reference_id": "2205260000001123",
"created_at": "2026-05-22T12:10:00+07:00"
}
],
"pagination": { "page": 1, "rows": 20, "total": 1, "last_page": 1 }
}
GET /products
Lists available PPOB products with their selling price.
| Query | Type | Description | |
|---|---|---|---|
q | string | optional | Search term. Matches any part of the product name or code, case-insensitive. |
provider | string | optional | Filter by provider. Also a partial match by default — THREE returns both THREE DATA and THREE REGULER. |
product_code | string | optional | Look up a single product by its exact code (case-insensitive). Takes precedence over q. |
match | string | optional | Set to exact to make q and provider exact matches instead of partial. q then has to equal the full product name or code. |
page | number | optional | Page number (default 1). |
rows | number | optional | Rows per page (default 100). |
q=tri returns nothing
because Three products are named Three Reguler …
under provider THREE REGULER. Use the provider
values listed below rather than guessing.
| Prepaid credit | Data package | Other |
|---|---|---|
TSEL REGULER | TSEL DATA | TOKEN (PLN prepaid) |
XL REGULER | XL DATA | |
ISAT REGULER | ISAT DATA | |
THREE REGULER | THREE DATA | |
AXIS REGULER | AXIS DATA | |
BYU REGULER | BYU DATA | |
SMART DATA |
# Partial search (default) curl "https://tokoatlas.com/api/partner/products?q=Three%20Reguler&rows=20" \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..." # One provider only, nothing else curl "https://tokoatlas.com/api/partner/products?provider=THREE%20REGULER&match=exact" \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..." # A single product by code curl "https://tokoatlas.com/api/partner/products?product_code=ATH5" \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": [
{
"product_code": "ATH5",
"name": "Three Reguler 5.000",
"provider": "THREE REGULER",
"price": 7000,
"active": true,
"disrupted": false
}
],
"pagination": { "more": false }
}
product_code that exists
upstream can still come back empty when it is not sellable to you.
POST /pln/inquiry
Resolves a PLN meter / customer id to the registered customer name and daya (power rating), before you buy a token. This call is synchronous: it waits for the biller and returns the result in the response. It does not deduct Poin and is not blocked by your balance.
| Field | Type | Description | |
|---|---|---|---|
target_number |
string | required | PLN meter number / customer id (IDPEL). |
curl -X POST https://tokoatlas.com/api/partner/pln/inquiry \
-H "X-Api-Key: ..." -H "X-Api-Secret: ..." \
-H "Content-Type: application/json" \
-d '{ "target_number": "512345678901" }'
Response (200 — found)
{
"success": true,
"data": {
"inquiry_id": "pi9f2c1a...",
"target_number": "512345678901",
"customer_name": "JULI MARIA",
"daya": "2200",
"status": "found"
}
}
Response (404 — not found)
{
"success": false,
"message": "Pelanggan tidak ditemukan.",
"data": { "inquiry_id": "pi9f2c1a...", "status": "invalid" }
}
Response (504 — biller still processing)
{
"success": false,
"message": "Inquiry masih diproses. Silakan cek ulang dengan inquiry_id.",
"data": { "inquiry_id": "pi9f2c1a...", "status": "pending" }
}
504, keep the inquiry_id and poll
PLN Inquiry Status below. Rate limit: 30 requests / minute
per company (429 when exceeded). A successful lookup is reused
for 120 seconds, so repeating the same meter within that window returns instantly.
GET /pln/inquiry/{inquiry_id}
Fetches a (possibly late) PLN inquiry result by its inquiry_id.
curl https://tokoatlas.com/api/partner/pln/inquiry/pi9f2c1a... \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."
Returns 200 (found), 202 (still
pending) or 404 (not found / unknown inquiry_id), with the
same data shape as above.
POST /transactions
Purchases a PPOB product. The product's price is deducted from your Poin.
| Field | Type | Description | |
|---|---|---|---|
product_code |
string | required | Product code (from /products). |
target_number |
string | required | Destination (phone number / meter id). |
partner_reference |
string | required | Your own reference for this order. Must be unique — reusing a value returns 409 and does not create a second transaction. Echoed back in every response. |
curl -X POST https://tokoatlas.com/api/partner/transactions \
-H "X-Api-Key: ..." -H "X-Api-Secret: ..." \
-H "Content-Type: application/json" \
-d '{
"product_code": "ATH5",
"target_number": "0895347740321",
"partner_reference": "ORDER-1001"
}'
Response (201)
{
"success": true,
"message": "Transaction submitted.",
"data": {
"code": "2205260000001123",
"partner_reference": "ORDER-1001",
"product_code": "ATH5",
"product_name": "Three Reguler 5.000",
"target_number": "0895347740321",
"amount": 5900,
"payment_status": "PAID",
"transaction_status": "PROCESS",
"created_at": "2026-05-22T12:00:00+07:00",
"balance": 1994100
}
}
402 with the
required amount and your current balance — no transaction is created.
partner_reference must be unique. Reusing a value returns
409 with the code of the existing
transaction — no second transaction is created and no Poin is charged.
{
"success": false,
"message": "partner_reference already used. It must be unique.",
"data": {
"partner_reference": "ORDER-1001",
"code": "2205260000001123"
}
}
GET /transactions/{code}
Returns the current status of a transaction you created.
You do not have to poll this endpoint to learn the outcome: register a
webhook and we push
transaction.success /
transaction.failed with these same fields as soon
as the supplier reports back. Keep this endpoint as the fallback.
curl https://tokoatlas.com/api/partner/transactions/2205260000001123 \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..."Response
{
"success": true,
"data": {
"code": "2205260000001123",
"partner_reference": "ORDER-1001",
"product_code": "ATH5",
"product_name": "Three Reguler 5.000",
"target_number": "0895347740321",
"amount": 5900,
"payment_status": "PAID",
"transaction_status": "PROCESS",
"created_at": "2026-05-22T12:00:00+07:00"
}
}
token field. It is null
while the order is processing; once the electricity token is issued,
transaction_status becomes
DONE and token holds
the 20-digit token.
{
"success": true,
"data": {
"code": "2205260000001124",
"partner_reference": "ORDER-1002",
"product_code": "APLN100",
"product_name": "TOKEN PLN 100K",
"target_number": "32201687475",
"amount": 101760,
"payment_status": "PAID",
"transaction_status": "DONE",
"created_at": "2026-05-22T12:00:00+07:00",
"token": "0758-9685-0347-2723-8165"
}
}
Register a webhook URL in your company dashboard under
Poin Perusahaan → Webhook URL. We send a
POST request to that URL whenever a relevant
event happens — a Poin top-up settling, or a Pulsa/Data or PLN Token order
reaching its final status.
Every request has the same envelope: the event name in
event, the event-specific fields in
data, and the server time in
timestamp.
| Event | When it fires |
|---|---|
topup.success | A Poin top-up has been received and credited. |
topup.expired | A Poin top-up ticket expired or was cancelled before payment. |
transaction.success | A Pulsa/Data or PLN Token order was fulfilled by the supplier. For PLN the electricity token is included. |
transaction.failed | A Pulsa/Data or PLN Token order failed at the supplier. Refundable failures are reversed to the source of payment. |
{
"event": "topup.success",
"data": {
"topup_code": "TOP2205260000001123",
"reference_code": "TOPUP-1001",
"amount": 500000,
"credited_amount": 500328,
"status": "success",
"balance": 2000000
},
"timestamp": "2026-05-22T12:00:00+07:00"
}
data carries the same fields as
GET /transactions/{code}, so no follow-up
call is needed to reconcile. Match the order against your own
partner_reference — the value you sent when
creating it.
| Field | Type | Description |
|---|---|---|
transaction_code | string | Our transaction code — the same value as code in the API responses. |
partner_reference | string | The reference you supplied when creating the order. Unique per partner, so it is safe to use as your reconciliation key. |
type | string | Product family, e.g. Pulsa, Data, pln. |
product_code | string | Product purchased. |
product_name | string | Human-readable product name. |
target_number | string | Destination phone number or PLN meter id. |
amount | number | Poin charged for the order. |
payment_status | string | Payment side of the order — PAID once the Poin was deducted. |
transaction_status | string | Fulfilment status: success, failed, or DONE for a PLN order whose token has been issued. |
status | string | Legacy field, kept for backward compatibility. Always success or failed, mirroring the event name. |
created_at | string | When the order was created (ISO 8601, +07:00). |
token | string|null | PLN Token orders only. The 20-digit electricity token, null on failure. |
{
"event": "transaction.success",
"data": {
"transaction_code": "2507260000001589",
"partner_reference": "ORDER-1001",
"type": "Pulsa",
"product_code": "ATH5",
"product_name": "Three Reguler 5.000",
"target_number": "089678920409",
"amount": 5900,
"payment_status": "PAID",
"transaction_status": "success",
"status": "success",
"created_at": "2026-07-25T11:42:04+07:00"
},
"timestamp": "2026-07-25T11:42:11+07:00"
}
PLN orders add token and report
transaction_status: "DONE" once the supplier
issues the token — matching what
GET /transactions/{code} returns.
{
"event": "transaction.success",
"data": {
"transaction_code": "2205260000001124",
"partner_reference": "ORDER-1002",
"type": "pln",
"product_code": "APLN100",
"product_name": "TOKEN PLN 100K",
"target_number": "32201687475",
"amount": 101760,
"payment_status": "PAID",
"transaction_status": "DONE",
"status": "success",
"created_at": "2026-05-22T12:00:00+07:00",
"token": "0758-9685-0347-2723-8165"
},
"timestamp": "2026-05-22T12:00:05+07:00"
}
status is kept for backward compatibility and always
mirrors the event (success /
failed). On a failure the payload has the same shape,
with status and
transaction_status set to
failed and token
null.
transaction.* event fires only when the supplier
reports a final result, so an order still being processed upstream produces no
event at all. Handle events idempotently, keyed on
transaction_code — a repeated supplier report
delivers the event again.
payment_status describes the original payment and
stays PAID even after a failed order is refunded.
Use status /
transaction_status to decide the outcome; a
refundable failure returns the Poin automatically and shows up in
GET /saldo/history.
Every request carries an X-Mbt-Signature header: an
HMAC-SHA256 of the raw request body, keyed by your API Secret.
The event name is also sent as X-Mbt-Event.
Recompute the signature and compare to confirm the request came from us.
// PHP example
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $payload, $apiSecret);
$signature = $_SERVER['HTTP_X_MBT_SIGNATURE'] ?? '';
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
2xx status to acknowledge receipt.
Non-2xx responses (or timeouts after 5 seconds) are logged on our side; delivery
is best-effort and is not retried.