MAS4GAMES

Developers · Version v1

Mas4games Dealer API

Sell our products automatically on your site, in your app or in your panel. Orders are paid from your balance at once and codes arrive in seconds; every step is predictable, documented and testable.

API address

Base URL
https://api.mas4games.com/v1
On this page

Quick start

Place your first order in three steps.

  1. 1

    Get your keys

    Our support team creates a test key (m4gc_test_…) and a live key (m4gc_live_…) for your account; each comes with a webhook signing secret (whsec_…).

  2. 2

    Try it in the sandbox

    With the test key you get the real catalog and prices, while orders are delivered with made-up codes and no money or stock moves.

  3. 3

    Go live

    Run the same code with the live key. Orders are paid from your balance and the real codes come through GET /orders/{id} and the webhook.

Request
curl https://api.mas4games.com/v1/me \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "account",
  "id": 18452,
  "name": "Örnek Bayi",
  "email": "[email protected]",
  "email_verified": true,
  "customer_type": "Bayi",
  "created_at": "2024-03-12T09:41:00Z",
  "currency": "TRY",
  "currency_decimals": 2,
  "balances": [
    { "amount": 1245075, "currency": "TRY", "decimals": 2 }
  ],
  "key": {
    "mode": "live",
    "created_at": "2026-11-06T08:00:00Z",
    "allowed_ips": ["203.0.113.10/32"],
    "webhook_url": "https://bayi.example.com/webhooks/mas4games"
  }
}

Authentication

Send your secret key in the Authorization header of every request:

HTTP
Authorization: Bearer m4gc_live_YOUR_SECRET_KEY
  • A key's start tells its environment: m4gc_live_ real orders, m4gc_test_ the sandbox. The two environments' orders are kept apart.
  • Keep the key on your server only; never in a browser, a mobile app or a code repository.
  • On request we limit your key to your servers' IP addresses; a request from anywhere else is refused with 401.
  • If a key leaks, have it renewed at once: the old one stops working the moment the new one is issued. Its webhook signing secret is renewed with it and its webhook address is cleared: set it again with the new key (PUT /webhook).

Sandbox

Requests with a test key use the real catalog, prices and rules; orders are simulated: no balance is charged and no stock changes.

Code products are delivered at once with clearly marked made-up codes: TEST-7F3A-91C2-0D44.

With a test key, GET /me shows one made-up balance in the key's currency: 1000000 in its smallest unit (10,000.00 in a currency with two decimals), which never changes. GET /balance/transactions answers an empty list.

To try errors and states, start the order's reference like this:

referenceMeaning and what to do
test_insufficient_balance…The order is refused with 402 insufficient_balance.
test_cancelled…The lines come back cancelled.
test_processing…The lines stay processing, to try the cancel endpoint.

Webhooks are sent for test orders too; the events' mode is test.

A test key places at most 500 test orders in 24 hours; beyond that POST /orders answers 429 rate_limited, and Retry-After gives the seconds until your oldest test order of the last 24 hours turns a day old and makes room for another. An Idempotency-Key refused this way can be sent again then.

Requests and responses

The API follows REST conventions and uses the same shapes everywhere.

  • Bodies are JSON with snake_case field names. A successful answer is the resource itself; an error answer is an error object.
  • Amounts are whole numbers in the smallest unit: {"amount": 41990, "currency": "TRY"} means 419.90 TL. No fractions are ever used, so there are no rounding errors. The number of decimals is currency_decimals in GET /me.
  • Dates are UTC in ISO-8601: 2026-11-06T08:12:40Z.
  • Lists return object: "list", data, has_more and next_cursor. For the next page send next_cursor as cursor; limit is 1–200 (default 50).
  • Every answer has an X-Request-Id header; include it when you write to support.
  • Each key may make 120 requests a minute. What is left is in X-RateLimit-Remaining; above it you get 429 with Retry-After. Some calls have limits of their own: POST /webhook/test 30 times an hour per key, and a test key's orders 500 in 24 hours (both 429 rate_limited with Retry-After).

Safe retries (idempotency)

