Purchasing keys through the API
This guide walks through buying wholesale keys end-to-end via the Eneba API: finding the product you want, locating an auction, paying with your EUR wallet, waiting for the checkout to complete, exporting the purchased keys and downloading the resulting archive.
The API checkout flow currently supports wholesale auctions only — non-wholesale listings cannot be purchased through the API at this time.
403 or an access-denied error.Requirements
- You have generated API credentials and obtained an
access_token— see the Getting Started guide for the OAuth flow, environment URLs and authentication header. - Your EUR wallet has enough balance to cover the purchase. Payment is always deducted from the EUR wallet regardless of the auction currency.
Sequence diagram
The following sequence shows the successful end-to-end flow between your client, the Eneba API and the S3 download endpoint that serves the keys archive.
1. Search for the product
Use the
P_wholesaleAuctionProducts query
to find products that have at least one active wholesale auction with available
stock. Search by name to narrow results, then note the id of the product you
want to buy — you will pass it to step 2.
query getWholesaleAuctionProducts($first: Int, $after: String, $search: String) {
P_wholesaleAuctionProducts(first: $first, after: $after, search: $search) {
totalCount
edges {
node {
id
name
slug
productType
regions
regionWhitelist
regionBlacklist
countriesWhitelist
countriesHardBlacklist
noActivationCountries
}
}
pageInfo {
hasNextPage
endCursor
}
}
}Variables:
{ "first": 20, "search": "call of duty modern warfare" }Products may carry region restrictions. If your integration needs to honour
them — for example when delivering keys to end users in specific countries —
check countriesWhitelist,
countriesHardBlacklist and
noActivationCountries on each result
to confirm the product is purchasable and activatable in the target country.
2. Find auctions for the product
Use the
P_wholesaleAuctions query
with the productIds argument to retrieve all active wholesale auctions for
the product you identified in step 1. Filter by merchant when several
merchants list the same product.
query getWholesaleAuctions($productIds: [P_Uuid!], $merchantSearch: String) {
P_wholesaleAuctions(productIds: $productIds, merchantSearch: $merchantSearch) {
edges {
node {
id
product { name }
wholesalePrice { amount currency }
wholesaleStock
merchant { displayName slug }
}
}
}
}Variables:
{ "productIds": ["<id from step 1>"] }Note the id of the desired auction — that is the auctionId you will pass
to step 3. The wholesaleStock field tells you how many keys are currently
available.
Price amounts are returned in cents — divide by 100 to display
the value (e.g. 1500 → €15.00).
3. Submit the purchase
Submit the purchase using the
S_purchaseWholesaleAuctions mutation.
Each call initiates an asynchronous checkout and returns an actionId you can
poll, plus the orderId of the created order.
mutation PurchaseWholesaleAuctions($input: S_API_PurchaseWholesaleAuctionsInput!) {
S_purchaseWholesaleAuctions(input: $input) {
success
orderId
actionId
}
}Variables:
{
"input": {
"items": [
{
"auctionId": "<id from step 2>",
"quantity": 1
}
]
}
}A single request supports up to 10 auction items, with
quantity between 1 and 2000 per item. All
auctionId values within one request must be unique.
A success: true response only confirms the checkout was queued — it does
not mean the purchase has completed. Poll
A_action with the
returned actionId (step 4) to track the actual outcome.
If the auction does not have enough stock when the checkout is processed,
the action will end in FAILED state at step 4. Note that the wholesaleStock
value reported by
P_wholesaleAuctions
reflects stock at query time and can change before your purchase runs.
4. Poll the checkout status
The checkout runs in the background. Poll the
A_action query with
the actionId returned in step 3 until state reaches a terminal value.
query getAction($actionId: A_Uuid!) {
A_action(actionId: $actionId) {
id
state
}
}Variables:
{ "actionId": "<actionId from step 3>" }| State | Meaning |
|---|---|
NEW | Checkout is still being processed |
COMPLETED | Purchase succeeded |
FAILED | Purchase failed |
Poll every 2–3 seconds. The query returns null if the action has not
started yet, or if 24 hours have passed since it was initiated.
5. Fetch the order details
Once the action is COMPLETED, fetch the order to obtain the entryToken
required to export the keys, confirm the orderState is FULFILLED, and
record the per-item identifiers used in the exported archive (step 8).
Use the O_orders query
with the orderIds filter, passing the orderId returned in step 3 — this
returns the single order directly instead of paginating through all of your
orders.
query getOrder($orderIds: [O_Uuid!]) {
O_orders(orderIds: $orderIds) {
edges {
node {
id
orderNumber
entryToken
orderState
paymentState
totalPrice { amount currency }
createdAt
items {
shortId
sellableName
sellableSlug
quantity
totalPrice { amount currency }
}
}
}
}
}Variables:
{ "orderIds": ["<orderId from step 3>"] }Only orders placed via the API checkout flow are returned here — orders the
same account placed through the Eneba storefront will not appear. To list
all of your API orders, call O_orders without
orderIds and paginate with first/after.
Persist these fields against the orderId you already have from step 3 so
you can drive the rest of the flow and reconcile the archive contents:
| Field | Why you need it |
|---|---|
id | Equal to the orderId from step 3 — confirms you fetched the right order. |
orderState / paymentState | Verify the order reached FULFILLED / PAID before exporting. Any other terminal state means no keys to fetch. |
entryToken | Required input for O_exportOrderKeys (step 6) and O_orderExport (step 7). |
orderNumber | First path segment of every entry in the exported ZIP (step 8). |
items[].sellableSlug | Second path segment for that item inside the ZIP. |
items[].shortId | Third path segment for that item inside the ZIP — the unique handle for the key files belonging to that line. |
6. Trigger the key export
Start the asynchronous key export with the
O_exportOrderKeys mutation.
A success: true response means the export has been queued; the file itself
is produced in the background.
mutation exportOrderKeys($input: O_API_ExportOrderKeysInput!) {
O_exportOrderKeys(input: $input) {
success
}
}Variables:
{
"input": {
"entryToken": "<entryToken from step 5>"
}
}7. Poll the export status
Poll the O_orderExport
query with the same entryToken until status reaches COMPLETED and
downloadUrl is populated.
query getOrderExport($entryToken: String!) {
O_orderExport(entryToken: $entryToken) {
id
status
downloadUrl
expired
}
}Variables:
{ "entryToken": "<entryToken from step 5>" }| Status | Meaning |
|---|---|
PENDING | Export is still being generated |
COMPLETED | Archive is ready and downloadUrl is populated |
FAILED | Export failed — call O_exportOrderKeys again |
Poll every 2–3 seconds.
The downloadUrl is a presigned S3 URL valid for
30 minutes. If the URL expires, or if expired: true
is returned, call O_exportOrderKeys
again with the same entryToken to produce a fresh export.
8. Download and extract the keys
Download the archive from the presigned URL and unzip it. Every entry inside the archive is encrypted with the email address of the Eneba account that placed the order.
curl -o export.zip "<downloadUrl>"
unzip -P "<your-account-email>" export.zipArchive layout
Each order item lives in its own directory whose path is composed from fields you already fetched in step 5:
{orderNumber}/{items[].sellableSlug}/{items[].shortId}/Inside each item directory you'll find:
keys.txt— present for items with text keys. One key per line.{keyId}.pngor{keyId}.jpg— present for each image key, named after the key's identifier. Images that fail to decode are saved as{keyId}.txtcontaining the raw value so no key is lost.
For example, an order with orderNumber: "o-8yZp8K" containing one item
(sellableSlug: "fifa-25-ea-app-key-global",
shortId: "6t7cnn4a3tbzfjm8babmfo6wxr") produces:
o-8yZp8K/
└── fifa-25-ea-app-key-global/
└── 6t7cnn4a3tbzfjm8babmfo6wxr/
└── keys.txtBecause every path segment comes from the O_orders response, you can match
each key file back to the corresponding order item without inspecting the
archive contents. See
O_orderExport
for the full reference, including a multi-item archive example that mixes
text keys (keys.txt) and image keys ({keyId}.png / {keyId}.jpg) in a
single order.
The archive remains available for 3 days after the export
completes. After that, call
O_exportOrderKeys
again with the same entryToken to produce a fresh export.