EZ PIN API
EZ PIN Access API · v2

API Reference

The EZ PIN Access API lets your systems buy digital gift cards from EZ PIN in real time. You can browse the products assigned to your account, check stock, place orders paid from your EZ PIN wallet, and get the card codes. Cards can also be delivered to your customers by email, SMS or WhatsApp.

The API uses REST conventions. Requests and responses are JSON over HTTPS, standard HTTP status codes report the result, and every error has a numeric EZ PIN error code that your code can act on.

Before you start

  • You need an EZ PIN business account with API access enabled. Ask your account manager if you don't see Settings → API Access in the EZ PIN panel.
  • Your account must have passed KYC verification before it can place orders.
  • Products and prices are assigned to your account by EZ PIN. The catalog only returns products you can buy.
  • Orders are paid from your EZ PIN wallet. Check your balance with Get balances.

Quickstart

This walkthrough takes you from API keys to card codes. Use a sandbox key while you build. The complete script is on the right.

  1. Create API keysIn the panel, open Settings → API Access and click Create Sandbox API Access. Enter a title and the public IP address your servers call from, and confirm with your Google Authenticator code. Copy the Client ID and Secret Key. The secret key is shown only once.
  2. Get an access tokenSend your keys to POST /auth/token/. You get a token that is valid for a few hours (the expire field says how many seconds). Send it in the Authorization: Bearer <token> header of every other request.
  3. Pick a productCall GET /catalogs/ and choose an item. Note its sku and a price between min_price and max_price.
  4. Check stock (optional)Call GET /catalogs/{sku}/availability/ with the quantity and price. availability: true means the cards can be delivered now.
  5. Place the orderGenerate a new UUID v4 as the reference_code and call POST /orders/. Save the reference_code and order_id. If anything goes wrong, use them to look the order up instead of ordering again.
  6. Get the cardsWhen the order's status is 1 (accepted), call GET /orders/{reference_code}/cards/. If the order is still pending, wait for the webhook or poll GET /orders/{reference_code}/.
Prefer Postman?

Import the Postman collection and set client_id and secret_key in its variables. It gets and refreshes the access token for you, and creates a new reference_code for each order.

import time
import uuid

import requests

BASE_URL = "https://api.ezpaypin.com/vendors/v2"

# 1. Get an access token (reuse it until it expires).
auth = requests.post(f"{BASE_URL}/auth/token/", json={
    "client_id": "YOUR_CLIENT_ID",
    "secret_key": "YOUR_SECRET_KEY",
}, timeout=30).json()
session = requests.Session()
session.headers["Authorization"] = f"Bearer {auth['access']}"

# 2. Pick a product from the catalog.
catalog = session.get(f"{BASE_URL}/catalogs/", timeout=60).json()
item = catalog["results"][0]
sku, price = item["sku"], item["min_price"]

# 3. Place the order with a new reference code.
reference_code = str(uuid.uuid4())
order = session.post(f"{BASE_URL}/orders/", json={
    "sku": sku,
    "quantity": 1,
    "price": price,
    "pre_order": False,
    "reference_code": reference_code,
}, timeout=60).json()
print(order)  # Store order["order_id"] and reference_code.

# 4. Wait until the order is accepted (1) or rejected (-1).
while order.get("status") == 0:
    time.sleep(10)
    order = session.get(f"{BASE_URL}/orders/{reference_code}/", timeout=30).json()

# 5. Get the card codes.
if order.get("status") == 1:
    cards = session.get(f"{BASE_URL}/orders/{reference_code}/cards/", timeout=30).json()
    for card in cards["results"]:
        print(card["card_number"], card["pin_code"], card["claim_url"])
import { randomUUID } from "node:crypto";

const BASE_URL = "https://api.ezpaypin.com/vendors/v2";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 1. Get an access token (reuse it until it expires).
const auth = await fetch(`${BASE_URL}/auth/token/`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ client_id: "YOUR_CLIENT_ID", secret_key: "YOUR_SECRET_KEY" }),
}).then((r) => r.json());