Use the Idempotency-Key header on POST requests so you can safely send an order again when the network drops.

  • Make a unique value (a UUID, for example) for every new operation. A second request with the same key opens no new order: it returns the first answer as it was, with the header Idempotent-Replayed: true.
  • Keys are kept 24 hours. The same key with a different body returns 422 idempotency_key_reused; while the first request is still running, 409 request_in_progress.
  • An order's reference is unique too: a second order with the same reference returns 409 duplicate_reference, and the error's order_id names the first order.
HTTP
Idempotency-Key: 4f9c1d2e-8a7b-4c3d-9e1f-2a3b4c5d6e7f

Errors

Errors come with the right HTTP status and a stable code. Build your logic on code, not on message; param points to the field at fault.

HTTP
HTTP/1.1 402 Payment Required
X-Request-Id: req_8c4f2a1e9b7d6c5a4b3e2f1d

{
  "error": {
    "type": "balance_error",
    "code": "insufficient_balance",
    "message": "Your balance in this currency does not cover the order.",
    "request_id": "req_8c4f2a1e9b7d6c5a4b3e2f1d"
  }
}
422 JSON
{
  "error": {
    "type": "order_error",
    "code": "field_invalid",
    "message": "\"playerId\" is missing or not valid for this product (see its type, min_length, max_length and options).",
    "param": "items[0].fields.playerId",
    "request_id": "req_0b1c2d3e4f5a6b7c8d9e0f1a"
  }
}
CodeHTTPMeaning and what to do
invalid_api_key401The key is missing, wrong, switched off, or not usable from this IP.
rate_limited429Too many requests: 120 a minute per key, 30 webhook tests an hour, or 500 orders in 24 hours with a test key. With each of them, Retry-After gives the seconds to wait.
route_not_found404No such endpoint; check the address.
invalid_json400The body is not valid JSON.
body_too_large413The body is larger than 256 KB.
parameter_missing400A required parameter is missing (see param).
parameter_invalid400A parameter's value is not valid (see param).
field_unknown400A field the product doesn't ask for was sent; see the product's fields.
field_invalid422A field is missing or breaks its rules (type, min_length, max_length, options).
product_not_found404No such product, or it is not on sale to you.
product_unavailable409The product is not on sale now.
out_of_stock409There is not enough stock for this quantity.
quantity_out_of_range422The quantity is outside the product's min_quantity and max_quantity.
outside_sale_hours422The product is not sold at this hour (sale_hours).
daily_limit_reached422The daily purchase limit for this product is reached.
spending_limit_reached422The amount or daily limit for paying from the balance is reached.
insufficient_balance402Your balance does not cover the order; top it up.
account_restricted403Your account cannot buy this product.
email_unverified403The account's e-mail address is not confirmed.
billing_details_required422Billing details (identity or tax number) are required for this order.
balance_payment_unavailable403Paying from the balance in this currency is not open to your account.
duplicate_reference409An order with this reference already exists (order_id).
idempotency_key_reused422The Idempotency-Key was used with a different request.
request_in_progress409The first request with this Idempotency-Key is still running; try again shortly.
order_not_found404No such order, or it is not yours.
item_not_found404No such line in this order.
order_not_cancellable409The order, or the lines named, can't be cancelled; the message says why: a line is delivered, already cancelled or being bought from a supplier; an order whose lines go together (a coupon, a bundle, a buy-together price, a free gift) was to be cancelled only in part or has a line delivered; or the order is held for payment review or handled by our staff.
webhook_not_set409Set a webhook address first.
order_failed422The order could not be placed; write to support with the request_id.
maintenance503The store is under maintenance; try again shortly.
service_unavailable503The service is briefly not answering; try again.
internal_error500Something went wrong on our side; write to support with the request_id. When you retry an order, send the same Idempotency-Key.

An order's life

An order is made of lines; each line has its own status, and the order's status follows from its lines.

Line statuses

processing
Delivery under way. Most code products are delivered within seconds; top-ups by ID may take a few minutes.
delivered
Delivered. Codes are in codes; delivery_note is the delivery's note (a top-up by ID's transaction number, say), or null. In the sandbox a top-up by ID gets a note like TEST-TX-….
cancelled
Cancelled; its amount went back to your balance (cancel_reason).

Order statuses

processing
At least one line is processing.
completed
Every line is delivered.
partially_completed
Some lines were delivered, the others cancelled.
cancelled
Every line was cancelled.

