← am08shop.com / API Documentation
API Documentation

Distributor API

A host-to-host interface for purchasing digital products automatically from your own system. Intended for distributor partners who already hold a balance at AM08SHOP.

Base URL

Production
https://api.am08shop.com/v1

Sandbox — not yet available, the name is reserved
https://sandbox-api.am08shop.com/v1

Every endpoint uses the POST method and exchanges JSON — including read-only endpoints such as the balance check. This is deliberate: the request signature includes a digest of the body, so the rules stay uniform and there are no special cases that could be applied incorrectly.

Prerequisites

The following four conditions must be met before your first request is accepted.

RequirementDetails
Distributor roleYour account must have the Distributor role. The prices that apply in the API are Distributor-tier prices, not storefront prices.
BalanceOrders can only be paid for with your balance. There is no other payment method in the API — top up your balance first through the member area.
PINYour account must already have a PIN. The PIN is sent with every order request.
IP whitelistRequests are only accepted from IP addresses you have registered on the API Credentials page. IPs outside the list are rejected before the signature is even checked. While the list is empty, every request is rejected — see Configuring the API Credentials page.

Credentials, the IP whitelist and callback URLs are managed by you through the API Credentials menu in your member area. Changing any of them requires your PIN.

Credentials

You hold two values with different roles. It is important to tell them apart: one may be seen by others, the other must never be.

API Key
Identifier · sent with every request

States who is calling. It is sent as-is in the X-Client-Id header on every request, so its value is visible by design — its role is like a username, not a password. It is safe to appear in your own logs.

pk_live_7f3a9c2e81b4d650
Secret Key
Signing key · never sent

Used only to compute X-Signature. Its value is never included in a request — only the result of the computation is sent. Its role is like a password: anyone who holds it can impersonate you.

sk_live_9d81ff4c6a27e05b3ac1…

This is why a signature is safer than simply sending a secret key in a header: even if a request is intercepted, the eavesdropper only obtains the API Key and one signature that is valid for that request only — not a key that can be used to forge new requests.

Getting and replacing credentials

Both are issued automatically the first time you open the API Credentials menu in the member area. You do not need to request anything from us: as soon as your account has the Distributor role, that menu appears, and your first visit there issues your keys.

The first visit is your only chance

The Secret Key is shown in full only at the moment it is issued. Once you leave that page, its value can no longer be displayed — it is not merely hidden, it is never sent again. Copy and store it before closing the tab. If you miss it, press Regenerate to issue a new one.

Regenerating makes the old Secret Key invalid immediately, so any integration still using it will fail with rc 11. Prepare the replacement on your side before pressing the button.

Configuring the API Credentials page

Besides showing your keys, that page is where you set where requests may come from and where we send callbacks. Saving any change here requires your PIN.

IP whitelist

The list of your servers' IP addresses. Requests from addresses outside the list are rejected first, before the signature is even checked.

An empty list rejects all requests

This is not an oversight; it is the gate itself. Newly issued credentials always start with an empty list, so no request can get in until you fill it — even a key that leaks before you have used it is useless to anyone. The side effect is useful: emptying the list is the quickest way to pause your whole integration without deleting anything.

RuleDetails
Exact addressesEnter IPs one by one. CIDR ranges (10.0.0.0/8) and hostnames (my-server.com) are rejected — a hostname can be repointed to another address without our knowledge, which would stop the list from being a guarantee.
SeparatorCommas or newlines, and you may mix them. 1.2.3.4, 5.6.7.8 and one-per-line are equivalent.
Maximum 20 addressesThis list is scanned on every API request you make, so its length is capped to keep it from becoming a cost you pay continuously.
IPv6 is normalizedIPv6 addresses are stored in canonical form. 2606:4700::ABCD will be shown back as 2606:4700::abcd, and ::ffff:1.2.3.4 becomes 1.2.3.4. These are the same addresses — not a change, and precisely what makes them match when your request arrives.

Callback URL

Where we send notifications. Leave it empty if you do not use callbacks and prefer to fetch status through check status.

RuleDetails
https:// requiredThe callback body contains the destination number, the SN and your remaining balance. http:// is rejected so that this data never travels in plain text.
Public addressNon-public addresses — localhost, 127.0.0.1, private ranges, cloud metadata addresses — are rejected. The check is repeated every time we contact you, not only when you save.
No redirects3xx replies are not followed and count as a failure. Point directly at the target endpoint.
Product Callback URL is not active yet

