OctoWiki
Vendor AdaptersTop-up Vendors

SEAGM

Direct-topup vendor adapter — gaming recharge, HMAC-SHA256 query-string signing, async with signed inbound webhook and a rich failure taxonomy.

Quick facts

Code SEAGM · Line direct-topup (gaming) · Auth HMAC-SHA256 query-string signing · Model async (create → poll/webhook) · Webhook signed (same HMAC) · Lookup none · Adapter services/external_vendors/direct_topup/seagm/

Gaming direct-topup (PUBG UC, BIGO, Tango) against the SEAGM Open API. Implements DirectTopUpVendor. Registered direct_topup/factory.go:176. See Order Lifecycle.

Auth — HMAC-SHA256 in the query string (headline)

Unlike most adapters, SEAGM signs via the query string, not headers. addAuth sets uid, timestamp (now.Unix()), and signature as query params on every call. The signing string covers every param — query + form body — except signature, sorted byte-wise, joined k=v&… with both key and value URL-encoded, then hex(HMAC_SHA256(string, secret_key)). The only header set is Content-Type: application/x-www-form-urlencoded. vendor_attributes: host, uid, secret_key (both in Credentials), optional callback_url. Inbound-webhook timestamp drift window is ±120s.

Catalog

  • GetCategories — signed GET /v1/recharge-categories (all active, TopupType:"GAMING"), with an N+1 GET /v1/product-category-info per category to fill image/description.
  • GetProducts(categoryID) — signed GET /v1/recharge-categories/{id}/recharge-types (lazy-loaded per category; ShouldSyncVariants()=false).

par_value ≠ money. MinAmount/MaxAmount/Price are set from unit_price (what the wallet debits), NOT par_value (the in-game currency count, e.g. 60 for "60 UC") — using par_value as money once caused a ~60× overcharge. VendorDiscount = ((origin_price − unit_price)/origin_price)×100, matching the DT One convention. par_value is surfaced as a display-only variant field. Input-field labels are derived from field names (via humanizeFieldName) because SEAGM ships duplicate/redundant labels.

Order create

POST /v1/recharge-orders (form-urlencoded): type_id, buy_amount="1" always, mch_order_id, plus InputData (e.g. playerid). Async (pay_status_code=2, send_status_code=1). Duplicate mch_order_id (info_code 20135) → ErrDuplicateMerchantRef. SEAGM returns its error envelope in the body even on HTTP 4xx, so terminal info_codes are promoted to FAILED.

Order status / poll

Signed GET /v1/recharge-orders/{id} with dual lookup via ?query_type=: orderId (SEAGM id) or mchOrderId (our merchant ref → backs SupportsExternalOrderRef()=true). Not-found: code 404 / info_code 20080ErrOrderNotFound. Status codes: 10000→PENDING, 10001/10002→PROCESSING, 10003→COMPLETE, 10004→FAILED, 10005→CANCELLED.

Webhook

Inbound JSON callback. VerifyWebhookSignature flattens the body, requires a timestamp, enforces ±120s, and recomputes the same HMAC (constant-time compare, identical rule to outbound). ParseWebhook maps status_code, id, mch_order_id, trade_id, sent_time.

Delivery / balance

No separate confirm call — terminal state arrives via webhook or poll; CompletedAt from sent_time. SupportsLookup()=false. GetBalance = signed GET /v1/me (Ping reuses it).

Failure taxonomy & quirks

⚠️ Security: secret logged in plaintext. signedPost has a "TEMP DIAGNOSTIC" block that logs secret_key, signature, the signing string, and the full body at Info level ("Remove after the prod issue is resolved"). It leaks the vendor secret to logs and signals an unresolved production issue (POST signatures rejected while GETs work). Flag for cleanup.

error_messages.go maps only terminal, customer-clear info_codes to canonical FailureCodes (INVALID_RECIPIENT, RECIPIENT_BARRED, PRODUCT_UNAVAILABLE/OUT_OF_STOCK, AMOUNT_OUT_OF_RANGE, …); absence of a mapping = leave PENDING for the retry job. User-fixable codes surface SEAGM's own message; non-user-fixable use canonical copy (so supplier detail doesn't leak). Sentinels 20135/20080 handled before the map. vendorhttp returns the body even on 4xx/5xx, which the adapter relies on to parse the error envelope.

Tests, mock, docs

Unit (error_messages_test.go) + contract (seagm_contract_test.go, build tag contract) against mocky (http://localhost:8788/seagm). Docs + Postman (prod openapi.seagm.com, sandbox openapi.seagm.io) under docs/seagm/.

On this page