For your security codes appear only in GET /orders/{id} and in webhooks; in the answers to creating and listing orders codes is null, and codes_count says how many there are.

Endpoints

Every address is under the API address.

Account

Account

GET/v1/me

Returns your account, your balances and the key in use (its mode, allowed IPs and webhook address). With a test key the balance is the sandbox's made-up one (1000000 in the key's currency).

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Request
curl https://api.mas4games.com/v1/me \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "account",
  "id": 18452,
  "name": "Örnek Bayi",
  "email": "[email protected]",
  "email_verified": true,
  "customer_type": "Bayi",
  "created_at": "2024-03-12T09:41:00Z",
  "currency": "TRY",
  "currency_decimals": 2,
  "balances": [
    { "amount": 1245075, "currency": "TRY", "decimals": 2 }
  ],
  "key": {
    "mode": "live",
    "created_at": "2026-11-06T08:00:00Z",
    "allowed_ips": ["203.0.113.10/32"],
    "webhook_url": "https://bayi.example.com/webhooks/mas4games"
  }
}

Possible errors:invalid_api_keyrate_limited

Catalog

List categories

GET/v1/categories

Returns the categories open to you. Sub-categories point to their parent with parent_id.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Request
curl https://api.mas4games.com/v1/categories \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "list",
  "data": [
    { "id": 3, "slug": "pubg-mobile", "name": "PUBG Mobile", "parent_id": null, "image_url": "https://www.mas4games.com/uploads/catalog/pubg.webp" },
    { "id": 5, "slug": "google-play", "name": "Google Play", "parent_id": null, "image_url": "https://www.mas4games.com/uploads/catalog/google-play.webp" }
  ],
  "has_more": false,
  "next_cursor": null
}

Possible errors:invalid_api_keyrate_limited

List products

GET/v1/products

Returns the products open to you at your own prices, ordered by product id. With updated_since you get only those whose price, stock or availability changed (changes are processed every five minutes).

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
limitoptionalquery · integerRecords per page, 1–200 (default 50)
cursoroptionalquery · stringThe previous answer's next_cursor
category_idoptionalquery · integerOnly this category's products (and its sub-categories')
idsoptionalquery · stringComma-separated product ids (at most 200)
updated_sinceoptionalquery · date-timeOnly those changed after this time (ISO-8601)
Request
curl https://api.mas4games.com/v1/products?limit=100 \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "list",
  "data": [
    {
      "object": "product",
      "id": 241,
      "sku": "pubg-mobile-660-uc",
      "name": "PUBG Mobile 660 UC",
      "names": { "tr": "PUBG Mobile 660 UC", "en": "PUBG Mobile 660 UC", "ar": "PUBG Mobile 660 UC" },
      "category": { "id": 3, "name": "PUBG Mobile" },
      "type": "topup",
      "region": "global",
      "price": { "amount": 41990, "currency": "TRY" },
      "list_price": { "amount": 44990, "currency": "TRY" },
      "available": true,
      "stock": null,
      "min_quantity": 1,
      "max_quantity": 10,
      "sale_hours": null,
      "fields": [
        {
          "key": "playerId",
          "labels": { "tr": "Oyuncu ID", "en": "Player ID", "ar": "معرّف اللاعب" },
          "type": "digits",
          "required": true,
          "min_length": 5,
          "max_length": 64,
          "options": null
        }
      ],
      "image_url": "https://www.mas4games.com/uploads/catalog/pubg.webp",
      "description": null,
      "updated_at": "2026-11-06T07:55:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "MjQx"
}

Possible errors:invalid_api_keyrate_limitedparameter_invalid

Retrieve a product

GET/v1/products/{id}

Returns one product. Check its price and availability here before high-value orders.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
idrequiredpath · integerThe product's id
Request
curl https://api.mas4games.com/v1/products/241 \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "product",
  "id": 241,
  "sku": "pubg-mobile-660-uc",
  "name": "PUBG Mobile 660 UC",
  "names": { "tr": "PUBG Mobile 660 UC", "en": "PUBG Mobile 660 UC", "ar": "PUBG Mobile 660 UC" },
  "category": { "id": 3, "name": "PUBG Mobile" },
  "type": "topup",
  "region": "global",
  "price": { "amount": 41990, "currency": "TRY" },
  "list_price": { "amount": 44990, "currency": "TRY" },
  "available": true,
  "stock": null,
  "min_quantity": 1,
  "max_quantity": 10,
  "sale_hours": null,
  "fields": [
    {
      "key": "playerId",
      "labels": { "tr": "Oyuncu ID", "en": "Player ID", "ar": "معرّف اللاعب" },
      "type": "digits",
      "required": true,
      "min_length": 5,
      "max_length": 64,
      "options": null
    }
  ],
  "image_url": "https://www.mas4games.com/uploads/catalog/pubg.webp",
  "description": null,
  "updated_at": "2026-11-06T07:55:00Z"
}

