Skip to main content

Web Payment — PHP SDK

Applies toCo-publishing (Standard) or exclusive/direct-publishing (Expub) games with a PHP backend
Packagealo-game/paymentsdk on Packagist
RequiresPHP ≥ 8.1
ImplementsWeb Payment — Standard, in full — plus Web Payment — Expub (including Expub's shared Mobile IAP)

This page is everything you need to integrate WebPay in PHP. You do not need to read Web Payment — Standard or Web Payment — Expub first — those describe the raw HTTP contract for teams writing their own endpoint by hand. This SDK implements both contracts for you: correct field names, correct signature scheme, correct response shape, on every call. You write a handful of PHP functions and never touch HTTP, signing, or JSON directly.

The package ships two independent modules — pick the one matching your game's publishing model, not both by default:

  • WebPay (below) — for co-publishing (co-pub) games, three hooks, HMAC-SHA256.
  • Expub (below) — for exclusive/direct-publishing (expub) games, four hooks, MD5, and also covers that same game's Mobile IAP purchases.
Not a separate contract

Whichever module applies to you, it's the exact same wire contract every other game on that publishing model uses — same calls, same wire format. The SDK just means you never write that wire format by hand.

SDK vs. hand-implementing the contract

Hand-implement the wire contractThis SDK
Signing, timestamp checkYou write and test it yourselfWebpayHandler/ExpubHandler does it, already tested
Field names (plat_order_num, order_id, productid/productId, …)Re-typed by hand from the doc — a subtly wrong field name here only surfaces once a real player's order failsTyped DTO properties ($request->orderId, …) — a wrong field name is a PHP fatal, not a silent mismatch discovered in production
Response envelope (errcode body vs. HTTP status)You build the exact shape by hand, per contractCheckUidResult/CreateOrderResult/PaymentReceivedResult build it for you
Business logic you writeEverything: parsing, signing, envelope, and your own player/order lookupsOnly your own player/order lookups — the on... hooks
Routing calls to handlersSame either way — PHP has no runtime router a package can hook intoSame either way
Idempotency (retry-safety)Your responsibility either way — the SDK can't know what "already delivered" means in your databaseYour responsibility either way

The SDK removes the parts that are pure transcription risk — signature scheme, field names, response envelope — and leaves untouched the parts that were never mechanical to begin with: your own database lookups, and which route dispatches to which handler. It's a smaller, more type-checked surface to get right, not a smaller amount of code you have to think about.

Which module do I need?

Same rule as the contract picker: it depends on your game's publishing model, not on when it was integrated.

  • Co-publishing (co-pub) game → the WebPay module. A player types their in-game UID by hand; no Alogame-linked account involved.
  • Exclusive/direct-publishing (expub) game → the Expub module, for both its web top-up page and its Mobile IAP purchases — a player is already logged into their Alogame account, which is what lets Alogame fetch a character list instead of asking them to type a UID.

A game is one or the other, never both — if you're not sure which, ask your Alogame Operations contact before writing any code.

WebPay module (Web Payment — Standard)

The flow

Alogame always initiates every call below. A player's top-up starts on the Alogame Portal — your server never calls out to Alogame, it only responds.

Request & response reference

Everything below is what actually goes over the wire — the SDK builds and parses all of it for you, but knowing the shape helps when you're reading logs or debugging a mismatch with Alogame Operations. Every call is POST, signed (x-timestamp + x-signature, see Authentication), and must be answered within 8 seconds — a slower response counts as a failure, and for onPaymentReceived that means a retry (see below).

onCheckUid — player enters their UID on the top-up screen

Wire endpoint: check_uid

CheckUidRequest propertyWire fieldTypeDescription
$uiduidstringIn-game character UID as typed by the player
$serverIdserver_idstring|nullOnly present if you implement ServerListHookInterface
$gameIdgame_idstring|nullOnly present for a partner Alogame splits into multiple store listings (rare — see ServerInfo/platform notes below)
{ "uid": "100002078" }

Return CheckUidResult::found($nickname, $server) or CheckUidResult::notFound(). The SDK turns that into the wire response for you:

// found
{ "errcode": 0, "msg": "success", "data": { "nickname": "DragonSlayer", "server": "Server 1" } }

// not found
{ "errcode": 1, "msg": "user not found" }
Why the exact string "user not found" matters

Alogame detects a missing character by matching msg case-insensitively against the substring "not found" or "not exist" — anything else is shown to the player as a generic system error instead of "check your UID". CheckUidResult::notFound() already returns compliant wording; you never write this string yourself.

onCreateOrder — player confirmed a package, before payment UI

Wire endpoint: create_order

CreateOrderRequest propertyWire fieldTypeDescription
$orderIdplat_order_numstringAlogame's order code — store it, it's the correlation key onPaymentReceived uses to find this order again
$uiduidstringSame UID onCheckUid already validated
$productIdproductidstringYour own product ID, exactly as submitted to Alogame
$amountamountintegerOrder total, VND, no decimals
$sandboxsandboxinteger (0/1)1 in dev/staging, 0 in production — Alogame sets this automatically from environment, it's not something you configure
$serverIdserver_idstring|nullOnly present if server-scoped
$gameIdgame_idstring|nullRare — see the note under onCheckUid
{
"uid": "100002078",
"plat_order_num": "260509161539010042",
"productid": "pack_500_gold",
"amount": 49000,
"sandbox": 0
}

Return CreateOrderResult::created($yourOwnOrderCode). $yourOwnOrderCode is required — the SDK's wire response always includes it:

{ "errcode": 0, "msg": "success", "data": { "order_num": "PARTNER-ORD-20260315-001" } }
Deduplicate on $request->orderId

If Alogame retries create_order (network hiccup, timeout) the same plat_order_num arrives twice. Return the same order_num you returned the first time — don't create a second order for one plat_order_num.

onPaymentReceived — payment provider confirmed payment, grant the item now

Wire endpoint: payment_notify

PaymentReceivedRequest propertyWire fieldTypeDescription
$orderIdplat_order_numstringAlogame's order code, same value as onCreateOrder
$orderCodeorder_numstringYour own order code, the one you returned from onCreateOrder
$amountamountintegerOrder total, VND
$gameIdgame_idstring|nullRare — see the note under onCheckUid

The wire body also carries a status field (always 1, "payment successful") — the SDK doesn't surface it on PaymentReceivedRequest because there is currently no other value it can take.

{
"plat_order_num": "260509161539010042",
"order_num": "PARTNER-ORD-20260315-001",
"amount": 49000,
"status": 1
}

Return PaymentReceivedResult::ok() after delivering the item, or PaymentReceivedResult::failed($reason):

// ok
{ "errcode": 0 }
Idempotency is required — this call is retried automatically

Anything other than PaymentReceivedResult::ok() — including your endpoint timing out — schedules a retry. Alogame makes up to 4 attempts total: the initial call, then retries after 60s, 120s, and 120s.

Match on $request->orderCode (or $request->orderId) and return ok() for an order you've already delivered — never grant the item twice, and never return failed() for an already-completed order, or the retry loop keeps running against an order that's actually done.

Prerequisites

Before you write any code, get from your Alogame Operations contact:

  • A shared HMAC secret_key — the only credential this module actually uses (passed straight into WebpayHandler's constructor). game_code itself never appears in any request or response body, and the SDK has no parameter for it — Alogame's own Console config is what ties your secret_key/base URL to a game, not anything you send.
  • Which environment (dev/staging vs prod) will call you first

1. Install

Public package, no credentials needed:

composer require alo-game/paymentsdk

Source: github.com/alo-game/alogame-paymentsdk-php (MIT licensed, CHANGELOG).

2. Implement the three hooks

The only interface you implement. Alogame calls these — you never call Alogame:

use Alogame\PaymentSdk\WebPay\Contracts\WebpayHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{
CheckUidRequest, CheckUidResult,
CreateOrderRequest, CreateOrderResult,
PaymentReceivedRequest, PaymentReceivedResult,
};

final class MyGameHooks implements WebpayHookInterface
{
public function onCheckUid(CheckUidRequest $request): CheckUidResult
{
$player = MyPlayerRepository::findByUid($request->uid);

return $player
? CheckUidResult::found($player->nickname, $player->serverName)
: CheckUidResult::notFound();
}

public function onCreateOrder(CreateOrderRequest $request): CreateOrderResult
{
// $request->orderId is Alogame's own reference (plat_order_num on
// the wire) — store it so onPaymentReceived can match this same
// order back to it.
$order = MyOrderRepository::create(
alogameOrderId: $request->orderId,
uid: $request->uid,
productId: $request->productId,
amount: $request->amount,
);

return CreateOrderResult::created($order->id);
}

public function onPaymentReceived(PaymentReceivedRequest $request): PaymentReceivedResult
{
$order = MyOrderRepository::findByOwnId($request->orderCode);
if ($order === null) {
return PaymentReceivedResult::failed('unknown order');
}

MyInventory::deliver($order->uid, $order->productId);

return PaymentReceivedResult::ok();
}
}
Both hooks must tolerate being called twice

onPaymentReceived is retried up to 4 times if it doesn't answer ok (including a timeout) — see Request & response reference above for the exact schedule and what to match on. onCreateOrder has the same requirement: a repeat with the same $request->orderId must return the same orderCode you returned the first time, not create a second order.

3. Wire the routes

Alogame calls three separate URLs, not one — handleCheckUid, handleCreateOrder and handlePaymentReceived each answer a different call, so something has to decide, per incoming request, which one to invoke. The SDK is deliberately framework-agnostic (it doesn't know Laravel's router from a bare $_SERVER['REQUEST_URI'] check), so that routing decision is the one piece of plumbing left to you — everything inside each branch below is already fully handled by the method you call. Plain PHP shown here; in Laravel/Symfony the match disappears and becomes three route definitions instead, each calling the same handler method:

use Alogame\PaymentSdk\WebPay\WebpayHandler;

$handler = new WebpayHandler(
secret: getenv('ALOGAME_WEBPAY_SECRET'),
hooks: new MyGameHooks(),
onError: fn (\Throwable $e) => error_log($e), // never let a bug leak to the wire
);

// Wire each to its own route — sign the raw body, never a re-parsed one
// (same rule as every other contract, see the Authentication page).
$response = match (true) {
$_SERVER['REQUEST_URI'] === '/webpay/check-uid' => $handler->handleCheckUid(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/webpay/create-order' => $handler->handleCreateOrder(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/webpay/payment-received' => $handler->handlePaymentReceived(getallheaders(), file_get_contents('php://input')),
};

http_response_code($response->status);
header('Content-Type: application/json');
echo json_encode($response->body);

4. Optional: multiple servers

Only needed if your game runs multiple servers and a player must pick one before topping up. Implement a second interface — Alogame detects it automatically, nothing to configure separately:

use Alogame\PaymentSdk\WebPay\Contracts\ServerListHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{GetServerListRequest, ServerInfo};

final class MyGameHooks implements WebpayHookInterface, ServerListHookInterface
{
// ...onCheckUid / onCreateOrder / onPaymentReceived as above...

public function onGetServerList(GetServerListRequest $request): array
{
return array_map(
static fn ($server) => new ServerInfo($server->id, $server->name),
MyServerRepository::all(),
);
}
}

Wire a fourth route to $handler->handleGetServerList(...). Skip this section entirely if your game has one shared server.

5. Wire a health check

One more route, wired once — the fourth if you skipped step 4, the fifth if you didn't:

$response = $handler->handleHealthCheck(getallheaders(), file_get_contents('php://input'));

Alogame can call this at any time — signed, same as every other call — to confirm your endpoint is reachable and the secret on file still matches. Give this path to Alogame Operations alongside the other three.

6. Give Alogame your endpoint

Send Alogame Operations, per environment:

  • Base URL + the four (or five, with the server list) paths
  • Confirmation that the secret_key from Prerequisites is wired in

Alogame Operations configures Console on their side — you don't touch Console yourself.

Where Alogame's responsibility ends

Alogame's side of this contract is exactly what the Request & response reference above describes: send the right data, in the right format, to the path you gave us, and read back whatever your response says — nothing more. What onCheckUid/onCreateOrder/onPaymentReceived do with the data in between (which database, which player, which order) is entirely internal to your backend, and Alogame has no way to see or verify it.

Before going live, Alogame's Console test tool calls your check-uid path with one UID you provide, and shows you the raw response. That only proves the path is reachable and the response is shaped correctly for that one call — it is not, and cannot be, a check that onCheckUid looks up an arbitrary UID correctly against your real player data. That correctness is entirely on you to verify before step 7, the same way you'd test any other part of your own backend.

7. Go live

Alogame switches your config to active. From this point every call is real player traffic.

Expub module (Web Payment — Expub + Mobile IAP)

Covers both an expub game's web top-up page and its Mobile IAP purchases — the two channels share createOrder_url/exchange_url entirely, so the same two hooks answer both.

The flow

The player reaches this flow only after logging into their Alogame account — unlike WebPay's co-pub flow, there is no UID typed by hand.

No check-uid step here

Unlike WebPay's co-pub flow, Alogame's own checkout never calls check-uid for an expub game — the player picked their character from onGetUserList's own response, so uid existence is already proven by construction. Some partners' backends have a standalone check-uid endpoint anyway (it predates this SDK, or their own tooling uses it) — see onCheckUid below, which is optional for exactly that reason.

Request & response reference

Every call is POST, signed (Signature header, MD5, timestamp in seconds — see Authentication), and errors are reported via HTTP status, not an errcode body field like WebPay.

onGetUserList — player opens the top-up screen

Wire endpoint: getUserList_url

GetUserListRequest propertyWire fieldTypeDescription
$userIduserIdstring (UUID)Alogame account ID — every character linked to this account should come back
$extInfoext_infostring|nullContext forwarded from the SDK
{ "userId": "550e8400-e29b-41d4-a716-446655440000", "ext_info": "{}" }

Return an array of UserCharacter. An empty array is valid — an account with no characters yet still returns 200:

{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"uids": [
{ "uid": "char_001", "server": "Server 1", "characterName": "DragonSlayer" }
]
}

onCheckUid — OPTIONAL, only if your backend already has this endpoint

Wire endpoint: checkUser_url

Optional — implement CheckUidHookInterface only if you need it

Not part of ExpubHookInterface. Alogame's own checkout flow never calls this for an expub game (see the note under The flow above) — ExpubHandler::handleCheckUid() answers 404 NOT_CONFIGURED unless your hooks class also implements CheckUidHookInterface. Implement it only if your backend already has a standalone check-uid endpoint for some other reason (predates this SDK, your own tooling calls it, etc.) — e.g. oe-007's ?ac=check_uid.

CheckUidRequest propertyWire fieldTypeDescription
$uiduidstringCharacter UID from the onGetUserList response
$extInfoext_infostring|nullContext forwarded from the SDK
{ "uid": "char_001", "ext_info": "{}" }

Return CheckUidResult::found($characterName, $server) or CheckUidResult::notFound():

// found — 200 OK
{ "characterName": "DragonSlayer", "server": "Server 1" }
Errors are HTTP status here, not errcode

CheckUidResult::notFound() produces a real 404, not a 200 with an error body — the opposite of WebPay's envelope above.

onCreateOrder — same hook serves web top-up and Mobile IAP

Wire endpoint: createOrder_url — shared with Mobile IAP

CreateOrderRequest propertyWire fieldTypeDescription
$orderIdorder_idstringAlogame's order code — store it, onPaymentReceived matches back to it
$uiduidstringCharacter UID from the onGetUserList response the player picked
$productIdproductIdstringYour own product ID
$amountpriceint|nullOrder total — branch on $osId to tell the channel apart, not on whether this is set
$serverIdserverIdstring|nullPresent if the game is server-scoped
$osIdos_idstring|null"ios"/"android" for a Mobile IAP purchase, null for a web top-up — the field that tells the two channels apart
$extInfoext_infostring|nullContext forwarded from the SDK
{ "order_id": "alo_ord_1a2b3c4d", "uid": "char_001", "productId": "pack_500_gold", "ext_info": "{}" }

Return CreateOrderResult::created($yourOwnOrderCode):

// 201 Created
{ "status": "success", "order_code": "PARTNER-ORD-20260315-001" }
Deduplicate on $request->orderId

If Alogame retries, return CreateOrderResult::duplicate($existingOrderCode) — the SDK answers 409 with the original order_code, never a new order.

onPaymentReceived — grant the item now

Wire endpoint: exchange_url — shared with Mobile IAP

PaymentReceivedRequest propertyWire fieldTypeDescription
$orderCodeorder_codestringYour own order code, from onCreateOrder
$extInfoext_infostring|nullContext forwarded from the SDK
{ "order_code": "PARTNER-ORD-20260315-001", "ext_info": "{}" }

Return PaymentReceivedResult::ok() after delivering the item:

// 200 OK
{ "processingStatus": "completed" }
Idempotency is required

Return PaymentReceivedResult::alreadyProcessed() (409) for an order you've already delivered — never grant twice, and never treat a repeat as orderCodeNotFound().

Prerequisites

  • A shared MD5 secret from your Alogame Operations contact — the only credential ExpubHandler takes.
  • Confirmation that this game is expub, not co-pub — see Which module do I need? above if unsure.

1. Install

Same package as WebPay above — no separate install:

composer require alo-game/paymentsdk

2. Implement the three required hooks (+ check-uid if you already have one)

use Alogame\PaymentSdk\Expub\Contracts\ExpubHookInterface;
use Alogame\PaymentSdk\Expub\Dto\{
GetUserListRequest, UserCharacter,
CreateOrderRequest, CreateOrderResult,
PaymentReceivedRequest, PaymentReceivedResult,
};

final class MyExpubGameHooks implements ExpubHookInterface
{
public function onGetUserList(GetUserListRequest $request): array
{
// Every character linked to this Alogame account — the player
// picks one on Alogame's side before topping up. Empty is valid
// (account linked, no characters yet).
return MyCharacterRepository::findAllByAlogameUserId($request->userId);
}

public function onCreateOrder(CreateOrderRequest $request): CreateOrderResult
{
// $request->osId is "ios"/"android" for a Mobile IAP purchase, null
// for a web top-up — same method, branch on it if delivery differs.
$existing = MyOrderRepository::findByAlogameOrderId($request->orderId);
if ($existing !== null) {
// Alogame retried — return the SAME order_code, never a new one.
return CreateOrderResult::duplicate($existing->orderCode);
}

$order = MyOrderRepository::create(
alogameOrderId: $request->orderId,
uid: $request->uid,
productId: $request->productId,
);

return CreateOrderResult::created($order->id);
}

public function onPaymentReceived(PaymentReceivedRequest $request): PaymentReceivedResult
{
$order = MyOrderRepository::findByOwnId($request->orderCode);
if ($order === null) {
return PaymentReceivedResult::orderCodeNotFound();
}

MyInventory::deliver($order->uid, $order->productId);

return PaymentReceivedResult::ok();
}
}
Both hooks must tolerate being called twice

Same requirement as WebPay above: onCreateOrder must answer a repeated $request->orderId with the same orderCode, and onPaymentReceived must answer an already-delivered $request->orderCode with ok(), never a second delivery.

3. Wire the routes

Four separate URLs, no health-check route for this contract (unlike WebPay) — Alogame doesn't call one for expub games today:

use Alogame\PaymentSdk\Expub\ExpubHandler;

$handler = new ExpubHandler(
secret: getenv('ALOGAME_EXPUB_SECRET'),
hooks: new MyExpubGameHooks(),
onError: fn (\Throwable $e) => error_log($e),
);

$response = match (true) {
$_SERVER['REQUEST_URI'] === '/expub/get-user-list' => $handler->handleGetUserList(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/check-uid' => $handler->handleCheckUid(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/create-order' => $handler->handleCreateOrder(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/payment-received' => $handler->handlePaymentReceived(getallheaders(), file_get_contents('php://input')),
};

http_response_code($response->status);
header('Content-Type: application/json');
echo json_encode($response->body);

Wire the check-uid route unconditionally, whether or not you implement CheckUidHookInterfacehandleCheckUid() itself answers 404 NOT_CONFIGURED when your hooks class doesn't, which is the correct response either way.

4. Optional: check-uid

Only needed if your backend already has a standalone check-uid endpoint for some other reason — Alogame's own checkout never calls this for an expub game (see The flow above). Implement a second interface — ExpubHandler detects it automatically, nothing to configure separately:

use Alogame\PaymentSdk\Expub\Contracts\CheckUidHookInterface;
use Alogame\PaymentSdk\Expub\Dto\{CheckUidRequest, CheckUidResult};

final class MyExpubGameHooks implements ExpubHookInterface, CheckUidHookInterface
{
// ...onGetUserList / onCreateOrder / onPaymentReceived as above...

public function onCheckUid(CheckUidRequest $request): CheckUidResult
{
$character = MyCharacterRepository::findByUid($request->uid);

return $character
? CheckUidResult::found($character->name, $character->server)
: CheckUidResult::notFound();
}
}

Skip this section entirely if your backend has no reason to expose one.

5. Give Alogame your endpoint

Send Alogame Operations, per environment, the base URL + all four paths, plus confirmation that the secret from Prerequisites is wired in. Alogame Operations configures Console on their side.

6. Go live

Alogame switches your config to active. From this point every call — both web top-up and Mobile IAP — is real player traffic.

Upgrading

Check the CHANGELOG before running composer update — this package runs inside your production backend, the same way any other dependency does. The two modules version independently within the same package; the CHANGELOG notes which one a given release actually touched.

Implementation checklist

WebPay module:

  • composer require alo-game/paymentsdk succeeds
  • onCheckUid, onCreateOrder, onPaymentReceived implemented against your own data
  • onCreateOrder and onPaymentReceived are safe to receive the same order twice
  • Three (or four, with server list) routes wired, each passing the raw request body
  • Health-check route wired and its path sent to Alogame Operations
  • Base URL + paths + secret confirmation sent to Alogame Operations
  • Passed Alogame's pre-go-live test call

Expub module:

  • composer require alo-game/paymentsdk succeeds
  • onGetUserList, onCreateOrder, onPaymentReceived implemented against your own data
  • onCheckUid implemented + CheckUidHookInterface added — only if your backend already has a standalone check-uid endpoint
  • onCreateOrder handles a repeated order_id as duplicate(), not a new order
  • onPaymentReceived handles a repeated order_code as alreadyProcessed(), not a second delivery
  • Four routes wired, each passing the raw request body
  • Base URL + all four paths + secret confirmation sent to Alogame Operations
  • Mobile IAP wired against the same createOrder_url/exchange_url, if this game has a mobile app