Your first sale in 15 minutes
You will take an uploaded-stock offer from nothing to a completed sandbox sale — offer
created, key uploaded, order driven through the real pipeline, and a signed order.paid
webhook received and verified. Every step is a runnable curl against the sandbox with
the exact response you should see. Each step carries a cumulative time budget.
You need a shell with curl and a sandbox token exported as $CK_TEST_TOKEN. To get a
token, do Your first API call in 5 minutes
first — it takes 30 seconds.
1. Get a token · 0:30
export CK_TEST_TOKEN="ck_test_..."
Confirm it works:
curl "https://sandbox-pay.mmokick.com/api/v1/whoami" -H "Authorization: Bearer $CK_TEST_TOKEN"
A 200 with your supplier and "environment": "sandbox" means you are ready. See
Authentication if you get a 401.
2. Pick a product · 2:00
Offers attach to platform-owned products. Use the seeded sandbox game key:
curl "https://sandbox-pay.mmokick.com/api/v1/products?type=game_key&limit=1" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Accept: application/json"
200:
{
"data": [
{
"id": "01SANDBX00000000000000PD01",
"name": "Cyber Odyssey — PC (Steam)",
"type": "game_key",
"platform": "steam",
"region": "global",
"status": "sellable",
"market": { "buyer_price": { "amount": 2049, "currency": "EUR" }, "active_offers": 2, "your_offer_selected": false }
}
],
"next_cursor": null
}
You will use product 01SANDBX00000000000000PD01. Only sellable products accept new
offers; a not_sellable product returns 422 / product_not_sellable at offer creation.
3. Create an offer · 4:00
Create an uploaded-stock offer: you hold the keys with us and we deliver them.
Price is in minor units EUR — 1999 = €19.99.
This is your first mutating call, so send an Idempotency-Key. If the network drops and
you retry with the same key and body, you get the original response back (with header
Idempotent-Replay: true) and exactly one offer is created. Use a fresh UUID or ULID per
logical operation:
curl -X POST "https://sandbox-pay.mmokick.com/api/v1/offers" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01JZWFIRSTSALE0000000OFFER1" \
-d '{
"product_id": "01SANDBX00000000000000PD01",
"stock_mode": "uploaded",
"price": { "amount": 1999, "currency": "EUR" },
"external_ref": "SKU-FIRST-SALE",
"low_stock_threshold": 5
}'
201 — the offer is created in draft. For an uploaded offer you omit fulfilment
and declared_stock (sending them returns 422):
{
"id": "01JZWX5N3PQRS7TV9WXY2Z4A6B",
"product_id": "01SANDBX00000000000000PD01",
"external_ref": "SKU-FIRST-SALE",
"status": "draft",
"stock_mode": "uploaded",
"price": { "amount": 1999, "currency": "EUR" },
"delivery_sla_minutes": 0,
"delivery_time": "instant",
"low_stock_threshold": 5,
"stock": { "mode": "uploaded", "declared": null, "available": 0, "reserved": 0, "delivered": 0 },
"health": { "score": 100, "routing_eligible": false, "cooldown_until": null },
"created_at": "2026-07-12T09:03:00Z",
"updated_at": "2026-07-12T09:03:00Z"
}
Note the response echoes your external_ref — map it to your own SKU system; it appears
on every order and webhook. If you already have an offer for this product you get 409 /
offer_exists; PATCH the existing offer instead.
4. Upload a key · 6:00
Add one text key to the offer's inventory. Inventory upload is idempotent too — send an
Idempotency-Key here so a network retry inserts the key exactly once:
curl -X POST "https://sandbox-pay.mmokick.com/api/v1/offers/01JZWX5N3PQRS7TV9WXY2Z4A6B/inventory" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01JZWFIRSTSALE00000INVENTORY" \
-d '{
"mode": "strict",
"items": [
{ "kind": "text", "value": "CKTEST-OK-00001" }
]
}'
201:
{
"accepted": 1,
"rejected": [],
"item_ids": ["01JZWY6P2R4T6V8X0Z2B4D6F8H"],
"stock_available": 1
}
In strict mode (the default) any invalid item rejects the whole batch with 422 /
inventory_rejected and stores nothing; use mode: partial to store the valid items
and get the rest reported. Your key is now available.
Activate the offer so it can be routed:
curl -X POST "https://sandbox-pay.mmokick.com/api/v1/offers/01JZWX5N3PQRS7TV9WXY2Z4A6B/activate" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Idempotency-Key: 01JZWFIRSTSALE0000000ACTIV8"
200 returns the offer with "status": "active". Activating an uploaded offer with zero
stock succeeds but stays unroutable until stock arrives — you just added a key, so you
are routable now.
5. Register a webhook endpoint · 8:00
Tell us where to push order events. The secret in the response is shown once — save it.
curl -X POST "https://sandbox-pay.mmokick.com/api/v1/webhooks" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01JZWFIRSTSALE00000WEBHOOK1" \
-d '{
"url": "https://your-app.example/hooks/cheap-keys",
"events": ["order.paid", "order.refunded"],
"description": "First sale intake"
}'
201 — the only time the secret appears:
{
"id": "01JZW0DC8FGH2JK4MN6PQ8RS0T",
"url": "https://your-app.example/hooks/cheap-keys",
"events": ["order.paid", "order.refunded"],
"description": "First sale intake",
"status": "active",
"secret": "whsec_4d9c2f81a7e35b60d1c8f42e9a0b7365",
"consecutive_failures": 0,
"disabled_at": null,
"created_at": "2026-07-12T09:05:00Z"
}
No public URL handy? Use any HTTPS tunnel (the sandbox allows any HTTPS port), or point at a request bin so you can inspect the exact signed request. Point at a public HTTPS host; private and link-local IPs are rejected. Save the
secretnow — you need it to verify the signature in step 7, and it is shown this one time only.
6. Trigger a sandbox test order · 10:00
POST /sandbox/orders is a sandbox-only endpoint (it returns 404 / not_found on
production) that runs the full real pipeline against your offer: order → key
allocation → delivery → webhooks. The default scenario is paid.
curl -X POST "https://sandbox-pay.mmokick.com/api/v1/sandbox/orders" \
-H "Authorization: Bearer $CK_TEST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01JZWFIRSTSALE000000ORDER01" \
-d '{
"offer_id": "01JZWX5N3PQRS7TV9WXY2Z4A6B",
"quantity": 1,
"buyer_country": "FR",
"scenario": "paid"
}'
201 returns the order object (your items only):
{
"id": "01JZY2H8K4M6N8P0Q2R4S6T8V0",
"number": "CK-01JZY2H8K4M6N8P0Q2R4S6T8V0",
"status": "completed",
"buyer_country": "FR",
"currency": "EUR",
"created_at": "2026-07-12T09:07:00Z",
"paid_at": "2026-07-12T09:07:01Z",
"completed_at": "2026-07-12T09:07:02Z",
"items": [
{
"id": "01JZY2H8KFAB3CD5EF7GH9JK1M",
"product_id": "01SANDBX00000000000000PD01",
"offer_id": "01JZWX5N3PQRS7TV9WXY2Z4A6B",
"external_ref": "SKU-FIRST-SALE",
"quantity": 1,
"unit_price": { "amount": 1999, "currency": "EUR" },
"gross": { "amount": 1999, "currency": "EUR" },
"commission": { "amount": 170, "currency": "EUR" },
"net": { "amount": 1829, "currency": "EUR" },
"status": "delivered",
"fulfilment": { "source": "uploaded", "reservation_id": null, "inventory_item_ids": ["01JZWY6P2R4T6V8X0Z2B4D6F8H"] },
"delivered_at": "2026-07-12T09:07:02Z"
}
],
"totals": {
"gross": { "amount": 1999, "currency": "EUR" },
"commission": { "amount": 170, "currency": "EUR" },
"net": { "amount": 1829, "currency": "EUR" }
}
}
Commission is max(round_half_up(gross × 0.085), 5) per item: 1999 → 170 → 1829 net,
exactly. See Fees.
7. Receive and verify order.paid · 13:00
The pipeline pushes an order.paid event to the endpoint you registered. The exact HTTP
request your server receives:
POST /hooks/cheap-keys HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: cheap-keys-webhooks/1.0 (+https://pay.mmokick.com/developers)
X-CK-Event: order.paid
X-CK-Event-Id: 01JZY3B1M5P7R9T2V4X6Z8A0C2
X-CK-Delivery-Id: 01JZY3B2N6Q8S0V2X4Z6B8D0F2
X-CK-Timestamp: 1783519402193
X-CK-Signature: v1=<hex hmac of "1783519402193.{raw body}">
{"id":"01JZY3B1M5P7R9T2V4X6Z8A0C2","type":"order.paid","mode":"test","api_version":"v1","test_fire":false,"created_at":"2026-07-12T09:07:01Z","data":{"order":{"id":"01JZY2H8K4M6N8P0Q2R4S6T8V0","number":"CK-01JZY2H8K4M6N8P0Q2R4S6T8V0","status":"completed","currency":"EUR"}}}
Verify it before trusting it. This is the canonical PHP verifier — compute
HMAC-SHA256(secret, "{timestamp}.{raw body}") over the raw request bytes and
compare in constant time:
function verify_ck_signature(
string $secret, string $signatureHeader, string $timestampHeader,
string $rawBody, int $toleranceMs = 300000
): bool {
// The header is a comma-separated list of scheme=value pairs (e.g.
// "v1=<hex>,v2=<hex>"). Pick the v1 entry; ignore unknown schemes.
if (!preg_match('/(?:^|,\s*)v1=([a-f0-9]{64})/', $signatureHeader, $m)) return false;
if (!ctype_digit($timestampHeader)) return false;
if (abs((int) round(microtime(true) * 1000) - (int) $timestampHeader) > $toleranceMs) return false;
$expected = hash_hmac('sha256', $timestampHeader . '.' . $rawBody, $secret);
return hash_equals($expected, $m[1]);
}
// A multi-scheme header still verifies on the v1 entry (the garbage v2 is skipped):
// verify_ck_signature($secret, "v1={$validHex},v2=deadbeef", $ts, $body) === true
Use the endpoint secret from step 5, $_SERVER['HTTP_X_CK_SIGNATURE'],
$_SERVER['HTTP_X_CK_TIMESTAMP'], and the raw body from
file_get_contents('php://input') (in Laravel, $request->getContent()). Return 2xx
fast and process asynchronously — a 2xx within 10 seconds acks the delivery; any other
status (or a timeout) fails it and we retry. The full scheme, Node and Python verifiers,
and copy-paste test vectors are in the Webhooks guide.
8. See the sale · 15:00
Read the order back and check your balance:
curl "https://sandbox-pay.mmokick.com/api/v1/orders/01JZY2H8K4M6N8P0Q2R4S6T8V0" \
-H "Authorization: Bearer $CK_TEST_TOKEN"
curl "https://sandbox-pay.mmokick.com/api/v1/balance" \
-H "Authorization: Bearer $CK_TEST_TOKEN"
GET /balance 200:
{
"currency": "EUR",
"available": { "amount": 0, "currency": "EUR" },
"pending": { "amount": 1829, "currency": "EUR" },
"updated_at": "2026-07-12T09:07:03Z"
}
Your €18.29 net is pending — it clears to available after the holding period
(7 days on production, 10 minutes on sandbox so you can test it in one session). Inspect
the ledger with GET /balance/entries.
That is the whole integration for uploaded stock: create an offer, upload keys, and we handle routing, delivery, webhooks, and settlement. Declared stock — where keys stay on your side and we call your endpoints to provision them — is the next guide.
Next steps
- Declared stock — keep keys on your systems and implement the fulfilment contract.
- Webhooks — signature verification in PHP, Node, and Python, plus retries and auto-disable.
- Top-up products — validate → execute delivery into player accounts.