Possible errors:invalid_api_keyrate_limitedproduct_not_found

Orders

Create an order

POST/v1/orders

Pays the order from your balance at once and starts delivery; returns the order with 201. If a line can't be ordered, no order is made and the error names the line in param. A test key places at most 500 test orders in 24 hours (429 rate_limited with Retry-After).

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Idempotency-Keyoptionalheader · stringA unique value for this operation (1–255 characters); the same one on a retry
referenceoptionalbody · stringYour own order number; unique per key (1–100 characters)
itemsrequiredbody · arrayThe order's lines (1–20)
items[].product_idrequiredbody · integerThe product's id
items[].quantityoptionalbody · integerQuantity (default 1; within the product's min_quantity–max_quantity)
items[].fieldsoptionalbody · objectValues for the fields in the product's fields list, by key
Request
curl -X POST https://api.mas4games.com/v1/orders \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d2c8e-SIP-10045" \
  -d '{
  "reference": "SIP-10045",
  "items": [
    {
      "product_id": 241,
      "quantity": 1,
      "fields": { "playerId": "5123456789" }
    },
    {
      "product_id": 77,
      "quantity": 2
    }
  ]
}'
Response
{
  "object": "order",
  "id": "M4G105146",
  "reference": "SIP-10045",
  "mode": "live",
  "source": "api",
  "status": "processing",
  "currency": "TRY",
  "total": { "amount": 61990, "currency": "TRY" },
  "refunded": { "amount": 0, "currency": "TRY" },
  "created_at": "2026-11-06T08:12:40Z",
  "items": [
    {
      "id": 5614,
      "product_id": 241,
      "product_name": "PUBG Mobile 660 UC",
      "quantity": 1,
      "unit_price": { "amount": 41990, "currency": "TRY" },
      "amount": { "amount": 41990, "currency": "TRY" },
      "gift": false,
      "status": "processing",
      "fields": { "playerId": "5123456789" },
      "codes": null,
      "codes_count": 0,
      "delivery_note": null,
      "delivered_at": null,
      "cancel_reason": null
    },
    {
      "id": 5615,
      "product_id": 77,
      "product_name": "Xbox 100 TL Hediye Kartı",
      "quantity": 2,
      "unit_price": { "amount": 10000, "currency": "TRY" },
      "amount": { "amount": 20000, "currency": "TRY" },
      "gift": false,
      "status": "delivered",
      "fields": {},
      "codes": null,
      "codes_count": 2,
      "delivery_note": null,
      "delivered_at": "2026-11-06T08:12:41Z",
      "cancel_reason": null
    }
  ]
}

Possible errors:invalid_api_keyrate_limitedinvalid_jsonparameter_missingparameter_invalidfield_unknownfield_invalidproduct_not_foundproduct_unavailableout_of_stockquantity_out_of_rangeoutside_sale_hoursdaily_limit_reachedspending_limit_reachedinsufficient_balanceaccount_restrictedemail_unverifiedbilling_details_requiredbalance_payment_unavailableduplicate_referenceidempotency_key_reusedrequest_in_progressorder_failedmaintenance

Retrieve an order

GET/v1/orders/{id}

