# GMJ Roaming API > Complete documentation for Large Language Models --- ## Document: Webhooks Get eSIM events pushed to your server instead of polling. URL: /webhooks # Webhooks We recommend webhooks over polling. When something happens to one of your eSIMs, we send a `POST` to the URL you configured for that event. ## Set up 1. Sign in to [console.gmjroaming.com](https://console.gmjroaming.com) with your reseller account. 2. Go to **Dashboard → Developer**. 3. Enter a URL for each event you want. Leave an event empty to skip it. ## Events | Event (`notifyType`) | When | Console field | | --- | --- | --- | | `ORDER_STATUS` | An order's eSIMs are ready | Order Status | | `ESIM_STATUS` | The eSIM's status changes (installed, in use, suspended, expired, ...) | eSIM Status | | `DATA_USAGE` | Data usage was updated | Data Usage | | `VALIDITY_USAGE` | Validity changed | Validity Usage | | `SMDP_EVENT` | Profile events from the eSIM server (download, install, delete) | SMDP Event | ## Payload Every webhook has the same wrapper: ```json { "status": "success", "notifyType": "DATA_USAGE", "content": { "transactionId": "order-20261111-0001", "orderNo": "B26111100001", "iccid": "8985200012345678901", "totalVolume": 3221225472, "orderUsage": 1073741824, "remain": 2147483648 } } ``` `content.transactionId` is the purchase `transaction_id` you sent to `/checkout`. Use it, or `iccid`, to find the eSIM on your side. | `notifyType` | Fields you can rely on in `content` | | --- | --- | | `ORDER_STATUS` | A **list** of the order's eSIMs, in the same provider format as `/myesim` source data (`iccid`, `esimTranNo`, `orderNo`, `transactionId`, `ac`, ...) | | `ESIM_STATUS` | `transactionId`, `orderNo`, `iccid` | | `DATA_USAGE` | `transactionId`, `iccid`, `totalVolume`, `orderUsage`, `remain` (bytes) | | `VALIDITY_USAGE` | `transactionId`, `iccid`, `durationUnit`, `totalDuration`, `expiredTime` | | `SMDP_EVENT` | `transactionId` plus the provider's event details | Apart from `ORDER_STATUS`, `content` is passed through from the network provider as is, so it usually has more fields than listed. Ignore fields you don't know. After an `ESIM_STATUS` event, read the new status with `POST /myesim`. ## Responding Reply `200` with this body as fast as you can: ```json { "success": true } ``` Do slow work (database writes, emails) after you reply. We wait up to 30 seconds. :::warning{title="Delivery is best-effort"} - Failed deliveries are **not retried**. If you need certainty, confirm with `POST /myesim` or `POST /check-status-payment`. - Webhooks aren't signed. Use a hard-to-guess URL (for example with a random token in the path), accept HTTPS only, and treat the payload as a hint: re-read the eSIM from the API before acting on it. ::: --- ## Document: Versions v1 is production. v2 is alpha. URL: /versions # Versions | Version | Base URL | Status | | --- | --- | --- | | v1 | `https://api.gmjroaming.com/api/v1` | **Production.** Stable. Use this. | | v2 | `https://api.gmjroaming.com/api/v2` | **Alpha.** Can change at any time. | :::danger{title="v2 is not for production"} v2 is under active development. Endpoints, field names and response shapes can change or be removed **without notice**. Don't build production integrations on v2. Use it only to try the new design and send us feedback. ::: ## What v2 changes - Prices are whole IDR amounts: `{ "amount": 69500, "currency": "IDR" }`. - eSIMs are referenced by `esimId` instead of an `iccid` + `transaction_id` pair. - There's a `POST /checkout/preview` that prices a basket without charging it. - Purchases are idempotent: a retry with the same `transaction_id` and body replays the result. - Responses are `{ "data": … }`. Errors are `{ "message": …, "errors"?: … }`. - v2 package codes are different from v1 codes and can't be mixed. See the [v2 reference](/api/v2). --- ## Document: Quickstart Buy your first eSIM with the v1 API in four requests. URL: /quickstart # Quickstart You need an API key from [console.gmjroaming.com](https://console.gmjroaming.com) and some account balance. ```bash export GMJ_KEY="your-api-key" export GMJ="https://api.gmjroaming.com/api/v1" ``` ## 1. Check your account ```bash curl "$GMJ/user" -H "X-API-KEY: $GMJ_KEY" -H "Accept: application/json" ``` `data.balance` is your balance in USD. ## 2. Find a destination ```bash curl "$GMJ/list-country?search=japan" -H "X-API-KEY: $GMJ_KEY" ``` ```json { "status": "success", "code": 200, "data": [ { "country_name": "Japan", "country_code": "jp", "image": "https://gmjroaming.com/img/country/jp.svg" } ] } ``` Pass `country_code` as `locationCode` in the next step. Regions such as `asia` or `europe` work too. ## 3. Pick a package ```bash curl "$GMJ/list-esim?locationCode=jp" -H "X-API-KEY: $GMJ_KEY" ``` Each package has a `packageCode`, a `name`, `priceUsd` (your price) and `volume` (bytes). ## 4. Buy it The body is an **array**. Generate your own `transaction_id` and store it: it's your reference for this purchase. The call waits until the eSIM is ready, so allow up to 60 seconds. ```bash curl -X POST "$GMJ/checkout" -H "X-API-KEY: $GMJ_KEY" -H "Content-Type: application/json" \ --max-time 60 \ -d '[{"packageCode":"JC016","count":1,"transaction_id":"order-20261111-0001"}]' ``` ```json { "status": "success", "code": 200, "message": "Checkout successful", "currency": "USD", "current_balance": "95.80", "transaction_id": "order-20261111-0001" } ``` ## 5. Get the eSIM ```bash curl -X POST "$GMJ/myesim" -H "X-API-KEY: $GMJ_KEY" \ -H "Content-Type: application/json" -d '{"transaction_id":"order-20261111-0001"}' ``` Send your customer `qrCodeUrl` (a QR to scan) or `publicUrl` (an install page). `ac` is the activation code for manual entry. ## Next - Set up [webhooks](/webhooks) to hear when your customer installs the eSIM and uses data. - Read [Buying eSIMs](/buying-esims) for retries and top-ups. :::tip Try read-only endpoints straight from the [API reference](/api/v1): click **Test request** and paste your key. Endpoints that spend balance have the playground turned off. ::: --- ## Document: Managing eSIMs Install links, usage, suspend, cancel and revoke. URL: /managing-esims # Managing eSIMs ## Finding an eSIM `POST /myesim` lists your eSIMs, newest first. Filter by `transaction_id`, `iccid` (an exact match when you pass a full 18–22 digit ICCID), `no_order`, `package_name` or `esim_status`. Page with `page` and `per_page` (max 100). ## Giving it to your customer | Field | Use | | --- | --- | | `qrCodeUrl` | Image of the QR code to scan | | `publicUrl` / `shortUrl` | Install page you can send in a chat | | `ac` | LPA activation code for manual entry | | `apn` | APN, if the device asks for one | You can also fetch links on demand with `GET /public-esim-link?esimTranNo=...`. ## Usage - [Webhooks](/webhooks) (`DATA_USAGE`, `ESIM_STATUS`) push changes to you. This is the recommended way. - `orderUsage` and `esimStatus` in `/myesim` are synced periodically. - `GET /esim-live-usage?esimTranNo=...` asks the network provider directly. It's slower, but current. ## Lifecycle actions | Action | Endpoint | Refund | Undo | | --- | --- | --- | --- | | Pause data | `POST /suspend` | — | `POST /unsuspend` | | Cancel an eSIM that was **never installed** | `POST /cancel-esim` | Yes, to balance | No | | Permanently disable | `POST /revoke` | No | No | All of them take the eSIM's `iccid` and the `transaction_id` of the purchase it came from. `cancel-esim` also needs `package_code`. --- ## Document: Introduction What the GMJ Roaming API does and how an integration fits together. URL: /introduction # Introduction The GMJ Roaming API lets you sell travel eSIMs from your own app, website or bot. You buy eSIMs from your **GMJ account balance**, and we give you everything your customer needs to install them: a QR code, an install link and an LPA activation code. ## What you can do | Task | Endpoints (v1) | | --- | --- | | Find packages for a destination | `GET /list-country`, `GET /list-esim` | | Buy eSIMs | `POST /checkout` | | Hand the eSIM to your customer | `POST /myesim`, `GET /public-esim-link` | | Follow installs, usage and expiry | [Webhooks](/webhooks), `GET /esim-live-usage` | | Add more data | `GET /list-topupdataplan`, `POST /topup-esim` | | Cancel, suspend or revoke | `POST /cancel-esim`, `/suspend`, `/unsuspend`, `/revoke` | ## How a purchase works >GMJ: GET /list-esim?locationCode=JP GMJ-->>You: packages + your prices You->>GMJ: POST /checkout [packageCode, count, transaction_id] GMJ-->>You: 200 Checkout successful You->>GMJ: POST /myesim {transaction_id} GMJ-->>You: eSIM with QR code and install link GMJ--)You: webhooks: ESIM_STATUS, DATA_USAGE, VALIDITY_USAGE`} /> 1. List packages for a destination with `GET /list-esim`. 2. Buy with `POST /checkout`. The call waits until the eSIMs are ready and returns `200`. 3. Fetch the eSIMs (QR code, install link, activation code) with `POST /myesim`. 4. Get [webhooks](/webhooks) when the customer installs the eSIM, uses data or reaches expiry. If a purchase fails, you get `500` and your balance is refunded automatically. ## Versions - **v1** (`/api/v1`) is the **production** API. Build on this. - **v2** (`/api/v2`) is **alpha**. It can change at any time and isn't for production. See [Versions](/versions). Next: [Quickstart](/quickstart). --- ## Document: Errors Response envelope and HTTP status codes. URL: /errors # Errors ## v1 response envelope Every v1 response looks like this: ```json { "status": "success", "code": 200, "message": "…", "data": { } } ``` Errors use `"status": "error"`. Validation errors add an `errors` object keyed by field: ```json { "status": "error", "code": 422, "message": "Validasi gagal", "errors": { "0.packageCode": ["The 0.packageCode field is required."] } } ``` `message` is meant for logs and may be in Indonesian. Branch on the HTTP status, not on the message text. ## Status codes | Code | Meaning | | --- | --- | | `200` | Done | | `202` | Accepted, finishing in the background (top-ups, and v2 purchases) | | `400` | Missing input, insufficient balance, or the action isn't allowed in this state | | `401` | `X-API-KEY` header missing | | `403` | API key invalid | | `404` | Not found, or it doesn't belong to your account | | `409` | That `transaction_id` is still being processed (v2: or it was used for a different request) | | `422` | Validation failed, or the eSIM has expired (`error_type: ESIM_EXPIRED`) | | `500` | Failed. For checkout, your balance was refunded and you can retry with the same `transaction_id`. | | `503` | Temporarily unavailable. Nothing was charged. Retry. | --- ## Document: Buying eSIMs Checkout, retries and top-ups. URL: /buying-esims # Buying eSIMs ## Checkout `POST /checkout` charges your balance and **waits until the eSIMs are provisioned**, usually a few seconds. Set your HTTP client timeout to at least 60 seconds. | Response | Meaning | What to do | | --- | --- | --- | | `200` | eSIMs are ready | `POST /myesim` with the `transaction_id` | | `400` | Insufficient balance, or the `transaction_id` belongs to an unpaid transaction | Top up your balance, or use another `transaction_id` | | `409` | Another request with this `transaction_id` is still running | Wait, then check `/myesim` | | `422` | Invalid body | Fix the request | | `500` | Provisioning failed. Your balance was **refunded** | Retry with the same `transaction_id` | ### `transaction_id` - Generate it yourself, for example `order-`. It's your reference for the purchase. - Use the same value on every item in one checkout. - Retrying a completed purchase with the same `transaction_id` returns `200` without charging again. That makes it safe to retry after a timeout. ### Several packages at once ```json [ { "packageCode": "JC016", "count": 2, "transaction_id": "order-1001" }, { "packageCode": "KR005", "count": 1, "transaction_id": "order-1001" } ] ``` ## Top-ups Top-ups are **asynchronous**. 1. `GET /list-topupdataplan?iccid=...` lists the plans that fit this eSIM. 2. `POST /topup-esim`: ```json { "packageCodeTopup": "", "iccid": "8985200012345678901", "transaction_id": "order-1001", "external_id": "renew-7781" } ``` - `transaction_id` is the **original purchase** this eSIM came from, not a new value. - `external_id` is your own unique key for this top-up. Always send it: a retry with the same `external_id` is never charged twice. 3. You get `202` with a new `transaction_id` (`topup-`). Check it with `POST /check-status-payment` until it's **`Success`**. On `FAILED`, your balance was refunded. Only eSIMs whose `supportTopUpType` is `2` or `3` can be topped up, and only before they expire. An expired eSIM returns `422` with `error_type: ESIM_EXPIRED`. --- ## Document: Authentication Authenticate with the X-API-KEY header. URL: /authentication # Authentication Send your API key in the `X-API-KEY` header on every request: ```http GET /api/v1/user HTTP/1.1 Host: api.gmjroaming.com X-API-KEY: your-api-key Accept: application/json ``` | Response | Meaning | | --- | --- | | `401` `{"message":"API key is missing"}` | No `X-API-KEY` header. | | `403` `{"message":"Invalid API key"}` | The key is wrong or was rotated. | :::warning Your key can spend your balance. Keep it on your server. Never put it in a mobile app, browser code or a public repository. If it leaks, rotate it in the console right away. ::: Every request acts on the account that owns the key. You can only see and manage eSIMs and transactions that belong to that account. --- ## Document: AI & MCP Use these docs from Claude, ChatGPT, Cursor and other AI tools. URL: /ai-tools # AI & MCP These docs are built to be read by AI assistants and coding agents as well as people. ## MCP server Connect any MCP client to: ``` https://docs.gmjroaming.com/mcp ``` It's **read-only** and needs no API key. It can search the guides and read the full API reference. It can't call the API or see your account. | Tool | What it does | | --- | --- | | `search_docs` | Search guides and endpoints | | `read_page` | Read one guide as Markdown | | `list_endpoints` | List v1 or v2 endpoints | | `get_endpoint` | Full definition of one endpoint, with schemas | **Claude Code** ```bash claude mcp add --transport http gmj-roaming-docs https://docs.gmjroaming.com/mcp ``` **Cursor / VS Code / others**: add an HTTP MCP server with the URL above. The [MCP page](/mcp-server) has copy-paste setup for each client. ## Plain text for LLMs | URL | Contents | | --- | --- | | [`/llms.txt`](/llms.txt) | Index of every page | | [`/llms-full.txt`](/llms-full.txt) | All guides in one file | | `/.md` | Any guide as Markdown | | [`/openapi/v1.yaml`](/openapi/v1.yaml) | v1 OpenAPI spec | | [`/openapi/v2.yaml`](/openapi/v2.yaml) | v2 OpenAPI spec (alpha) | Each page also has **Use in Claude** and **Use in ChatGPT** buttons. # API reference ## v1 (production) Base URL: https://api.gmjroaming.com/api/v1. Full spec: /openapi/v1.yaml - GET /user: Get account - POST /update-user: Update account - GET /list-country: List countries and regions - GET /list-esim: List eSIM packages - POST /checkout: Buy eSIMs - POST /reseller/checkout: Buy eSIMs (sub-reseller account) - GET /list-topupdataplan: List top-up plans for an eSIM - POST /topup-esim: Top up an eSIM - POST /myesim: List my eSIMs - GET /public-esim-link: Get install link and QR code - GET /esim-live-usage: Get live data usage - POST /suspend: Suspend an eSIM - POST /unsuspend: Resume a suspended eSIM - POST /cancel-esim: Cancel an unused eSIM (refund) - POST /revoke: Revoke an eSIM - POST /history-transaction: List transactions - POST /check-status-payment: Get transaction status - GET /announcement: Active announcements - POST /cart: Add to cart (legacy) (deprecated) ## v2 (ALPHA, may change at any time, not for production) Base URL: https://api.gmjroaming.com/api/v2. Full spec: /openapi/v2.yaml - GET /user: Get account - POST /update-user: Update account - GET /list-country: List countries and regions - GET /list-esim: List eSIM packages - POST /checkout/preview: Preview a purchase - POST /checkout: Buy eSIMs - POST /reseller/checkout: Buy eSIMs (reseller account) - POST /cart: Save a basket to the cart - GET /list-topupdataplan: List top-up plans for an eSIM - POST /topup-esim: Top up an eSIM - POST /myesim: List my eSIMs - GET /public-esim-link: Get install link and QR code - POST /esim-usage: Get data usage - POST /esim-progress: Get installation events - POST /esim-device-info: Get device info - POST /suspend: Suspend an eSIM - POST /unsuspend: Resume a suspended eSIM - POST /cancel-esim: Cancel an unused eSIM (refund) - POST /revoke: Revoke an eSIM - POST /history-transaction: List transactions - POST /check-status-payment: Get transaction status - GET /announcement: Active announcements