The field is already available and you may fill it in now, but nothing is sent to it yet. The product.changed callback will start working once the feature is released; until then, fetch price changes through the product list using the updated_since parameter.

Authentication

Every request carries three headers. There is no token to store or refresh — the signature is recomputed for each request.

HeaderValue
X-Client-IdYour API Key — see Credentials.
X-Request-TimeThe request time in ISO 8601 format, for example 2026-08-05T09:14:22+07:00 or 2026-08-05T02:14:22Z.
X-SignatureHMAC-SHA256 as lowercase hexadecimal, computed with the Secret Key. How to build it is described below.

Building the signature

  1. Compose the JSON body, then strip all spaces and newlines so it becomes a single compact line.
  2. Compute the MD5 of that body string.
  3. Join four parts separated by colons: the method, the path without the leading slash, the MD5 result, and X-Request-Time.
  4. Compute HMAC-SHA256 over that string using your Secret Key as the key.
// 1 — compact the body
{"service_code":"ML10","reference_number":"INV-0001",...}

// 2 — md5 of the body above
ca06c494d590574127263feeed50bae5

// 3 — the string to be signed
POST:v1/order:ca06c494d590574127263feeed50bae5:2026-08-05T09:14:22+07:00

// 4 — final result, goes into the X-Signature header
hmac_sha256(secret, string_above)  // hex, lowercase
Time limit

X-Request-Time is rejected if it differs by more than 5 minutes from our server clock. Make sure your server clock is NTP-synchronized — a drifting clock is the most common and most confusing cause of failed integrations, because the signature itself is correct.

Each signature can be used only once. To resend (for example after a timeout), recompute X-Request-Time and the signature.

Protecting the Secret Key

The Secret Key is only used to compute the signature and is never sent in a request. Do not put it in client-side code, public repositories or logs. If it leaks, regenerate it from the API page — signatures made with the old key stop working immediately.

One string, used twice

The X-Request-Time value in the header must be exactly the same as the one you put into the signing string. Compute it once, keep it in a variable, and use it in both places. Generating it twice separately is the hardest-to-trace cause of rc 11, because even a one-second difference produces a different signature.

Code examples

A signing function together with an example call to the order endpoint. All examples produce identical signatures for the same input.

// Node.js 18+ — no external dependencies
const crypto = require('crypto');

const BASE_URL   = 'https://api.am08shop.com';
const API_KEY    = process.env.PK_API_KEY;     // pk_live_…
const SECRET_KEY = process.env.PK_SECRET_KEY;  // sk_live_… never hardcode this

function buatSignature(pathname, payload, requestTime) {
  const md5 = crypto.createHash('md5').update(payload).digest('hex');
  const raw = `POST:${pathname}:${md5}:${requestTime}`;
  return crypto.createHmac('sha256', SECRET_KEY).update(raw).digest('hex');
}

async function panggil(pathname, body) {
  // JSON.stringify already produces a compact string with no spaces/newlines.
  // The signed payload MUST be the very same string that is sent.
  const payload     = JSON.stringify(body);
  const requestTime = new Date().toISOString();

  const res = await fetch(`${BASE_URL}/${pathname}`, {
    method:  'POST',
    headers: {
      'Content-Type':   'application/json',
      'X-Client-Id':    API_KEY,
      'X-Request-Time': requestTime,
      'X-Signature':    buatSignature(pathname, payload, requestTime),
    },
    body: payload,
  });
  return res.json();
}

// Example: create an order
panggil('v1/order', {
  service_code:     'ML10',
  reference_number: 'INV-0001',
  inputs:           { user_id: '12345678', zone_id: '2201' },
  price:            3000,
  pin:              process.env.PK_PIN,
}).then(console.log);
Do not rebuild the JSON

Sign exactly the same body string that you send — do not re-serialize the JSON just for signing. Key order, whitespace and character escaping can differ between calls, and a single differing character makes the MD5 — and therefore the whole signature — mismatch. In all the examples above, a single payload variable is used for both.

Verifying the callback signature

The computation is the same; only pathname is replaced with the path of your own callback endpoint.

// Node.js — verify an incoming callback
const diterima = req.headers['x-signature'];
const harusnya = buatSignature('webhook/am08shop', rawBody, req.headers['x-request-time']);

// Compare with timingSafeEqual, not === , so the comparison time
// does not leak information about the correct signature.
const cocok = diterima?.length === harusnya.length &&
  crypto.timingSafeEqual(Buffer.from(diterima), Buffer.from(harusnya));

