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.

This feature is available only to accounts with the Wholesale Buyer role. Contact Eneba support if you receive a 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.

S3EnebaClientS3EnebaClientloop[poll until COMPLETED]loop[poll until COMPLETED]P_wholesaleAuctionProducts (search)productId, nameP_wholesaleAuctions (productIds)auctionId, wholesaleStock, wholesalePriceS_purchaseWholesaleAuctionsorderId, actionIdA_action(actionId)O_orders(orderIds: [orderId])orderNumber, entryToken, items[].shortId, items[].sellableSlugO_exportOrderKeys(entryToken)O_orderExport(entryToken)downloadUrlGET downloadUrlexport.zip

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>" }
StateMeaning
NEWCheckout is still being processed
COMPLETEDPurchase succeeded
FAILEDPurchase 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:

FieldWhy you need it
idEqual to the orderId from step 3 — confirms you fetched the right order.
orderState / paymentStateVerify the order reached FULFILLED / PAID before exporting. Any other terminal state means no keys to fetch.
entryTokenRequired input for O_exportOrderKeys (step 6) and O_orderExport (step 7).
orderNumberFirst path segment of every entry in the exported ZIP (step 8).
items[].sellableSlugSecond path segment for that item inside the ZIP.
items[].shortIdThird 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>" }
StatusMeaning
PENDINGExport is still being generated
COMPLETEDArchive is ready and downloadUrl is populated
FAILEDExport 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.zip

Archive 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}.png or {keyId}.jpg — present for each image key, named after the key's identifier. Images that fail to decode are saved as {keyId}.txt containing 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.txt

Because 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.

Copyright 2026 Eneba. All Rights Reserved. JSC “Helis play”, Gyneju St. 4-333, Vilnius, the Republic of Lithuania