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
Latest version2.2.0 (CHANGELOG)
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 or HMAC-SHA256 (whichever your game uses in Console), 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).

onGetServerList — player opens the top-up screen (optional)​

Wire endpoint: get_server_list

Only called at all if your hooks object implements ServerListHookInterface — skip this whole subsection if your game has one shared server. Alogame detects the interface, not a config flag, so there's nothing to toggle on your end besides implementing it.

GetServerListRequest propertyWire fieldTypeDescription
$gameIdgame_idstring|nullRare — see the note under onCheckUid below
{}

Return an array of ServerInfo instances. The SDK turns that into the wire response for you:

{
"errcode": 0,
"data": [
{ "serverId": "1", "name": "Server 1" },
{ "serverId": "2", "name": "Server 2" }
]
}

There is no failure case to signal here — an empty array is a valid answer (the player simply sees no server picker), so onGetServerList has no notFound()-style counterpart the other hooks have.

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.

onGetCharacterList — UID validated, player picks a character (optional)​

Wire endpoint: get_character_list
Requires SDK ≥ 2.1.0 — run composer update alo-game/paymentsdk first if you are on 2.0.x, or the interface below won't exist.

Only called at all if your hooks object implements CharacterListHookInterface — skip this whole subsection if a uid identifies exactly one character in your game. Independent of ServerListHookInterface: implement either, both, or neither.

GetCharacterListRequest propertyWire fieldTypeDescription
$uiduidstringThe UID onCheckUid has already validated — the list is scoped to it
$serverIdserver_idstring|nullOnly present if you also implement ServerListHookInterface
$gameIdgame_idstring|nullRare — see the note under onCheckUid above
{ "uid": "100002078", "server_id": "1" }

Return an array of CharacterInfo instances. The SDK turns that into the wire response for you:

{
"errcode": 0,
"data": [
{ "characterId": "c-1001", "name": "DragonSlayer" },
{ "characterId": "c-1002", "name": "IronMage" }
]
}

There is no failure case to signal here, for the same reason onGetServerList has none: a uid that doesn't exist was already rejected by onCheckUid, and an empty array for a valid uid is a legitimate answer (the player simply sees no character picker). Whichever characterId the player picks arrives on the next onCreateOrder call as $request->characterId.

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
$characterIdcharacter_idstring|nullSDK ≥ 2.1.0. Only present if you implement CharacterListHookInterface — the character the player picked. Deliver to this character, not to whatever $uid alone resolves to
$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. Optional: several characters per UID​

Requires SDK ≥ 2.1.0. Only needed if one uid of yours can own more than one character and the player must pick which one receives the top-up. Independent of step 4 — implement either, both, or neither. Same detection mechanism: Alogame reads the interface, not a config flag.

use Alogame\PaymentSdk\WebPay\Contracts\CharacterListHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{CharacterInfo, GetCharacterListRequest};

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

public function onGetCharacterList(GetCharacterListRequest $request): array
{
return array_map(
static fn ($character) => new CharacterInfo($character->id, $character->name),
MyCharacterRepository::forUid($request->uid, $request->serverId),
);
}
}

Wire another route to $handler->handleGetCharacterList(...). The picked value then arrives on the same onCreateOrder call as $request->characterId — deliver to that character. Skip this section entirely if a uid identifies exactly one character.

6. Wire a health check​

One more route, wired once — after however many of steps 4 and 5 apply to your game:

$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 and signed with the strategy your game uses in Console. ExpubHandler detects which one from each request's headers — there is nothing to configure in code:

StrategyWhat Alogame sendsHow outcomes are answered
MD5 (existing expub titles)Signature header, timestamp in seconds as a body field — see AuthenticationHTTP status per outcome, bodies as shown below
HMAC-SHA256 (recommended for a new integration, SDK ≥ 2.2.0)x-timestamp in milliseconds + x-signature headers, no body timestamp — see AuthenticationAlways 200, with {"errcode", "msg", "data"}

The hooks and DTOs are identical under both — only the wire response differs, and the SDK builds it. Under HMAC:

Hook resultWire response
onGetUserList → characters{"errcode":0,"msg":"success","data":[{"uid","characterName","server"}]}
CheckUidResult::found(...) / notFound(){"errcode":0,...,"data":{"characterName","server"}} / {"errcode":1,"msg":"uid not found"}
CreateOrderResult::created($code) or duplicate($code){"errcode":0,"msg":"success","data":{"order_num":"<code>"}}
CreateOrderResult::productNotFound() / uidNotFound(){"errcode":1,"msg":"product not found"} / {"errcode":1,"msg":"uid not found"}
PaymentReceivedResult::ok() or alreadyProcessed(){"errcode":0,"msg":"success"}
PaymentReceivedResult::orderCodeNotFound(){"errcode":1,"msg":"order_code not found"}

A duplicate order and an already-delivered payment are errcode 0 on purpose: they are retries of something that already succeeded. Web top-up and Mobile IAP also spell some fields differently under HMAC (order_id vs plat_order_num, productId vs productid, order_code vs order_num, price vs amount) — the SDK accepts both, so your hooks always see the same DTO properties.

Upgrading an HMAC game from SDK ≤ 2.1

Before 2.2.0 ExpubHandler verified MD5 only. An HMAC game on an older version gets 401 SIGNATURE_INVALID ("timestamp" missing or expired) on every call, because the HMAC timestamp is a header, not a body field. Run composer update alo-game/paymentsdk — no code change needed.

onGetUserList — player opens the top-up screen​

Wire endpoint: getUserList_url

GetUserListRequest propertyWire fieldTypeDescription
$userIduserIdstringAlogame account's numeric id — the same userId the Alogame client SDK returns to your game at login, not its uuid. Every character linked to this account should come back
$extInfoext_infostring|nullContext forwarded from the SDK
{ "userId": "1234", "ext_info": "{}" }

Return an array of UserCharacter. An empty array is valid — an account with no characters yet still returns 200. MD5 response (for HMAC see the table above):

{
"userId": "1234",
"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 secret (used for MD5 or HMAC alike) 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 CheckUidHookInterface — handleCheckUid() 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.

Current: 2.2.0. Additive over 2.1.x — nothing to migrate, and every existing hook keeps the same signature. The Expub module now also serves a game signing with HMAC-SHA256 (see Request & response reference); an MD5 game sees no change.

2.1.0 added the optional onGetCharacterList hook (plus $characterId on onCreateOrder) to the WebPay module, for games where one UID owns several characters.

handleHealthCheck() reports the WebPay module's own contract version (1.1.0 as of package 2.1.0, up from 1.0.0) — that is what Alogame support reads to tell whether your deployment already supports the character list.

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 routes wired (plus one per optional picker — server list, character list), 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