Returns the order with the codes of its delivered lines. Without webhooks, follow delivery with this endpoint.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
idrequiredpath · stringThe order's id (M4G…; TST… in the sandbox)
Request
curl https://api.mas4games.com/v1/orders/M4G105146 \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "order",
  "id": "M4G105146",
  "reference": "SIP-10045",
  "mode": "live",
  "source": "api",
  "status": "completed",
  "currency": "TRY",
  "total": { "amount": 61990, "currency": "TRY" },
  "refunded": { "amount": 0, "currency": "TRY" },
  "created_at": "2026-11-06T08:12:40Z",
  "items": [
    {
      "id": 5614,
      "product_id": 241,
      "product_name": "PUBG Mobile 660 UC",
      "quantity": 1,
      "unit_price": { "amount": 41990, "currency": "TRY" },
      "amount": { "amount": 41990, "currency": "TRY" },
      "gift": false,
      "status": "delivered",
      "fields": { "playerId": "5123456789" },
      "codes": [],
      "codes_count": 0,
      "delivery_note": "L8RZvemd2K2d39qfS7",
      "delivered_at": "2026-11-06T08:13:05Z",
      "cancel_reason": null
    },
    {
      "id": 5615,
      "product_id": 77,
      "product_name": "Xbox 100 TL Hediye Kartı",
      "quantity": 2,
      "unit_price": { "amount": 10000, "currency": "TRY" },
      "amount": { "amount": 20000, "currency": "TRY" },
      "gift": false,
      "status": "delivered",
      "fields": {},
      "codes": ["XB1A-7KQ2-PL9D-3M2X", "XB1A-8LR3-QM0E-4N3Y"],
      "codes_count": 2,
      "delivery_note": null,
      "delivered_at": "2026-11-06T08:12:41Z",
      "cancel_reason": null
    }
  ]
}

Possible errors:invalid_api_keyrate_limitedorder_not_found

List orders

GET/v1/orders

Returns your orders, newest first (orders placed on the site come too, with source: store). Codes are not in the list.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
limitoptionalquery · integerRecords per page, 1–200 (default 50)
cursoroptionalquery · stringThe previous answer's next_cursor
statusoptionalquery · stringprocessing, completed, partially_completed or cancelled
referenceoptionalquery · stringThe order placed with this reference
created_afteroptionalquery · date-timeCreated at or after this time
created_beforeoptionalquery · date-timeCreated before this time
Request
curl "https://api.mas4games.com/v1/orders?status=completed&created_after=2026-11-01T00:00:00Z&limit=50" \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "list",
  "data": [
    { "object": "order", "id": "M4G105146", "reference": "SIP-10045", "status": "completed", "…": "…" }
  ],
  "has_more": false,
  "next_cursor": null
}

Possible errors:invalid_api_keyrate_limitedparameter_invalid

Cancel an order

POST/v1/orders/{id}/cancel

Cancels lines not delivered yet and refunds their amounts to your balance at once. Without items, every cancellable line is cancelled. An order whose lines go together (a coupon on the order, a bundle, a buy-together price, a free gift line or the line that earned it) is cancelled whole or not at all: leave items out (or list all of its lines); naming only some of them, or having any of its lines delivered or being bought from a supplier, is 409 order_not_cancellable with a message saying why. Orders held for payment review or handled by our staff are never cancelled through the API: contact support. Other orders keep the per-line cancel. Only orders paid from your balance (every API order is) can be cancelled.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Idempotency-Keyoptionalheader · stringA unique value for this operation (1–255 characters); the same one on a retry
idrequiredpath · stringThe order's id (M4G…; TST… in the sandbox)
itemsoptionalbody · arrayIds of the lines to cancel; without it, every cancellable line (in an order whose lines go together: all of them or none). An empty list is refused with parameter_invalid
reasonoptionalbody · stringWhy it is cancelled (at most 200 characters)
Request
curl -X POST https://api.mas4games.com/v1/orders/M4G105146/cancel \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-M4G105146" \
  -d '{ "items": [5614], "reason": "Customer changed their mind" }'
Response
{
  "object": "order",
  "id": "M4G105146",
  "status": "partially_completed",
  "refunded": { "amount": 41990, "currency": "TRY" },
  "items": [
    { "id": 5614, "status": "cancelled", "cancel_reason": "Customer changed their mind", "…": "…" },
    { "id": 5615, "status": "delivered", "…": "…" }
  ]
}

Possible errors:invalid_api_keyrate_limitedinvalid_jsonparameter_invalidorder_not_founditem_not_foundorder_not_cancellableidempotency_key_reusedrequest_in_progress

Balance

Balance transactions

GET/v1/balance/transactions