if (!cocok) return res.status(401).end();

rawBody must be the raw body as received, not the result of parsing and then re-stringifying it. In Express, use express.raw() or capture the raw body through the verify option before the JSON is parsed.

Response codes

Every response carries rc and message. The HTTP status is always 200 as long as the request was received and authenticated — read the business outcome from rc, not from the HTTP status.

rcMeaningSuggested action
00Success—
01Transaction is being processedWait for the callback. Do not resend the order.
02Transaction failedYour balance is being refunded automatically.
03Balance has been refundedFinal status. Safe to place the order again.
10Unknown Client IDCheck the X-Client-Id header.
11Invalid signatureCheck the body compaction and the order of the signing string.
12Request time expiredSynchronize your server clock.
13IP not allowedAdd the IP on the API page.
14Wrong PINStop retrying. See the note below.
15API access disabledContact us. Also returned for new orders while the website is under maintenance (see message); resending an existing reference_number still returns that order's status.
16Request already receivedA signature is valid only once. Resend with a new X-Request-Time and signature.
20Product not foundRefresh your catalog.
21Product is currently inactiveTry again later or use another product.
22Price mismatchRe-fetch the price from the product list.
30Insufficient balanceTop up your balance.
31Incomplete account dataComplete inputs according to inputs_schema.
40Reference number already used for a different orderUse a new reference number.
99System errorTry again in a moment.
Repeated wrong PIN

Sending a wrong PIN repeatedly will lock your API access for 15 minutes. This lockout applies to the API only and does not affect your PIN in the member area. If you receive rc 14, stop sending and fix your configuration — automatic retries only extend the lockout.

Balance

Returns your account's remaining balance in Rupiah.

POST/v1/balance

Request

{}

The body is empty, but it must still be sent as {} and be included in the signature.

Response

{
  "rc": "00",
  "message": "Sukses",
  "data": {
    "balance": 1250000,
    "currency": "IDR"
  }
}

Product list

Returns the catalog with your Distributor-tier prices and the shape of the account data each product requires.

POST/v1/products

Request

FieldTypeDetails
updated_sinceoptionalstringISO 8601. If provided, only products changed since that time are returned. Use it for periodic synchronization without pulling the whole catalog.
category_codeoptionalstringRestrict to a single category.

Response

{
  "rc": "00",
  "message": "Sukses",
  "data": [
    {
      "service_code": "ML10",
      "service_name": "10 Diamonds",
      "category_code": "MLBB",
      "category_name": "Mobile Legends",
      "price": 3000,
      "status": "active",
      "updated_at": "2026-08-05T09:00:00+07:00",
      "inputs_schema": [
        { "key": "user_id", "label": "User ID", "type": "number", "required": true },
        { "key": "zone_id", "label": "Zone ID", "type": "number", "required": true }
      ]
    }
  ]
}

inputs_schema is the contract for the inputs field when creating an order. Mobile top-up products usually have a single entry; games usually have two. Store this schema alongside your catalog so you can validate on your side before sending an order.

Create order

POST/v1/order

Request

FieldTypeDetails
service_coderequiredstringThe product code from the product list.
reference_numberrequiredstringYour own reference number, unique per account. Max. 64 characters.
inputsrequiredobjectDestination account data, keyed according to the product's inputs_schema.
priceoptionalnumberThe unit price you believe applies. If sent and it does not match, the order is rejected with rc 22. If omitted (or 0), the order is charged at the Distributor-tier price in effect at that moment.
pinrequiredstringYour account's 6-digit PIN.
qtyunusednumberNo longer used. Every order always has a quantity of 1. You may still send this field — its value is ignored, so existing integrations do not need to change. To order several units, send several orders with different reference_number values.
{
  "service_code": "ML10",
  "reference_number": "INV-0001",
  "inputs": {
    "user_id": "12345678",
    "zone_id": "2201"
  },
  "price": 3000,
  "pin": "123456"
}

Response

{
  "rc": "01",
  "message": "Transaksi sedang diproses",
  "data": {
    "order_id": "TRX-20260805-A1B2C",
    "reference_number": "INV-0001",
    "service_code": "ML10",
    "service_name": "10 Diamonds",
    "status": "PROCESSING",
    "price": 3000,
    "qty": 1,
    "total": 3000,
    "sn": "",
    "balance_after": 1247000,
    "created_at": "2026-08-05T09:14:22+07:00"
  }
}
Idempotency