const api = (path, init = {}) =>
  fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${auth.access}`, "Content-Type": "application/json" },
  }).then((r) => r.json());

// 2. Pick a product from the catalog.
const catalog = await api("/catalogs/");
const { sku, min_price: price } = catalog.results[0];

// 3. Place the order with a new reference code.
const referenceCode = randomUUID();
let order = await api("/orders/", {
  method: "POST",
  body: JSON.stringify({ sku, quantity: 1, price, pre_order: false, reference_code: referenceCode }),
});
console.log(order); // Store order.order_id and referenceCode.

// 4. Wait until the order is accepted (1) or rejected (-1).
while (order.status === 0) {
  await sleep(10_000);
  order = await api(`/orders/${referenceCode}/`);
}

// 5. Get the card codes.
if (order.status === 1) {
  const cards = await api(`/orders/${referenceCode}/cards/`);
  for (const card of cards.results) console.log(card.card_number, card.pin_code, card.claim_url);
}
# 1. Get an access token
curl -X POST "https://api.ezpaypin.com/vendors/v2/auth/token/" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "YOUR_CLIENT_ID", "secret_key": "YOUR_SECRET_KEY"}'

export EZPIN_ACCESS_TOKEN="<access from the response>"

# 2. List the catalog
curl "https://api.ezpaypin.com/vendors/v2/catalogs/" \
  -H "Authorization: Bearer $EZPIN_ACCESS_TOKEN"

# 3. Place an order (new UUID v4 every time)
curl -X POST "https://api.ezpaypin.com/vendors/v2/orders/" \
  -H "Authorization: Bearer $EZPIN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sku": 1148, "quantity": 1, "price": 10, "pre_order": false,
       "reference_code": "'"$(uuidgen | tr A-Z a-z)"'"}'

# 4. Check the order, then 5. get the cards
curl "https://api.ezpaypin.com/vendors/v2/orders/REFERENCE_CODE/" \
  -H "Authorization: Bearer $EZPIN_ACCESS_TOKEN"
curl "https://api.ezpaypin.com/vendors/v2/orders/REFERENCE_CODE/cards/" \
  -H "Authorization: Bearer $EZPIN_ACCESS_TOKEN"

The script stops at the first problem for clarity. In production, check HTTP status codes and the code field as described in Errors.

Authentication

The API uses your Client ID and Secret Key to issue short-lived access tokens (JSON Web Tokens). Send the token in the Authorization header of every request except the token request itself.

Get your API keys

  1. Sign in to the EZ PIN panel as the account owner and open Settings → API Access.
  2. Click Create Sandbox API Access. After you have a sandbox key, the same button creates a production key.
  3. Enter a Title and your Trusted IP, the public IPv4 or IPv6 address your servers call the API from. Confirm with your Google Authenticator code.
  4. Copy the Secret Key right away. It is shown only once. The Client ID stays visible on the page.

To create API keys, your account needs a confirmed phone number, a confirmed WhatsApp number and two-step verification. Otherwise you get error 722.

Use an access token

Header
Authorization: Bearer <access>. The word Bearer, one space, then the token.
Lifetime
The expire field of the token response, in seconds. This is currently 10800 (3 hours), but read the value from the response instead of hard-coding it.
Renewal
There is no refresh token. Request a new token shortly before the old one expires (for example 5 minutes before). The old token keeps working until it expires.
Reuse
Reuse one token for all requests, and share it between your servers. The token endpoint is limited to 1 request per second, so don't request a token for every call.

IP allowlist

If your API key has trusted IPs, every request with a token must come from one of them. The check uses the public IP address EZ PIN sees, so add the address of your NAT gateway or proxy. Requests from other addresses fail with 400 and error 743. The error message includes the IP address that EZ PIN saw. Getting a token is not restricted by IP.

You manage trusted IPs yourself in the panel under Settings → API Access. Support can't change them for you.

Key security

  • Treat the secret key like a password. Keep it on your servers, in an environment variable or a secret manager. Never put it in a mobile app, a browser or source control.
  • When you generate a new secret key, every token issued with the old key stops working right away.
  • When you deactivate an API key in the panel, its tokens are rejected with error 742.

Authentication errors

HTTP Response Cause
401 Authentication credentials were not provided. The Authorization header is missing or doesn't start with Bearer.
401 Given token not valid for any token type The token is expired, malformed or was signed for another key. messages gives the reason, for example Signature has expired. Get a new token.
401 Token contained no recognizable user identification The secret key was changed after this token was issued. Get a new token with the new key.
401 code 764 The account linked to the token no longer exists.
400 code 615 On /auth/token/: the client ID and secret key don't match an active API key.
400 code 742 The API key, user or company is inactive.
400 code 743 The request came from an IP address that is not on the key's allowlist.
curl "https://api.ezpaypin.com/vendors/v2/balance/" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
import time
import requests

BASE_URL = "https://api.ezpaypin.com/vendors/v2"
_token = {"value": None, "expires_at": 0}


def get_token():
    """Return a cached token and renew it 5 minutes before it expires."""
    if _token["value"] and time.time() < _token["expires_at"] - 300:
        return _token["value"]
    resp = requests.post(f"{BASE_URL}/auth/token/", json={
        "client_id": CLIENT_ID,
        "secret_key": SECRET_KEY,
    }, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    _token.update(value=data["access"], expires_at=time.time() + data["expire"])
    return _token["value"]
const BASE_URL = "https://api.ezpaypin.com/vendors/v2";
let token = { value: null, expiresAt: 0 };

// Return a cached token and renew it 5 minutes before it expires.
export async function getToken() {
  if (token.value && Date.now() < token.expiresAt - 5 * 60 * 1000) return token.value;
  const res = await fetch(`${BASE_URL}/auth/token/`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: process.env.EZPIN_CLIENT_ID,
      secret_key: process.env.EZPIN_SECRET_KEY,
    }),
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
  const data = await res.json();
  token = { value: data.access, expiresAt: Date.now() + data.expire * 1000 };
  return token.value;
}
<?php

function ezpin_token(): string
{
    static $token = null, $expiresAt = 0;
    if ($token && time() < $expiresAt - 300) {
        return $token;
    }
    $ch = curl_init('https://api.ezpaypin.com/vendors/v2/auth/token/');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode([
            'client_id' => getenv('EZPIN_CLIENT_ID'),
            'secret_key' => getenv('EZPIN_SECRET_KEY'),
        ]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);
    $token = $data['access'];
    $expiresAt = time() + $data['expire'];
    return $token;
}
{
  "detail": "Given token not valid for any token type",
  "code": "token_not_valid",
  "messages": [
    { "message": "Signature has expired" }
  ]
}

Sandbox and production

Sandbox and production use the same base URL. The type of API key decides which one you're working with.

Sandbox key Production key
Catalog Only test products from sandbox categories Every product assigned to your account
Currency The test currency EZD, which has no real value Real currencies (USD, EUR, ...)
Orders Test orders paid from your EZD wallet. Your real wallets are never charged. Real orders, real card codes, paid from your real wallets
Everything else The same: authentication, IP allowlist, balance checks, pricing rules, rate limits, order statuses and webhooks.
Sandbox orders use the test currency EZD

Sandbox products are priced in EZD, and sandbox orders are paid from your EZD wallet. Check its balance with GET /balance/. If you run out, ask your account manager to top it up.

Each API key has its own webhook settings. Configure the webhook separately with the sandbox token and the production token.

Sandbox:     https://api.ezpaypin.com/vendors/v2/  + sandbox key
Production:  https://api.ezpaypin.com/vendors/v2/  + production key

Making requests

Protocol
HTTPS only. Use TLS 1.2 or later.
Body format
Send POST bodies as JSON with Content-Type: application/json. Responses are always JSON (UTF-8).
Trailing slash
Every path ends with /, as in /orders/. Without it, a GET gets a redirect and a POST fails.
Path parameters
Shown in braces, for example /orders/{reference_code}/. Replace the braces and the name with the value.
Numbers
Prices and amounts are JSON numbers in the product's currency, for example 10 or 24.99. Send prices with at most 2 decimal places. Responses can have more decimals (for example 4.912). Round for display only.
Booleans
true / false.
Unknown fields
Fields the API doesn't recognize are ignored. New fields can be added to responses at any time, so your parser must not fail on them.
Timeouts
Use a client timeout of at least 120 seconds for POST /orders/ and POST /retailer_order/, because instant orders get their cards from the supplier during the request. If the request still times out, the order may have been created. Look it up before you retry. Use 30 seconds for other calls, and 60 seconds for the catalog.

The Node.js samples use the built-in fetch and top-level await. They need Node.js 18 or later and an ES module (.mjs, or "type": "module" in package.json). The Python samples use the requests library.

POST /vendors/v2/orders/ HTTP/1.1
Host: api.ezpaypin.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
Content-Type: application/json
Accept: application/json

Errors

The API uses HTTP status codes to say whether a request worked. Codes in the 2xx range mean success. Codes in the 4xx range mean the request failed because of the information sent or the state of your account. Codes in the 5xx range mean something went wrong at EZ PIN (rare).

HTTP status codes

Status Meaning
200 OK The request worked. The two availability endpoints also use 200 for some errors (see below).
400 Bad Request A parameter is missing or invalid, or a business rule failed (balance, stock, limits, and so on). The body has an error code.
401 Unauthorized The access token is missing, invalid or expired. See Authentication.
403 Forbidden Your account can't place API orders yet: API ordering is disabled (661) or KYC is not approved (798).
404 Not Found The path is wrong, or no order with this reference_code exists in your account.
405 Method Not Allowed The endpoint doesn't support this HTTP method.
406 Not Acceptable The terminal_pin is wrong or inactive (code 617).
429 Too Many Requests You hit a rate limit. Wait for the number of seconds in the Retry-After header.
500, 502, 503, 504 Server or gateway error. Retry with backoff. For orders, first look the order up before you retry.

Error object

Most errors return a JSON object with two fields:

detailstring
A message in English that explains the error. You can log it or show it to your staff. The wording can change, so don't parse it.
codeinteger or string

The EZ PIN error code. Use this to handle errors in code. See the error code reference.

Depending on where the error was raised, the code is a JSON number (610) or a numeric string ("610"). Convert it to an integer before you compare it. Token errors use the string "token_not_valid".

Field validation errors

When a body field is missing or has the wrong type, the response is 400 and the body has one key per invalid field. Each key holds a list of messages. These responses have no code.

Errors returned with HTTP 200

Check availability and Check retailer availability return validation errors (for example 648, 708, 792) with status 200. Treat a response with no availability field as an error and read its code.

Handling errors

  • Branch on the HTTP status first, then on code. Log detail along with your reference_code.
  • Don't retry 4xx errors without changing something. The exceptions are 429 and 609 (order not ready yet), which you can retry after a wait.
  • On timeouts, network errors and 5xx responses for POST /orders/, the order may have been created. Look it up by reference_code before you retry. See Reference codes.
  • Retry other requests with exponential backoff (for example 1 s, 2 s, 4 s, up to 5 tries).
{
  "detail": "Your balance is not sufficient.",
  "code": 610
}
{
  "quantity": ["A valid integer is required."],
  "pre_order": ["This field is required."]
}
{
  "detail": "Bad request: the 'item_count' and 'price' fields are required and must be numbers.",
  "code": 648
}
resp = session.post(f"{BASE_URL}/orders/", json=payload, timeout=60)
body = resp.json()

if resp.ok:
    handle_order(body)
elif "code" in body:
    code = str(body["code"])           # 610 or "610"
    if code == "610":
        alert_finance("Top up the EZ PIN wallet")
    elif code in ("614", "635"):
        mark_out_of_stock(payload["sku"], payload["price"])
    else:
        raise EzpinError(code, body["detail"])
else:
    raise ValueError(body)             # field errors: {"field": ["message"]}
const resp = await fetch(`${BASE_URL}/orders/`, options);
const body = await resp.json();

if (resp.ok) {
  handleOrder(body);
} else if ("code" in body) {
  const code = String(body.code); // 610 or "610"
  if (code === "610") alertFinance("Top up the EZ PIN wallet");
  else if (code === "614" || code === "635") markOutOfStock(payload.sku, payload.price);
  else throw new EzpinError(code, body.detail);
} else {
  throw new Error(JSON.stringify(body)); // field errors
}

Rate limits

Limits are counted for your whole EZ PIN account. All your API keys and servers share them.

Endpoint Limit
POST /auth/token/ 1 request per second (per IP address)
POST /orders/, POST /retailer_order/ 1 request per second, plus the general limit
GET /catalogs/, GET /catalogs/{sku}/availability/, GET /retailer_products/ Not rate limited
All other endpoints 100 requests per minute, shared across these endpoints

A request over the limit gets 429 Too Many Requests and a Retry-After header with the number of seconds to wait.

Staying within the limits

  • For more cards, raise quantity in one order instead of sending many single-card orders.
  • Sync the catalog to your own database once a day, and again when EZ PIN notifies you of catalog changes. Serve products and prices from your copy (see List catalog).
  • Use webhooks instead of tight polling. If you poll, wait at least 10 seconds between checks of the same order.
  • Send order requests one after another, not in parallel.
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{
  "detail": "Request was throttled. Expected available in 1 second."
}

Pagination

List orders is paginated with limit and offset. The other list endpoints return all results in one response.

limitintegerOptional
The maximum number of results to return. The default is 10. Use up to 100 for good response times.
offsetintegerOptional
The number of results to skip. The default is 0.

Paginated response

countinteger
The total number of results across all pages.
nextstringnullable
The full URL of the next page, or null on the last page.
previousstringnullable
The full URL of the previous page, or null on the first page.
resultsarray
The results on this page.

To read every page, follow next until it is null. The next URL already contains your filters.

url = f"{BASE_URL}/orders/"
params = {"start_date": "2026-09-01", "end_date": "2026-10-01", "limit": 100}

orders = []
while url:
    page = session.get(url, params=params, timeout=30).json()
    orders.extend(page["results"])
    url, params = page["next"], None   # "next" already has the query string
let url = `${BASE_URL}/orders/?start_date=2026-09-01&end_date=2026-10-01&limit=100`;
const orders = [];
while (url) {
  const page = await fetch(url, { headers }).then((r) => r.json());
  orders.push(...page.results);
  url = page.next;
}

Reference codes

Every order needs a reference_code, a UUID v4 that you generate before you send the request. It is your ID for the order. You use it to look the order up and to get its cards. It also protects you from buying twice when a request fails partway through.

Format

  • A random UUID version 4 in lowercase, with hyphens (36 characters), for example 8f1453f0-96cd-4dbf-89c4-cb44b293241d.
  • Uppercase letters, missing hyphens, braces and other UUID versions are rejected with 645.
  • Reference codes must be unique across all of EZ PIN, not just your account. A random UUID v4 from a standard library is unique in practice. Never build one from your own order numbers.

Each code works only once

A reference code can only be used once. If you send a code that was already used, the request fails with 400 and error 680. The API does not return the existing order. This is what stops a retry from buying the same cards twice.

Retrying safely

If POST /orders/ times out, loses its connection, or returns a 5xx, you can't tell whether the order was created. Follow these steps:

  1. Look the order upWait a few seconds, then call GET /orders/{reference_code}/.
  2. Found (200)The order exists. Continue with its status and don't create another order.
  3. Not found (404)The order wasn't created. Send the same request again with the same reference_code. If the first request did create the order after all, the retry fails with 680 instead of buying twice. Go back to step 1.
After an error response, use a new code

Some errors happen after the order is recorded: 611, 618, 621, 779 and 780. The order is then saved as rejected (-1), anything charged is refunded, and the reference code is used up. You may also get a webhook for it. Use a new reference_code for your next attempt.

In short, reuse a reference code only to retry a request whose result you don't know. Use a new one for every new purchase attempt.

import uuid

reference_code = str(uuid.uuid4())   # "8f1453f0-96cd-4dbf-89c4-cb44b293241d"
import { randomUUID } from "node:crypto";

const referenceCode = randomUUID(); // "8f1453f0-96cd-4dbf-89c4-cb44b293241d"
<?php

function uuid_v4(): string
{
    $b = random_bytes(16);
    $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); // version 4
    $b[8] = chr((ord($b[8]) & 0x3f) | 0x80); // RFC 4122 variant
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
# macOS / Linux; EZ PIN requires lowercase
uuidgen | tr '[:upper:]' '[:lower:]'
import time
import uuid

import requests


def create_order(session, payload):
    ref = str(uuid.uuid4())
    body = {**payload, "reference_code": ref}

    for attempt in range(4):
        try:
            resp = session.post(f"{BASE_URL}/orders/", json=body, timeout=120)
        except requests.RequestException:
            resp = None                       # outcome unknown

        if resp is not None and resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 1)))
            continue
        if resp is not None and resp.status_code < 500:
            data = resp.json()
            if resp.ok:
                return data                   # status 1 (accepted) or 0 (pending)
            if str(data.get("code")) != "680":
                raise EzpinError(data)        # a real 4xx: fix the request
            # 680: this reference code exists already; look it up below

        time.sleep(5 * (attempt + 1))
        found = session.get(f"{BASE_URL}/orders/{ref}/", timeout=30)
        if found.status_code == 200:
            return {**found.json(), "reference_code": ref}
        # 404: not created yet, retry with the same reference code

    raise RuntimeError(f"Could not confirm order {ref}; check it later")

Dates and times

  • All times are UTC.
  • Responses use ISO 8601 without a time zone suffix, with microseconds: 2026-06-20T11:17:21.743063. Read these values as UTC.
  • Date filters (start_date, end_date) accept YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, in UTC. Both ends are inclusive.
  • A date without a time means midnight at the start of that day. To include all of 30 September, use end_date=2026-09-30T23:59:59 or end_date=2026-10-01.
  • To filter by a local day, convert it to UTC first. For example, 3 January in Dubai (UTC+4) is start_date=2026-01-02T20:00:00 to end_date=2026-01-03T19:59:59.
  • Daily purchase limits (error 639) reset at 00:00 UTC.
created_time        2026-06-20T11:17:21.743063   (UTC)
start_date          2026-09-01                   (2026-09-01 00:00:00 UTC)
end_date            2026-09-30T23:59:59          (inclusive)
Guide

Products and pricing

The catalog lists every product you can buy and its prices for your account. Each item has a sku (the product ID) and an allowed price range.

Range and fixed-price products

Type How to recognize it Price you can order
Range min_price < max_price, for example 5 to 500 Any amount in the range, both ends included, with up to 2 decimals
Fixed min_price = max_price Exactly that amount
A SKU can appear more than once

Products sold in fixed denominations (for example 25, 50 and 100) appear once per denomination. All these rows have the same sku and different prices. Identify a catalog item by sku + price, not by sku alone. To order one, send its sku and its exact price.

What you pay

The price you send is the card's face value, the amount your customer can spend. What you pay depends on two catalog fields:

Case Your cost per card Example
activation_fee > 0 face value + activation_fee 10.00 + 3.09 = 13.09
otherwise face value × (1 + percentage_of_buying_price / 100) -2 % on 50.00 = 49.00
+1.5 % on 50.00 = 50.75

A negative percentage_of_buying_price is a discount and a positive one is a markup. EZ PIN calculates the exact amount when the order is placed and returns it in total_customer_cost. Your own calculation can differ by a cent because of rounding.

showing_price is set only if your account hides buying prices and you uploaded a display price list in the panel. It holds your display (resale) price in your display currency. EZ PIN never charges it.

Region and currency

These are separate. regions says where the card can be redeemed (for example US or GLC for global). currency is the card's currency, which is also the wallet the order is charged to. A product with no region set shows {"name": "Unknown", "code": "NULL"}.

Reward types

reward_type reward_type_text What the customer gets
0 Gift-Card A card number to redeem with the brand, plus a PIN code for brands that use one
1 Link A redemption link

Good practice

  • Sync the catalog into your system once a day, and again when EZ PIN notifies you of catalog changes. Read products and prices from your copy, and use Check availability for live stock.
  • Only offer prices from the catalog. Other prices are rejected with 612 (outside the range) or 605 (not an offered denomination).
  • Rarely, a catalog product can't be sold through the API. Ordering it returns 711, and the message lists the channels where it is sold.
[
  { "sku": 562, "title": "PlayStation US $25",  "min_price": 25.0,  "max_price": 25.0 },
  { "sku": 562, "title": "PlayStation US $50",  "min_price": 50.0,  "max_price": 50.0 },
  { "sku": 562, "title": "PlayStation US $100", "min_price": 100.0, "max_price": 100.0 }
]
{
  "sku": 1152,
  "title": "Amazon Canada",
  "min_price": 5.0,
  "max_price": 500.0,
  "activation_fee": 0,
  "percentage_of_buying_price": -2.0,
  "currency": { "currency": "Dollars", "symbol": "CAD$", "code": "CAD" }
}

With this product, a CAD 100.00 card costs about CAD 98.00, charged to your CAD wallet.

Guide

Stock and pre-orders

Some products are always in stock. Others depend on the supplier's stock. Check availability tells you, for a quantity and price, whether the order can be filled now and what to do if it can't.

Availability delivery_type

Value availability Meaning How to order
1 true Available. The cards can be delivered now. pre_order: false. The order is usually accepted in the response.
2 false Standard. Not in stock now. Filled automatically, usually in 5 to 10 minutes. pre_order: true only. With false the order fails with 614.
3 false Pre-order. Not in stock now. Filled when the stock arrives, usually in 1 to 2 business days. pre_order: true only. With false the order fails with 614.
0 false Out of stock. Can't be ordered now. This also covers an unknown SKU and a price outside the range. Don't order. The order fails with 635.

delivery_type_text is a readable label such as available, Out of stock or This product can be pre ordered. The wording can change, so base your logic on delivery_type.

Two fields named delivery_type

In availability responses, delivery_type is the stock status above. In orders, delivery_type is how the cards are sent to your customer (0 none, 1 email, 2 SMS, 3 WhatsApp). See Delivering cards.

The pre_order flag

  • pre_order: false means "only if it can be delivered now". If there isn't enough stock, the order is refused (614 or 635) and nothing is charged.
  • pre_order: true means "accept the order and deliver it when the cards are ready". The order is created with status 0 (pending) and finishes later. Use it when delivery time is not critical.
  • If you send pre_order: true but the stock is already there, the order may be filled right away as a normal order.
  • pre_order: true on a product whose catalog pre_order is false fails with 613.

Checking availability first is optional, because Create an order checks stock again. It is useful to show stock to your users, or to choose pre_order before you order.

{
  "availability": true,
  "detail": "This catalog is available.",
  "delivery_type": 1,
  "delivery_type_text": "available"
}
{
  "availability": false,
  "detail": "This catalog is not available.",
  "delivery_type": 3,
  "delivery_type_text": "This product can be pre ordered"
}
check = session.get(
    f"{BASE_URL}/catalogs/{sku}/availability/",
    params={"item_count": qty, "price": price}, timeout=30,
).json()

if "availability" not in check:
    raise EzpinError(check["code"], check["detail"])
if check["delivery_type"] == 1:
    pre_order = False
elif check["delivery_type"] in (2, 3):
    pre_order = True          # accepted now, delivered later
else:
    raise OutOfStock(sku, price)
Guide

Order lifecycle

Every order has a numeric status and a matching status_text. Your logic should use status.

status status_text Meaning Final
0 pending Paid and waiting for the cards. The amount has been taken from your wallet. No
1 accept Done. The cards are ready, is_completed is true and share_link is set. Yes
-1 reject The order failed (anything charged is refunded to your wallet), or EZ PIN cancelled a completed order because its codes have a redemption problem. Yes
-2 created Being recorded. This lasts a moment and you rarely see it. Treat it like pending. No
Order status transitions: an order is accepted right away or becomes pending; a pending order is accepted or rejected; EZ PIN can cancel an accepted order if its codes have a redemption problem. in stock pre-order or supplier delay cards ready failed cancelled: redemption problem POST /orders/ Pending status 0 Accepted status 1 Rejected status -1

How long an order takes

  • Instant orders (pre_order: false, in stock): EZ PIN gets the cards from the supplier during the request. The response usually has status 1. If the supplier is slow, the response can have status 0 and the order finishes later, usually within 5 minutes.
  • Pre-orders (pre_order: true): the response has status 0. The order is accepted when the cards arrive. That is usually 5 to 10 minutes for availability delivery_type 2, and 1 to 2 business days for 3.
  • Pending orders don't expire. EZ PIN checks them at least every 5 minutes until they are accepted or rejected.
  • EZ PIN can cancel a completed order if its codes have a redemption problem. Its status then changes from 1 to -1, and you get a webhook.

Tracking orders

  1. Store the order_id and reference_code from the create response.
  2. If status is 1, get the cards with GET /orders/{reference_code}/cards/.
  3. If status is 0, wait for the webhook. As a backup, poll GET /orders/{reference_code}/: every 30 seconds for the first 10 minutes, then every 5 minutes.
  4. If status is -1, the order failed and your wallet was refunded. To try again, create a new order with a new reference_code.
{
  "order_id": 15513467,
  "status": 1,
  "status_text": "accept",
  "is_completed": true,
  "share_link": "https://vc.ezpaypin.com/bcfe1f46-e49e-4e0a-8941-13ede0dab01f",
  "...": "..."
}
{
  "order_id": 15513468,
  "status": 0,
  "status_text": "pending",
  "is_completed": false,
  "share_link": null,
  "...": "..."
}
import time

def wait_for_order(session, ref, max_wait=3 * 24 * 3600):
    start = time.time()
    while time.time() - start < max_wait:
        order = session.get(f"{BASE_URL}/orders/{ref}/", timeout=30).json()
        if order["status"] in (1, -1):
            return order
        elapsed = time.time() - start
        time.sleep(30 if elapsed < 600 else 300)
    raise TimeoutError(ref)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function waitForOrder(ref, maxWaitMs = 3 * 24 * 3600 * 1000) {
  const start = Date.now();
  while (Date.now() - start < maxWaitMs) {
    const res = await fetch(`${BASE_URL}/orders/${ref}/`, { headers });
    const order = await res.json();
    if (order.status === 1 || order.status === -1) return order;
    await sleep(Date.now() - start < 600_000 ? 30_000 : 300_000);
  }
  throw new Error(`Timed out waiting for ${ref}`);
}
Guide

Delivering cards

You have two options. You can get the codes through the API and give them to your customer yourself. Or EZ PIN can send them for you: set delivery_type and destination when you create the order.

delivery_type Channel destination What the customer gets
0 None (default) Not needed Nothing. You deliver the cards yourself.
1 Email A valid email address An email with every card in the order.
2 SMS A phone number in international format A text with the order number, date, product and a link to the cards.
3 WhatsApp A WhatsApp number in international format A WhatsApp message with a link to the cards.
  • Phone and WhatsApp numbers must include the country code. Use digits only, with an optional leading +: +971501234567 or 971501234567. Spaces, dashes and brackets are rejected with 708. Numbers are stored without the +.
  • EZ PIN sends the message when the order is accepted: right away for instant orders, and later for pre-orders. Nothing is sent for rejected orders.
  • Each order uses one channel. To reach a customer on two channels, send the share_link through your own channel.
  • Errors: an unknown delivery_type returns 726. A missing or invalid destination returns 708.

Links to the cards

share_link
On the order, once it is accepted. Opens a page with every card in the order.
claim_url
On each card from Retrieve order cards. Opens a page with that one card. Use it to send single cards to different people, and for reward_type 1 (link) products.
Card codes are worth money

Anyone with a card code, share_link or claim_url can redeem the card. Store them encrypted, show them only to the buyer, and keep them out of logs and analytics.

{
  "sku": 1148,
  "quantity": 2,
  "price": 25,
  "pre_order": false,
  "reference_code": "3d497a5e-64bd-4b95-a5db-61f42b6c70d5",
  "delivery_type": 1,
  "destination": "[email protected]"
}
{
  "sku": 1148,
  "quantity": 1,
  "price": 25,
  "pre_order": false,
  "reference_code": "88813f11-5ba2-4ea2-b34a-e2ea4998892c",
  "delivery_type": 3,
  "destination": "+971501234567"
}
Guide

Terminals

Terminals are points of sale under your account, such as stores, cash registers or sales agents. Create them in the panel under Users → Terminals. The Terminal ID column shows the ID to send in API calls. You can also give a terminal a PIN.

Pass terminal_id (and terminal_pin if it has a PIN) when you create an order. The order is then recorded under that terminal. This is useful for per-store reports and limits. Without terminal_id, the order is placed as your main account.

How the terminal is identified

You send Result
Neither The order is placed as your main account.
terminal_id The order is placed as that terminal. It must be an active terminal of your account, otherwise you get 617 (HTTP 400).
terminal_pin The order is placed as the terminal that has this PIN.
Both The PIN must belong to that terminal.
  • If EZ PIN has set PIN required on your account, every order must have a terminal_pin. Otherwise it fails with 616.
  • A wrong or inactive PIN returns 617 with HTTP 406.
  • Daily spending limits for the terminal apply to its orders (error 780).

The terminal_id in order responses is the ID of the terminal (or main account) that placed the order.

{
  "sku": 1148,
  "quantity": 1,
  "price": 10,
  "pre_order": false,
  "reference_code": "f0b0c2f8-2a5e-4f4c-9a63-8c1d1e0f3b7a",
  "terminal_id": 1912,
  "terminal_pin": "4821"
}
Guide

Wallets and currencies

Your account has one wallet per currency. GET /balance/ lists them. You top up wallets in the panel.

Which wallet pays

  1. An order is charged to the wallet in the product's currency (the catalog item's currency).
  2. If that wallet doesn't have enough, EZ PIN can charge your wallet in the product's settlement currency instead (set by EZ PIN for each product). The amount is converted at the current exchange rate. No other wallet is tried.
  3. If neither wallet has enough, the order fails with 610. If there is no exchange rate between the two currencies, it fails with 649.

Reading the amounts

  • face_value, total_face_value, total_fees, total_discounts and total_customer_cost are always in the product's currency.
  • If another wallet paid, the order's exchange object shows the exchange_rate, the wallet currency that was charged, and the amount actually charged (exchange.total_customer_cost). Otherwise exchange is an empty object ({}).
  • Amount charged = total_customer_cost ÷ exchange_rate, rounded up.

To avoid conversion, keep enough balance in each product currency you sell.

{
  "product": { "sku": 904, "title": "App Store & iTunes Canada" },
  "face_value": 5.0,
  "count": 1,
  "total_face_value": 5.0,
  "total_fees": 0,
  "total_discounts": -0.088,
  "total_customer_cost": 4.912,
  "exchange": {
    "exchange_rate": 1.3447,
    "total_customer_cost": 3.65,
    "currency": { "currency": "Dollars", "symbol": "$", "code": "USD" }
  }
}

The card costs CAD 4.912. The CAD wallet didn't have enough, so USD 3.65 (4.912 ÷ 1.3447) was charged to the USD wallet.

Authentication

Get an access token

Exchanges your Client ID and Secret Key for an access token. This is the only endpoint that doesn't need an Authorization header.

POST/vendors/v2/auth/token/

Body parameters

client_idstringRequired
Your Client ID from Settings → API Access.
secret_keystringRequired
The Secret Key of the same API key.

Returns

accessstring
The access token (a JWT). Send it as Authorization: Bearer <access>.
expireinteger
How many seconds the token is valid for, counted from now (currently 10800, 3 hours). This is a duration, not a timestamp.

Errors

Code HTTP When
615 400 The client_id and secret_key don't match an active API key.
742 400 The API key, user or company is inactive.
759 400 Your account's owner user is not active or confirmed. Contact support.
— 429 More than 1 token request per second.
{
  "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NfaWQiOjEyLCJleHAiOjE3OTE...",
  "expire": 10800
}
Balance

Get balances

Returns the balance of each of your currency wallets. Orders are charged to the wallet of the product's currency. See Wallets and currencies. The values are live, not cached.

GET/vendors/v2/balance/

Parameters

None.

Returns

An array with one object per wallet, ordered by currency.

currencyobject
The wallet currency.
Show child attributes
currencystring
The currency name, for example Dollars.
symbolstring
The display symbol, for example $ or CAD$.
codestring
The currency code, for example USD. Use this to compare currencies.
balancenumber
The balance you can spend in this currency, with 2 decimal places.
[
  {
    "currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
    "balance": 673.67
  },
  {
    "currency": { "currency": "Dollars", "symbol": "CAD$", "code": "CAD" },
    "balance": 49.5
  },
  {
    "currency": { "currency": "Euro", "symbol": "€", "code": "EUR" },
    "balance": 4.71
  }
]
Catalog

The catalog item object

A product you can buy, at one price range or one fixed denomination, with your pricing. See Products and pricing.

Attributes

skuinteger
The product ID. Send it as sku when you check availability or order. It is not unique in the catalog: products with fixed denominations have one row per denomination.
upcinteger
The product's UPC as a number. Leading zeros are dropped, so prefer upc_string.
upc_stringstring
The 12-digit UPC as a string, with leading zeros.
titlestring
The product name. For fixed denominations it includes the denomination's name.
min_pricenumber
The lowest face value you can order, in currency.
max_pricenumber
The highest face value you can order. The same as min_price for a fixed-price item.
pre_orderboolean
Whether you can order this product with pre_order: true when it is out of stock.
activation_feenumbernullable
A fixed fee per card that is added to the face value. 0 if the product has no fee.
percentage_of_buying_pricenumbernullable
How much your cost differs from the face value, in percent. -2.5 means you pay 2.5 % less than face value, and 1.5 means 1.5 % more. It is 0 when the product has an activation_fee.
showing_priceobjectnullable
Your display price from the panel's display price list, if your account uses one. Only for single-price items. EZ PIN doesn't charge it.
Show child attributes
pricenumber
The display price.
showing_currencyobject
The display currency: currency, symbol, code.
currencyobject
The card's currency, which is also the wallet that is charged: currency (name), symbol, code.
categoriesarray of objects
The categories and sub-categories of the product, each as {"name": "..."}. Names can repeat.
regionsarray of objects
Where the card can be redeemed, each as {"name": "United States", "code": "US"}. GLC means global. [{"name": "Unknown", "code": "NULL"}] means no region is set.
imagestringnullable
The URL of the product image.
descriptionstring
The product description and redemption instructions. It can be empty and can contain HTML.
reward_typeinteger
What the customer receives.
  • 0Gift card: a card number, plus a PIN code for brands that use one.
  • 1Link: a redemption URL.
reward_type_textstring
Gift-Card or Link.
{
  "sku": 1152,
  "upc": 659245710948,
  "upc_string": "659245710948",
  "title": "Amazon Canada",
  "min_price": 5.0,
  "max_price": 500.0,
  "pre_order": false,
  "activation_fee": 0,
  "showing_price": null,
  "percentage_of_buying_price": -2.0,
  "currency": {
    "currency": "Dollars",
    "symbol": "CAD$",
    "code": "CAD"
  },
  "categories": [
    { "name": "Amazon" },
    { "name": "Shopping" }
  ],
  "regions": [
    { "name": "Canada", "code": "CA" }
  ],
  "image": "https://media.ezpaypin.com/media/products/images/2020/09/amazon.jpg",
  "description": "Redeem at amazon.ca/redeem",
  "reward_type": 0,
  "reward_type_text": "Gift-Card"
}
Catalog

List catalog

Returns every product your account can buy, with your prices. The list is not paginated.

GET/vendors/v2/catalogs/

Parameters

None.

Returns

countinteger
The number of items in results.
resultsarray of catalog items
Products with your price ranges, followed by fixed denominations.
Sync the catalog, don't call it on every request

Store the catalog in your own database. Sync it once a day, and again when EZ PIN notifies you of catalog changes. Serve products and prices from your copy, and use Check availability for live stock.

The response is cached for each access token for up to 60 minutes. It can be large, so allow a 60-second timeout.

The catalog only has products that are assigned to your account with a price and are active at EZ PIN and at the supplier. An item can be listed and still be out of stock, so check availability before you promise delivery. Sandbox keys only see sandbox products.

{
  "count": 2,
  "results": [
    {
      "sku": 1152,
      "upc": 659245710948,
      "upc_string": "659245710948",
      "title": "Amazon Canada",
      "min_price": 5.0,
      "max_price": 500.0,
      "pre_order": false,
      "activation_fee": 0,
      "showing_price": null,
      "percentage_of_buying_price": -2.0,
      "currency": { "currency": "Dollars", "symbol": "CAD$", "code": "CAD" },
      "categories": [{ "name": "Amazon" }],
      "regions": [{ "name": "Canada", "code": "CA" }],
      "image": "https://media.ezpaypin.com/media/products/images/2020/09/amazon.jpg",
      "description": "",
      "reward_type": 0,
      "reward_type_text": "Gift-Card"
    },
    {
      "sku": 562,
      "upc": 659245724761,
      "upc_string": "659245724761",
      "title": "Call of Duty: Modern Warfare 2,400 Points",
      "min_price": 19.99,
      "max_price": 19.99,
      "pre_order": true,
      "activation_fee": 0,
      "showing_price": {
        "price": 35.0,
        "showing_currency": { "currency": "Lira", "symbol": "TL", "code": "TRY" }
      },
      "percentage_of_buying_price": 2.0,
      "currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
      "categories": [{ "name": "Call of Duty" }],
      "regions": [{ "name": "Global", "code": "GLC" }],
      "image": "https://media.ezpaypin.com/media/products/images/2020/08/2400_CALL_OF_DUTY.jpg",
      "description": "",
      "reward_type": 0,
      "reward_type_text": "Gift-Card"
    }
  ]
}
Catalog

Check availability

Checks whether a quantity of a product, at a price, can be delivered now, can only be pre-ordered, or is out of stock. See Stock and pre-orders.

GET/vendors/v2/catalogs/{sku}/availability/

Path parameters

skuintegerRequired
The sku of a catalog item.

Query parameters

item_countintegerRequired
The number of cards you want to order.
pricenumberRequired
The face value per card, between the item's min_price and max_price.

Returns

availabilityboolean
true only when the cards can be delivered now (delivery_type 1).
detailstring
This catalog is available. or This catalog is not available.
delivery_typeinteger
The stock status.
  • 1Available now. Order with pre_order: false.
  • 2Not in stock. Order with pre_order: true. Usually filled in 5 to 10 minutes.
  • 3Not in stock. Order with pre_order: true. Usually filled in 1 to 2 business days.
  • 0Out of stock, unknown SKU, or price out of range. Can't be ordered.
delivery_type_textstring
A readable label for delivery_type. For display only.

Errors

Code HTTP When
648 200 item_count or price is missing or not a number, or the supplier's stock check failed. Retry shortly.
602 200 The supplier doesn't know this product.
605 200 The supplier doesn't offer this price.
614 200 The supplier reports not enough stock.
619 200 The product is temporarily unavailable.

These errors come back with HTTP 200, so check for the availability field before you read the result. See Errors.

This endpoint is not rate limited. For products that don't come from a supplier, stock counts are cached for up to 15 minutes.

{
  "availability": true,
  "detail": "This catalog is available.",
  "delivery_type": 1,
  "delivery_type_text": "available"
}
Orders

The order object

A purchase of one or more cards of one product at one price. Every order endpoint returns this object.

Attributes

order_idinteger
EZ PIN's ID for the order. Webhooks use it, so store it.
reference_codestring
The UUID you sent when you created the order. It is returned by Create and List, but not by Retrieve, where it is already in the URL.
statusinteger
  • 0Pending: paid, waiting for the cards.
  • 1Accepted: the cards are ready.
  • -1Rejected: the order failed and was refunded, or EZ PIN cancelled it because its codes have a redemption problem.
  • -2Created: being recorded (very short). Treat it like pending.
status_textstring
pending, accept, reject or created.
created_timestring
When the order was created, in ISO 8601 format and UTC, for example 2026-06-20T11:17:21.743063.
delivery_typeinteger
How EZ PIN sends the cards to your customer: 0 none, 1 email, 2 SMS, 3 WhatsApp. See Delivering cards.
destinationstringnullable
The email address or phone number the cards are sent to. null when delivery_type is 0.
terminal_idintegernullable
The terminal (or main account) that placed the order. See Terminals.
productobject
sku (integer) and title (string) of the product ordered.
face_valuenumber
The value of one card: the price you ordered at.
countinteger
The number of cards: the quantity you ordered.
total_face_valuenumber
face_value × count.
total_feesnumber
The total activation fees for products with an activation_fee, and 0 for other products.
total_discountsnumber
The total price difference for products priced as a percentage. Negative is a discount (you pay less than face value) and positive is a markup. 0 for products with an activation fee.
total_customer_costnumber
What you pay, in the product's currency: total_face_value + total_fees + total_discounts.
is_completedboolean
true when every card in the order is ready.
share_linkstringnullable
A link to a page with every card in the order. null until the order is accepted. Anyone with the link can see the codes.
exchangeobject
Set when the order was paid from another currency's wallet. Otherwise {}.
Show child attributes
exchange_ratenumber
Units of the product's currency per unit of the charged currency.
total_customer_costnumber
The amount charged to the wallet below.
currencyobject
The currency of the wallet that was charged: currency, symbol, code.
{
  "order_id": 15513467,
  "delivery_type": 1,
  "destination": "[email protected]",
  "status": 1,
  "status_text": "accept",
  "created_time": "2026-06-20T11:17:21.743063",
  "terminal_id": 1912,
  "reference_code": "5133fce1-7d02-4f37-9c01-73459d1735e2",
  "product": {
    "sku": 1148,
    "title": "App Store & iTunes US"
  },
  "face_value": 25.0,
  "count": 2,
  "total_face_value": 50.0,
  "total_fees": 0,
  "total_discounts": -1.0,
  "total_customer_cost": 49.0,
  "is_completed": true,
  "share_link": "https://vc.ezpaypin.com/bcfe1f46-e49e-4e0a-8941-13ede0dab01f",
  "exchange": {}
}
Orders

Create an order

Buys quantity cards of one product at one price and charges your wallet. The response is the order object. Check its status: 1 means the cards are ready now, and 0 means they will come later (see Order lifecycle).

POST/vendors/v2/orders/

Body parameters

skuintegerRequired
The sku of a catalog item.
pricenumberRequired
The face value of each card, with at most 2 decimals. It must be between the item's min_price and max_price. For a fixed denomination, send its exact price.
quantityintegerRequired
The number of cards, at least 1. Some products limit the quantity per order (741), the value per order (740) or the quantity per day (639).
pre_orderbooleanRequired
false: fill the order now or refuse it. true: accept the order even if the cards aren't in stock and deliver them later. See Stock and pre-orders.
reference_codestringRequired
A new lowercase UUID v4 for this order. See Reference codes.
delivery_typeintegerOptional
Have EZ PIN send the cards to your customer when the order is accepted.
  • 0None (default). You deliver the cards.
  • 1Email. destination is an email address.
  • 2SMS. destination is a phone number.
  • 3WhatsApp. destination is a WhatsApp number.
destinationstringRequired if delivery_type is 1–3
The email address, or the phone number in international format (digits with an optional leading +, for example +971501234567).
terminal_idintegerOptional
Place the order as one of your terminals.
terminal_pinstringOptional
The PIN of the terminal placing the order. Required if your account has PIN required set.

The v1 fields email, pin and store_id are not part of v2. Use delivery_type with destination, and terminal_pin, instead.

Returns

The order object, including reference_code. Rate limit: 1 request per second (see Rate limits). Use a client timeout of at least 120 seconds.

Errors

Code HTTP When
661 403 API ordering isn't enabled for your account.
798 403 Your KYC verification isn't approved.
746 400 Your account isn't allowed to buy digital cards.
708 400 reference_code is missing, or destination is missing or invalid for the chosen delivery_type. detail names the field.
645 400 reference_code isn't a lowercase UUID v4.
680 400 The reference_code has already been used. Look the order up.
726 400 delivery_type isn't 0, 1, 2 or 3.
602 400 The SKU is unknown or inactive, or it isn't a sandbox product and you used a sandbox key.
711 400 The product can't be sold through the API. The message lists where it is sold.
612 400 The price is outside the item's min_price to max_price range.
605 400 The price isn't one of the product's fixed denominations.
690 400 The product isn't priced for your account at this price.
692 400 The product can't be sold at this price. Contact support.
613 400 You sent pre_order: true, but this product can't be pre-ordered.
614 400 Not enough stock to deliver now. Retry with pre_order: true, or lower the quantity.
635 400 Out of stock, and the product can't be pre-ordered now.
619 400 The product is temporarily unavailable.
741 400 quantity is above the product's maximum per order.
740 400 price × quantity is above the product's maximum value per order.
639 400 Your daily quantity limit for this SKU is reached. It resets at 00:00 UTC.
779 400 Your company's daily spending limit in this currency is reached.
780 400 The daily spending limit of the terminal or user is reached.
610 400 Your wallet balance is too low.
649 400 Your product-currency wallet is too low, and there is no exchange rate to pay from your other wallet.
616 400 Your account requires a PIN and terminal_pin is missing.
617 400 terminal_id isn't an active terminal of your account.
617 406 terminal_pin is wrong or inactive, or belongs to another terminal.
618 400 The order couldn't be created. If it was recorded, it is saved as rejected. Retry with a new reference_code.
611 400 Calculating the cost failed. The order is saved as rejected and refunded. Retry with a new reference_code.
621 400 The supplier didn't respond in time. The order is saved as rejected and refunded. Retry with a new reference_code.
— 429 More than 1 order request per second.

Field validation errors (for example a missing sku) use the field error format. All codes are described in the error code reference.

{
  "order_id": 15513467,
  "delivery_type": 1,
  "destination": "[email protected]",
  "status": 1,
  "status_text": "accept",
  "created_time": "2026-06-20T11:17:21.743063",
  "terminal_id": 1912,
  "reference_code": "5133fce1-7d02-4f37-9c01-73459d1735e2",
  "product": { "sku": 1148, "title": "App Store & iTunes US" },
  "face_value": 25.0,
  "count": 2,
  "total_face_value": 50.0,
  "total_fees": 0,
  "total_discounts": -1.0,
  "total_customer_cost": 49.0,
  "is_completed": true,
  "share_link": "https://vc.ezpaypin.com/bcfe1f46-e49e-4e0a-8941-13ede0dab01f",
  "exchange": {}
}
{
  "detail": "Not enough cards available. You can try the pre-order option.",
  "code": "614"
}
Orders

Retrieve an order

Returns one order by its reference code. Use it to check the status of a pending order, or to find out whether a request with an unknown result created an order.

GET/vendors/v2/orders/{reference_code}/

Path parameters

reference_codestringRequired
The UUID you sent when you created the order.

Returns

The order object, without reference_code.

Errors

Code HTTP When
— 404 Your account has no order with this reference code. The body is {"detail": "Not found."}.
{
  "order_id": 15513468,
  "delivery_type": 0,
  "destination": null,
  "status": 0,
  "status_text": "pending",
  "created_time": "2026-06-20T11:20:02.118400",
  "terminal_id": 1912,
  "product": { "sku": 1159, "title": "Amazon DE" },
  "face_value": 10.0,
  "count": 1,
  "total_face_value": 10.0,
  "total_fees": 3.09,
  "total_discounts": 0,
  "total_customer_cost": 13.09,
  "is_completed": false,
  "share_link": null,
  "exchange": {}
}
Orders

Retrieve order cards

Returns the card codes of an accepted order. You can call it again later to get the same cards. Every retrieval is logged.

GET/vendors/v2/orders/{reference_code}/cards/

Path parameters

reference_codestringRequired
The order's reference code.

Returns

{"results": [ ... ]}, one card object per card. The list is not paginated. For a few products the API doesn't return card codes and results is empty. In that case, give the customer the order's share_link.

card_numberstring
The card number or redemption code. Always present.
pin_codestringnullable
The PIN that goes with the card number. null when the brand doesn't use a PIN, so show it only when it is set.
claim_urlstringnullable
A link to a page with this card, ready to send to the customer. Use it for reward_type 1 (link) products.
expire_datestring
The card's expiry date as the brand gives it, or an empty string if it has none.
barcodestring
A barcode value to show for in-store redemption, or an empty string.
barcode_standardstring
The barcode format to draw barcode with, or an empty string.
reward_typeinteger
0 gift card, 1 link.
reward_type_textstring
Gift-Card or Link.

Errors

Code HTTP When
609 400 The order is still pending. Wait for the webhook, or retry in 30 seconds or more.
793 400 The order was rejected and has no cards.
710 400 The supplier didn't return the cards. Contact support with the reference_code.
699 400 The supplier couldn't be reached. Retry shortly.
— 404 Your account has no order with this reference code.
{
  "results": [
    {
      "card_number": "ZNNPZRRYP49VT9UK",
      "pin_code": "393145478240831916",
      "claim_url": "https://vc.ezpaypin.com/card/9853476c-3b05-4871-a56f-9bb7eb33891d",
      "expire_date": "",
      "reward_type": 0,
      "reward_type_text": "Gift-Card",
      "barcode": "",
      "barcode_standard": ""
    },
    {
      "card_number": "NWUJ7LAR3KGTU77X",
      "pin_code": "930854294202589407",
      "claim_url": "https://vc.ezpaypin.com/card/db095ef3-7d21-4307-8cdc-3b1c0fa300c3",
      "expire_date": "",
      "reward_type": 0,
      "reward_type_text": "Gift-Card",
      "barcode": "",
      "barcode_standard": ""
    }
  ]
}
{
  "detail": "Order is not ready yet. Please try again later.",
  "code": 609
}
Orders

List orders

Returns your company's orders created in a time range. This includes orders placed in the panel and through other channels, not just API orders. Use it for reconciliation and reports.

GET/vendors/v2/orders/

Query parameters

start_datestringRequired
Orders created at or after this time, in UTC: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS. See Dates and times.
end_datestringRequired
Orders created at or before this time, in UTC. A plain date means midnight at the start of that day.
limitintegerOptional
The page size. The default is 10. See Pagination.
offsetintegerOptional
The number of results to skip.

Returns

A paginated list of order objects, each with reference_code. The order of results isn't guaranteed, so sort by created_time if you need to.

Errors

Code HTTP When
600 400 start_date or end_date is missing.
699 400 A date has the wrong format.
{
  "count": 66,
  "next": "https://api.ezpaypin.com/vendors/v2/orders/?end_date=2026-09-30T23%3A59%3A59&limit=2&offset=2&start_date=2026-09-01",
  "previous": null,
  "results": [
    {
      "order_id": 156609,
      "delivery_type": 2,
      "destination": "18885051234",
      "status": 1,
      "status_text": "accept",
      "created_time": "2026-09-09T06:28:55.467051",
      "terminal_id": null,
      "reference_code": "3d497a5e-64bd-4b95-a5db-61f42b6c70d5",
      "product": { "sku": 21, "title": "Amazon US" },
      "face_value": 10.0,
      "count": 1,
      "total_face_value": 10.0,
      "total_fees": 0,
      "total_discounts": 0.1,
      "total_customer_cost": 10.1,
      "is_completed": true,
      "share_link": "https://vc.ezpaypin.com/01c79b97-8ab4-423c-a11d-225897f2135b",
      "exchange": {}
    },
    {
      "order_id": 156610,
      "delivery_type": 1,
      "destination": "[email protected]",
      "status": 0,
      "status_text": "pending",
      "created_time": "2026-09-09T09:08:23.167070",
      "terminal_id": null,
      "reference_code": "88813f11-5ba2-4ea2-b34a-e2ea4998892c",
      "product": { "sku": 1159, "title": "Amazon DE" },
      "face_value": 10.0,
      "count": 1,
      "total_face_value": 10.0,
      "total_fees": 3.09,
      "total_discounts": 0,
      "total_customer_cost": 13.09,
      "is_completed": false,
      "share_link": null,
      "exchange": {}
    }
  ]
}
Retailer products

The retailer product object

Retailer products are the same products as the catalog, each tied to one fixed price and identified by an EAN-13 product_code. Use them if your point-of-sale system works with barcodes: scan or send one code instead of a SKU and price.

Product code format

A product_code has 13 digits: the SKU (5 digits, zero-padded), the price in cents (7 digits, zero-padded) and an EAN-13 check digit. For example, 0091800010009 is SKU 918 at 10.00. You don't need to build codes. Use them as returned by List retailer products.

Attributes

product_codestring
The EAN-13 code for this product and price. Send it to the retailer availability and order endpoints.
pricenumber
The face value of the card, in the product's currency.
buying_pricenumbernullable
What one card costs you, fees included, in the product's currency.
productobject
The product.
Show child attributes
skuinteger
The product ID.
titlestring
The product name.
currencyobject
currency, symbol, code.
categoriesarray of objects
Each as {"name": "..."}.
upcstring
The 12-digit UPC, as a string here.
imagestringnullable
The product image URL.
pre_orderboolean
Whether the product can be pre-ordered.
descriptionstring
The product description.
regionsarray of objects
Each as {"name", "code"}.
reward_typeinteger
0 gift card, 1 link.
reward_type_textstring
Gift-Card or Link.
{
  "product_code": "0091800010009",
  "price": 10.0,
  "buying_price": 10.2,
  "product": {
    "sku": 918,
    "title": "App Store & iTunes US",
    "currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
    "categories": [
      { "name": "App Store & iTunes US" },
      { "name": "Software" }
    ],
    "upc": "659245727236",
    "image": "https://media.ezpaypin.com/media/products/images/2021/11/itunes2.jpg",
    "pre_order": true,
    "description": "",
    "regions": [{ "name": "United States", "code": "US" }],
    "reward_type": 0,
    "reward_type_text": "Gift-Card"
  }
}
0 0 9 1 8 | 0 0 0 1 0 0 0 | 9
  SKU 918 |   price 10.00 | check digit
Retailer products

List retailer products

Returns every retailer product you can buy. The list is not paginated, and it is cached for each access token for up to 60 minutes.

GET/vendors/v2/retailer_products/

Parameters

None.

Returns

countinteger
The number of items in results.
resultsarray of retailer products
The retailer products.
{
  "count": 1,
  "results": [
    {
      "product_code": "0091800010009",
      "price": 10.0,
      "buying_price": 10.2,
      "product": {
        "sku": 918,
        "title": "App Store & iTunes US",
        "currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
        "categories": [{ "name": "App Store & iTunes US" }, { "name": "Software" }],
        "upc": "659245727236",
        "image": "https://media.ezpaypin.com/media/products/images/2021/11/itunes2.jpg",
        "pre_order": true,
        "description": "",
        "regions": [{ "name": "United States", "code": "US" }],
        "reward_type": 0,
        "reward_type_text": "Gift-Card"
      }
    }
  ]
}
Retailer products

Check retailer availability

Like Check availability, but for a product_code. Both parameters are query parameters.

GET/vendors/v2/retailer_product_availability/

Query parameters

product_codestringRequired
The product_code of a retailer product.
item_countintegerRequired
The number of cards you want to order.

Returns

The same fields as Check availability: availability, detail, delivery_type and delivery_type_text.

Errors

Code HTTP When
708 200 item_count or product_code is missing or invalid. detail names the field.
792 200 The product code isn't a valid EAN-13 code.
602 200 No product matches the code.
605 200 The supplier doesn't offer this price.
614 200 The supplier reports not enough stock.
619 200 The product is temporarily unavailable.
648 200 The supplier's stock check failed. Retry shortly.

These errors come back with HTTP 200, so check for the availability field before you read the result.

This endpoint counts toward the general limit of 100 requests per minute.

{
  "availability": true,
  "detail": "This catalog is available.",
  "delivery_type": 1,
  "delivery_type_text": "available"
}
Retailer products

Create a retailer order

Like Create an order, but the product and price come from a product_code. The new order works with Retrieve an order, Retrieve order cards and webhooks like any other order.

POST/vendors/v2/retailer_order/

Body parameters

product_codestringRequired
The product_code of a retailer product. It sets the SKU and the price.
quantityintegerRequired
The number of cards, at least 1.
pre_orderbooleanRequired
The same as in Create an order.
reference_codestringRequired
A new lowercase UUID v4. See Reference codes.
delivery_typeintegerOptional
0 none (default), 1 email, 2 SMS, 3 WhatsApp.
destinationstringRequired if delivery_type is 1–3
The email address or phone number in international format.
terminal_idintegerOptional
Place the order as one of your terminals.
terminal_pinstringOptional
The terminal PIN. Required if your account has PIN required set.

Returns

The order object, including reference_code.

Errors

Code HTTP When
746 400 Your account isn't allowed to buy digital cards.
708 400 reference_code is missing, or destination is missing or invalid for the chosen delivery_type. detail names the field.
645 400 reference_code isn't a lowercase UUID v4.
792 400 product_code isn't a valid EAN-13 code from List retailer products.
680 400 The reference_code has already been used. Look the order up.
726 400 delivery_type isn't 0, 1, 2 or 3.
602 400 The product in the product code is unknown or inactive, or it isn't a sandbox product and you used a sandbox key.
711 400 The product can't be sold through the API. The message lists where it is sold.
612 400 The price in the product code is outside the product's current price range. Get a fresh code from List retailer products.
605 400 The price in the product code isn't offered for this product.
690 400 The product isn't priced for your account at this price.
692 400 The product can't be sold at this price. Contact support.
613 400 You sent pre_order: true, but this product can't be pre-ordered.
614 400 Not enough stock to deliver now. Retry with pre_order: true, or lower the quantity.
635 400 Out of stock, and the product can't be pre-ordered now.
619 400 The product is temporarily unavailable.
741 400 quantity is above the product's maximum per order.
740 400 price × quantity is above the product's maximum value per order.
639 400 Your daily quantity limit for this SKU is reached. It resets at 00:00 UTC.
779 400 Your company's daily spending limit in this currency is reached.
780 400 The daily spending limit of the terminal or user is reached.
610 400 Your wallet balance is too low.
649 400 Your product-currency wallet is too low, and there is no exchange rate to pay from your other wallet.
616 400 Your account requires a PIN and terminal_pin is missing.
617 400 terminal_id isn't an active terminal of your account.
617 406 terminal_pin is wrong or inactive, or belongs to another terminal.
618 400 The order couldn't be created. If it was recorded, it is saved as rejected. Retry with a new reference_code.
611 400 Calculating the cost failed. The order is saved as rejected and refunded. Retry with a new reference_code.
621 400 The supplier didn't respond in time. The order is saved as rejected and refunded. Retry with a new reference_code.
— 429 More than 1 order request per second.
{
  "order_id": 156734,
  "delivery_type": 3,
  "destination": "18004441234",
  "status": 1,
  "status_text": "accept",
  "created_time": "2026-09-19T12:46:19.577169",
  "terminal_id": 97,
  "reference_code": "bb715b4e-6792-45ca-9ca5-2d708f41a6e0",
  "product": { "sku": 918, "title": "App Store & iTunes US" },
  "face_value": 10.0,
  "count": 1,
  "total_face_value": 10.0,
  "total_fees": 0,
  "total_discounts": 0.2,
  "total_customer_cost": 10.2,
  "is_completed": true,
  "share_link": "https://vc.ezpaypin.com/01c79b97-8ab4-423c-a11d-225897f2135a",
  "exchange": {}
}
Exchange rates

List exchange rates

Returns the currency exchange rates EZ PIN uses when an order is paid from a wallet in another currency. See Wallets and currencies.

GET/vendors/v2/exchange_rates/

Parameters

None.

Returns

countinteger
The number of rates.
resultsarray of objects
The active rates.
Show child attributes
from_currencyobject
The currency of the wallet that pays: currency, symbol, code.
to_currencyobject
The currency of the product being bought.
ratenumber
How many units of to_currency one unit of from_currency buys. Up to 6 decimals. Amount charged in from_currency = amount in to_currency ÷ rate.

Rates can change during the day. The rate applied to an order is returned in the order's exchange object.

{
  "count": 2,
  "results": [
    {
      "from_currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
      "to_currency": { "currency": "Dollars", "symbol": "CAD$", "code": "CAD" },
      "rate": 1.3447
    },
    {
      "from_currency": { "currency": "Dollars", "symbol": "$", "code": "USD" },
      "to_currency": { "currency": "Euro", "symbol": "€", "code": "EUR" },
      "rate": 0.83
    }
  ]
}

With the first rate, a CAD 4.912 order paid from the USD wallet costs 4.912 ÷ 1.3447 = USD 3.65.

Webhooks

Webhooks

EZ PIN can send an HTTPS POST to your server when an API order becomes accepted (1) or rejected (-1). With webhooks you don't need to poll for pre-orders and delayed orders.

Set up

  1. Create an HTTPS endpoint that accepts JSON POST requests.
  2. Generate a long random secret (32 characters or more) to use as the confidential_key.
  3. Register both with POST /notification_config/. Each API key has its own settings, so register once with your sandbox token and once with your production token.

Notifications for an order go to the endpoint of the API key that created it. Orders placed in the panel don't send webhooks.

Event payload

The request has Content-Type: application/json and this body:

type_codeinteger
Always 1 (digital order).
type_textstring
Always digital.
order_idinteger
The order_id from the create response. The payload has no reference_code, so keep a mapping from order_id to your reference_code.
statusinteger
The order status when the notification was sent: 1 accepted or -1 rejected.
confidential_keystring
Your confidential_key, sent back so you can verify that the request is from EZ PIN. Not included if you haven't set one.

Responding and retries

  • Return HTTP 200 to acknowledge the event. Any other status counts as a failure, including 201 and 204, and so do timeouts and connection errors.
  • Respond within a few seconds. Save the event and do the work in the background.
  • A failed delivery is retried twice, 3 minutes apart: 3 attempts in total over about 6 minutes. After that the event is dropped, so reconcile with Retrieve an order or List orders.

Handling events correctly

  • Verify the confidential_key with a constant-time comparison. Return 200 and ignore the event if it doesn't match, so a probe gets no hint.
  • Confirm with GET /orders/{reference_code}/ before you release cards or goods. Treat the webhook as a signal to check, not as proof.
  • Expect duplicates. Make your handler idempotent, for example by recording the order_id and status you've already processed.
  • Expect any order. The webhook for an instant order can arrive before your POST /orders/ call returns. An accepted order becomes rejected if EZ PIN cancels it because its codes have a redemption problem.
  • Unknown order_id: a create request that failed with an error (see Reference codes) can still send a reject webhook for an order you never saw. Return 200 and ignore it.
  • Use HTTPS with a valid certificate. Your confidential_key is in the body, so never use plain HTTP.
POST /ezpin/webhook/ HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "type_code": 1,
  "type_text": "digital",
  "order_id": 15513467,
  "status": 1,
  "confidential_key": "k9V2pXr7..."
}
# Flask
import hmac
import os

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["EZPIN_WEBHOOK_KEY"]


@app.post("/ezpin/webhook/")
def ezpin_webhook():
    event = request.get_json(silent=True) or {}
    key = str(event.get("confidential_key", ""))
    if not hmac.compare_digest(key, SECRET):
        return "", 200                       # ignore, reveal nothing

    # Save it and process in the background, idempotently.
    enqueue_order_check(event["order_id"], event["status"])
    return "", 200
// Express
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = Buffer.from(process.env.EZPIN_WEBHOOK_KEY);

app.post("/ezpin/webhook/", express.json(), (req, res) => {
  const key = Buffer.from(String(req.body?.confidential_key ?? ""));
  const valid = key.length === SECRET.length && crypto.timingSafeEqual(key, SECRET);
  if (!valid) return res.sendStatus(200); // ignore, reveal nothing

  // Save it and process in the background, idempotently.
  enqueueOrderCheck(req.body.order_id, req.body.status);
  res.sendStatus(200);
});

app.listen(3000);
<?php
// ezpin-webhook.php
$event = json_decode(file_get_contents('php://input'), true) ?: [];
$key = (string) ($event['confidential_key'] ?? '');

if (!hash_equals(getenv('EZPIN_WEBHOOK_KEY'), $key)) {
    http_response_code(200); // ignore, reveal nothing
    exit;
}

// Save it and process in the background, idempotently.
enqueue_order_check($event['order_id'], $event['status']);
http_response_code(200);
Webhooks

Get webhook settings

Returns the webhook settings of the API key that issued the access token.

GET/vendors/v2/notification_config/

Parameters

None.

Returns

endpointstringnullable
Your webhook URL. null or empty means webhooks are off.
confidential_keystringnullable
The key that is sent in every event.
{
  "endpoint": "https://example.com/ezpin/webhook/",
  "confidential_key": "k9V2pXr7..."
}
Webhooks

Update webhook settings

Sets the webhook URL and key of the API key that issued the access token. Only the fields you send are changed. To turn webhooks off, send "endpoint": null.

POST/vendors/v2/notification_config/

Body parameters

endpointstringOptional
A full URL, up to 200 characters, for example https://example.com/ezpin/webhook/. Use HTTPS.
confidential_keystringOptional
A secret of up to 255 characters that is sent back in every event. Use at least 32 random characters.

Returns

{"detail": "API setting added successfully.", "code": 801}. An invalid URL returns a field error, for example {"endpoint": ["Enter a valid URL."]}.

To rotate the key without missing events, make your receiver accept both the old and the new key for a while. Then send the new key here, and stop accepting the old one.

{
  "detail": "API setting added successfully.",
  "code": 801
}
Resources

Error codes

Every code the Access API v2 can return, with its HTTP status, its message (the detail field) and what to do about it. Messages can be reworded, but codes stay the same. See Errors for the response format.

Code HTTP Message What to do
Authentication and account
615 400 API access not found. Please check your credentials. Check the client_id and secret_key, and that the API key is active.
633 400 User not found. The API key behind this token was deleted. Create a new key and token.
742 400 Your account is inactive. The API key, user or company is deactivated. Reactivate the key in the panel or contact support.
743 400 Your IP address is not whitelisted. Your current IP address is {ip_address}. Call from a trusted IP, or add the IP shown in the message to the API key in the panel (Settings → API Access).
759 400 Merchant not found. Your account's owner user is inactive or unconfirmed. Contact support.
764 401 The token is invalid or has expired. Get a new access token.
722 400 Your phone and WhatsApp numbers must be confirmed, and two-step verification must be enabled. Panel only, when you create API keys. Confirm both numbers and turn on two-step verification.
746 400 You do not have purchase permission. Your account isn't allowed to buy digital cards. Contact your account manager.
661 403 Digital orders are not permitted. API ordering isn't enabled for your account. Contact your account manager.
798 403 Your KYC verification has not been approved. Complete KYC verification in the panel.
Request validation
600 400 Not allowed. List orders: send both start_date and end_date. Also returned by the old /orders/notification_config/ path.
645 400 Invalid UUID format for reference code. Send a lowercase UUID v4 with hyphens.
648 200 Bad request: the 'item_count' and 'price' fields are required and must be numbers. Availability endpoints: send item_count and price as numbers. Also returned when the supplier's stock check fails, so retry shortly.
680 400 Invalid reference code. The reference_code was already used. Look the order up, and use a new code for a new order.
708 400 / 200 The '{field_name}' field is missing, empty, or invalid. Fix the field named in the message: reference_code, destination, item_count or product_code.
726 400 Invalid delivery type. Use a delivery_type from 0 to 3.
792 400 / 200 Invalid product code. Use a product_code from List retailer products.
Products and prices
602 400 / 200 No product found. Unknown or inactive SKU, or a non-sandbox SKU with a sandbox key. Refresh your catalog.
605 400 / 200 Price not permitted. This product only sells fixed denominations. Use a price from the catalog.
612 400 Product price is incorrect. Use a price between min_price and max_price.
613 400 You cannot pre-order this product. Send pre_order: false. The catalog shows which products allow pre-orders.
619 400 / 200 This product is temporarily unavailable. Try again later or offer another product.
690 400 Product is not assigned to you. Your account has no price for this product and amount. Refresh your catalog.
692 400 The order cannot be placed at this price. Please contact the administrator. A pricing problem on EZ PIN's side. Contact support.
711 400 This product is only available via: {methods}. The product can't be sold through the API. Remove it from your offer.
Stock
614 400 / 200 Not enough cards available. You can try the pre-order option. Order with pre_order: true, lower the quantity, or try later.
635 400 Not enough cards available. Out of stock and can't be pre-ordered. Try later.
Limits
639 400 You have reached your daily purchase limit for this SKU. Your account's daily quantity for this product is used up. It resets at 00:00 UTC.
740 400 The maximum amount for this product is {amount} per transaction. Lower price × quantity, or split the order.
741 400 The maximum quantity for this product is {quantity} per transaction. Lower quantity, or split the order.
779 400 The company's daily limit has been reached. Your company's daily spending limit in this currency is used up. Try tomorrow or ask to raise it.
780 400 The user's daily limit has been reached. The daily spending limit of the terminal or user is used up.
Payment
610 400 Your balance is not sufficient. Top up the wallet in the product's currency (see Wallets).
649 400 Exchange rate not found. Your product-currency wallet is short and no rate exists to pay from another wallet. Top up the product-currency wallet.
Terminals
616 400 PIN or terminal is required. Your account requires a PIN. Send terminal_pin.
617 400 / 406 Incorrect PIN or terminal. 400: terminal_id isn't an active terminal of yours. 406: the PIN is wrong or inactive, or belongs to another terminal.
Order processing
609 400 Order is not ready yet. Please try again later. The order is pending. Wait for the webhook or retry later.
611 400 An error occurred while calculating activation costs. The order was rejected and refunded. Retry with a new reference_code.
618 400 An error occurred while creating the order. The order was rejected and refunded. Retry with a new reference_code. If it keeps happening, contact support.
621 400 An error occurred. Please try again later. The supplier didn't respond in time. The order was rejected and refunded. Retry with a new reference_code.
710 400 Digital cards not found. The supplier didn't return the cards. Contact support with the reference_code.
793 400 The digital order was rejected. The order has no cards. Create a new order if needed.
General
699 400 An error occurred. Please try again later. Unexpected error. Check the date format on List orders. Retry with backoff, and contact support if it continues.
900 400 The service is down for maintenance. Please try again later. Scheduled maintenance. Retry later.
Success messages
801 200 API setting added successfully. Webhook settings saved.
Resources

FAQ

My order request timed out. Did I buy the cards?

Maybe. Look it up with GET /orders/{reference_code}/. If it returns the order, it was created. If you get 404, retry with the same reference_code. A duplicate is rejected with 680, so you can't buy twice. See Retrying safely.

Why does the same SKU appear several times in the catalog?

Products sold in fixed denominations have one row per denomination, all with the same sku. Identify an item by sku + price. See Products and pricing.

What is the difference between region and currency?

They are separate. regions says where the card can be redeemed. currency is the card's currency, which is also the wallet that pays for it. A card can be redeemable worldwide and still be priced in USD.

Availability is false. Can I still order?

If delivery_type is 2 or 3, yes: order with pre_order: true and the cards are delivered later. If it is 0, the product can't be ordered right now.

How long do pre-orders take?

Usually 5 to 10 minutes when availability delivery_type is 2, and 1 to 2 business days when it is 3. Pending orders don't expire. You get a webhook when they finish.

Can I cancel an order?

No. Orders can't be cancelled through the API or on request. EZ PIN only cancels an order when its codes have a redemption problem. If a customer can't redeem a code, contact support with the reference_code. EZ PIN first checks the codes with the brand, then decides whether to cancel the order. If it is cancelled, its status changes to -1 and you get a webhook.

Can I get the card codes again later?

Yes. GET /orders/{reference_code}/cards/ returns the same cards every time. Every retrieval is logged.

I'm not receiving webhooks.
  • Check the settings with GET /notification_config/, using a token from the same API key (sandbox and production have separate settings).
  • Your endpoint must be reachable from the internet over HTTPS and return exactly 200.
  • Each event is tried 3 times over about 6 minutes. If your server was down longer, reconcile with List orders.
  • Orders placed in the panel don't send webhooks.
Why is total_discounts negative?

A negative value is a discount: you paid less than face value. total_customer_cost = total_face_value + total_fees + total_discounts.

How often should I sync the catalog?

Once a day, and again whenever EZ PIN notifies you of catalog changes, such as new products, removed products or price changes. Store the catalog in your own database and serve products and prices from there. Don't call GET /catalogs/ for every customer request. For live stock, use Check availability.

My server's IP address changed and every request fails with 743.

The message shows the IP address EZ PIN sees. Add that address to your API key's trusted IPs yourself in the panel, under Settings → API Access. Support can't change trusted IPs for you.

Do sandbox orders cost money?

No. Sandbox products are priced in the test currency EZD, and sandbox orders are paid from your EZD test wallet. Your real wallets are never charged. If your EZD balance runs out, ask your account manager to top it up.

Resources

Changelog

October 2026

  • The documentation was rewritten: every endpoint now lists all of its parameters, response fields and errors, with request samples in cURL, Python, Node.js and PHP.
  • Removed: the physical card endpoints (/cards/, /cards/check/, /cards/activate/, /cards/{reference_code}/) are no longer part of the Access API.
  • Error messages were reworded for clarity. Error codes did not change. Codes that the API no longer returns were removed from the error code reference.
  • The Postman collection was updated. It now refreshes the access token automatically.

Deprecated

  • /orders/notification_config/ returns error 600 in v2. Use /notification_config/.
  • The v1 API (/vendors/v1/) is kept only for existing integrations. New integrations must use v2.
Resources

Support

If you need help with your integration, contact your EZ PIN account manager or email [email protected]. To get an answer faster, include:

  • whether you use a sandbox or production key, and your Client ID (never share your secret key);
  • the endpoint, the time of the request (UTC) and the HTTP status;
  • the reference_code and order_id for order questions;
  • the full response body, including code and detail.