Returns your balance's movements (purchases, refunds, top-ups), newest first, for reconciliation. With a test key the list is always empty: the sandbox moves no money.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
limitoptionalquery · integerRecords per page, 1–200 (default 50)
cursoroptionalquery · stringThe previous answer's next_cursor
currencyoptionalquery · stringCurrency (default: your key's currency)
Request
curl "https://api.mas4games.com/v1/balance/transactions?limit=20" \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "list",
  "data": [
    {
      "object": "balance_transaction",
      "id": 90211,
      "type": "purchase",
      "amount": { "amount": -61990, "currency": "TRY" },
      "balance_after": { "amount": 1183085, "currency": "TRY" },
      "order_id": "M4G105146",
      "created_at": "2026-11-06T08:12:40Z"
    }
  ],
  "has_more": true,
  "next_cursor": "OTAyMTE"
}

Possible errors:invalid_api_keyrate_limitedparameter_invalid

Webhook endpoint

Retrieve the webhook endpoint

GET/v1/webhook

Returns the webhook address set and the last characters of your signing secret.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Request
curl https://api.mas4games.com/v1/webhook \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "webhook_endpoint",
  "url": "https://bayi.example.com/webhooks/mas4games",
  "secret_hint": "whsec_…Q3xZ",
  "events": ["order.item.delivered", "order.item.cancelled", "product.updated", "ping"]
}

Possible errors:invalid_api_keyrate_limited

Set the webhook endpoint

PUT/v1/webhook

Sets the https address events are sent to; null stops webhooks. A new address is sent to at once, even when the old one was paused after failures.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
urlrequiredbody · stringAn https address, at most 300 characters; null stops webhooks
Request
curl -X PUT https://api.mas4games.com/v1/webhook \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://bayi.example.com/webhooks/mas4games" }'

# then send a test event:
curl -X POST https://api.mas4games.com/v1/webhook/test -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "webhook_endpoint",
  "url": "https://bayi.example.com/webhooks/mas4games",
  "secret_hint": "whsec_…Q3xZ",
  "events": ["order.item.delivered", "order.item.cancelled", "product.updated", "ping"]
}

Possible errors:invalid_api_keyrate_limitedinvalid_jsonparameter_invalid

Send a test event

POST/v1/webhook/test

Sends a signed ping event to your address and tells you the result; use it while building your receiver. A successful test also resumes at once an address paused after failures. At most 30 an hour per key: beyond that 429 rate_limited with Retry-After.

Parameters

NameInDescription
Authorizationrequiredheader · stringBearer and your secret key
Request
curl -X POST https://api.mas4games.com/v1/webhook/test \
  -H "Authorization: Bearer m4gc_live_YOUR_SECRET_KEY"
Response
{
  "object": "webhook_test",
  "event_id": "evt_5d4c3b2a1f0e9d8c7b6a5f4e",
  "delivered": true,
  "response_status": 200,
  "error": null
}

Possible errors:invalid_api_keyrate_limitedwebhook_not_set

Webhooks

Wait for events instead of polling orders: when a line is delivered or cancelled, and when your products change, we send a signed POST request to your address.

EventsMeaning and what to do
order.item.deliveredAn order line was delivered; data.order is the whole order with codes, data.item_id the line.
order.item.cancelledAn order line was cancelled and its amount went back to your balance.
product.updatedProducts whose price, stock or availability changed (every five minutes, up to 100 at a time); a product taken off sale comes with status: removed.
pingThe test event sent by POST /webhook/test.
order.item.delivered
POST /webhooks/mas4games HTTP/1.1
Host: bayi.example.com
Content-Type: application/json
Mas4games-Signature: t=1793952765,v1=5f0b3c…e91a
Mas4games-Event-Id: evt_2c1a9f0e8b7d6c5a4b3e2f1d
Mas4games-Event-Type: order.item.delivered