Resending the same reference_number does not create a second order — you receive the status of the existing order back. This also applies when your connection drops before the response is received: resend with the same body and reference_number, but a new X-Request-Time and signature — your balance will not be deducted twice. A signature is valid only once; an exact duplicate request is rejected with rc 16.

If the same reference_number is sent with different content, the request is rejected with rc 40.

Your balance is deducted when the order is accepted. If the order ends in failure, the balance is refunded automatically and the status becomes REFUNDED — you do not need to file anything.

Check status

Use this as a safety net when a callback is not received. It is not a replacement for callbacks — do not poll aggressively.

POST/v1/order/status

Request

Provide one of the following two fields.

FieldTypeDetails
reference_numberstringYour reference number.
order_idstringThe order number from our system.
{ "reference_number": "INV-0001" }

Response

The shape is exactly the same as the create order response, with the latest status and sn.

Transaction status callback

Sent to your transaction callback URL whenever an order reaches a final status. We send a JSON POST with the same signature headers as your requests to us — so you can verify it with code that works identically.

{
  "event": "transaction.status",
  "data": {
    "order_id": "TRX-20260805-A1B2C",
    "reference_number": "INV-0001",
    "service_code": "ML10",
    "status": "SUCCESS",
    "rc": "00",
    "message": "Transaksi berhasil",
    "sn": "SN-8842190",
    "price": 3000,
    "qty": 1,
    "total": 3000,
    "balance_after": 1247000,
    "updated_at": "2026-08-05T09:15:04+07:00"
  }
}

Reply with HTTP 200 to acknowledge the callback. Any response other than 200, or no response within 10 seconds, counts as a failure and will be retried every 60 seconds, up to five times. After that the callback stops and you need to fetch the status through check status.

Idempotency on your side

The same callback may be delivered more than once — for example if your 200 response is lost in transit. Make sure your processing is safe to repeat, keyed on reference_number.

Product change callback

Sent to your product callback URL when a product's price or status changes. Changes are batched first for about 20 seconds, then delivered as a single callback containing an array.

Why changes are batched

A single admin action — for example changing a category's margin — can change the prices of hundreds of products at once. If each product sent its own callback, your endpoint would receive hundreds of requests in an instant. With batching, one admin action produces one callback, and you can process it in a single database transaction.

{
  "event": "product.changed",
  "data": [
    {
      "service_code": "ML10",
      "price": 3100,
      "status": "active",
      "changed": ["price"],
      "updated_at": "2026-08-05T09:20:00+07:00"
    },
    {
      "service_code": "ML86",
      "price": 21000,
      "status": "inactive",
      "changed": ["status"],
      "updated_at": "2026-08-05T09:20:00+07:00"
    }
  ]
}

The changed field lists what changed, so you can update only what is needed. The retry rules are the same as for the transaction callback.

Callback signature verification

Callbacks from us carry the X-Client-Id, X-Request-Time and X-Signature headers. The signed string uses your own callback path, without the leading slash.

// Your callback URL: https://your-server.com/webhook/am08shop
POST:webhook/am08shop:<md5(compacted body)>:<X-Request-Time>
Always verify

Without verification, anyone who knows your callback URL can send fake notifications — for example claiming an order is SUCCESS when it is not. Compare the signature before processing the body, and reject the request if it does not match.

Transaction status

PROCESSING → SUCCESS or FAILED → REFUNDED
StatusrcMeaning
PROCESSING01The order has been accepted and is being processed. Your balance has been deducted. The status is not final.
SUCCESS00The product was delivered. sn contains the serial number if the product has one. Final status.
FAILED02The product could not be delivered. A refund is in progress — this status is temporary.
REFUNDED03The balance has been returned to your account. Final status.

You will receive a callback on SUCCESS and REFUNDED. If an order stays in PROCESSING longer than usual, it is awaiting manual handling — do not send a replacement order, because the original order may still end in success.

Key rules

The price is checked at order time

The price field is optional. If you send it, it is compared against the Distributor-tier price in effect at that moment. If they differ, the order is rejected — it is never silently processed at a different price. This protects both parties when your catalog lags behind our price changes.

If price is omitted, the order is charged at the current price. The amount charged is returned in data.price and data.total. We recommend still sending price so that price changes never go unnoticed.

One reference number, one order

reference_number must be unique within your account. This is what makes resending safe when the network misbehaves.

Failures are always refunded

No failed order holds on to your balance. Refunds run automatically and are recorded in your balance history.

Rate limit

Maximum 100 requests per second per account. Exceeding it returns rc 99 temporarily.