{
  "id": "evt_2c1a9f0e8b7d6c5a4b3e2f1d",
  "object": "event",
  "type": "order.item.delivered",
  "mode": "live",
  "api_version": "v1",
  "created_at": "2026-11-06T08:12:45Z",
  "data": {
    "item_id": 5615,
    "order": {
      "object": "order",
      "id": "M4G105146",
      "reference": "SIP-10045",
      "status": "processing",
      "items": [
        { "id": 5615, "status": "delivered", "codes": ["XB1A-7KQ2-PL9D-3M2X", "XB1A-8LR3-QM0E-4N3Y"], "delivery_note": null, "…": "…" }
      ],
      "…": "…"
    }
  }
}
product.updated
{
  "id": "evt_9a8b7c6d5e4f3a2b1c0d9e8f",
  "object": "event",
  "type": "product.updated",
  "mode": "live",
  "api_version": "v1",
  "created_at": "2026-11-06T09:00:00Z",
  "data": {
    "products": [
      { "object": "product", "id": 241, "price": { "amount": 40990, "currency": "TRY" }, "available": true, "stock": null, "…": "…" },
      { "object": "product", "id": 312, "sku": "old-product", "available": false, "status": "removed" }
    ]
  }
}

Verifying the signature

Every request has a Mas4games-Signature header: t=1793952765,v1=5f0b3c…. v1 is the HMAC-SHA256, with your whsec_ secret, of the raw body prefixed with the time and a dot (t.body).

  1. 1Read the raw body before parsing it as JSON.
  2. 2Take t and v1 from the header; compute HMAC-SHA256(secret, t + "." + body).
  3. 3Compare it with v1 in constant time and check t is within 300 seconds of now.
  4. 4If it doesn't match, answer 400 and don't process the event.
import crypto from "node:crypto";
import express from "express";

const app = express();
// The signature is over the raw body: read it before any JSON parsing.
app.post("/webhooks/mas4games", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Mas4games-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  const expected = crypto.createHmac("sha256", process.env.MAS4GAMES_WEBHOOK_SECRET)
    .update(`${t}.${req.body.toString("utf8")}`).digest("hex");
  const ok = parts.v1 && parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - t) <= 300;
  if (!ok) return res.status(400).end();

  const event = JSON.parse(req.body.toString("utf8"));
  if (alreadyHandled(event.id)) return res.status(200).end(); // the same event can arrive twice
  queue(event); // handle it after answering
  res.status(200).end();
});

Acknowledge an event with 2xx within 8 seconds and do the work afterwards: a call that takes longer counts as failed, and the body of your answer is ignored. Any other answer is tried again after 1, 5 and 15 minutes, then hourly and every six hours (at most 12 times).

After 3 failures in a row your address is paused for 5 minutes, the pause doubling with each further failure up to an hour; your events wait meanwhile. Setting a new address (PUT /webhook) or a successful POST /webhook/test resumes it at once.

The same event may arrive more than once, and an order's lines are told one by one, possibly over several rounds (a test order's too); an event keeps its id every time it is sent, so keep the ids and ignore repeats. Order is not guaranteed; for the exact state, use GET /orders/{id}.

Set your address yourself with PUT /webhook; the signing secret is issued with your key. Renewing the key clears the address: set it again with the new key.

Good practices

  • Add a new Idempotency-Key to every POST; on a timeout, send it again with the same key.
  • Give every order your own reference: you can always find whether an order was made.
  • Use webhooks, and check missed ones with GET /orders/{id}.
  • Fetch the catalog once in full, then keep it current with updated_since or product.updated.
  • Try the whole flow (errors included) with the test key first, then go live.
  • Keep the key and the webhook secret on the server only, in environment variables.

Help

If you get stuck, write to our support team; with the answer's X-Request-Id and the error code we can help faster.

Release notes

  • 6 November 2026 (update 2): delivery_note now carries the delivery's own note (a top-up by ID's transaction number, say; TEST-TX-… in the sandbox), or null. The fixed topup_completed value is gone: status: delivered with codes_count: 0 says a top-up by ID is done.
  • 6 November 2026 (update): a test key's GET /me shows a sandbox balance and its balance transactions are empty; an order whose lines go together is cancelled whole or not at all, and orders held for review or handled by our staff aren't cancelled through the API; 429 limits with Retry-After for test orders (500 in 24 hours) and webhook tests (30 an hour); webhook calls time out after 8 seconds and an address that keeps failing is paused; renewing a key clears its webhook address.
  • v1 (6 November 2026): first version. Account, catalog, orders, cancelling, balance transactions, signed webhooks, Idempotency-Key and the sandbox.