تاجر جني
Which API?
Merchant API For merchants: manage your own catalogue, orders and finances through the v2 Third-Party API. Application API For partner apps: browse Gini merchants and products, send customers to checkout and follow their orders.

New: Application API docs

Pick the API you are reading here — the Merchant API for your own store, the Application API for partner apps.

Merchant dashboard
Getting started
Authentication

Application API

For partner applications: browse merchants and products, hand customers a checkout link, and follow the orders it creates.

Authentication

Every request carries your app_id and an HMAC-SHA256 signature computed from the request itself — method, path, canonical query, body and timestamp — keyed with your app_secret.

This signing scheme is not the merchant API's: here the canonical query string is part of what you sign. A merchant signer reused as-is fails with 401 Invalid signature on any request that carries query parameters.
Required headers

Every request must carry three headers:

Header Description
X-API-KEY Your app_id.
X-TIMESTAMP Current Unix time in seconds (UTC).
X-SIGNATURE Hex-encoded HMAC-SHA256 of the string to sign (below).
String to sign
String to sign
<METHOD><PATH><CANONICAL_QUERY><BODY><TIMESTAMP>

X-SIGNATURE = hex( HMAC_SHA256(app_secret, stringToSign) )
Part What it is
METHOD Uppercase HTTP method (GET, POST).
PATH Request path including the /api/... prefix, e.g. /api/third-party/v1/applications/products.
CANONICAL_QUERY The canonical query string prefixed with ?, or the empty string when the request has no query parameters. See Canonical query string below.
BODY The raw request body, byte for byte. Empty for GET. For the two POST endpoints, sign exactly the bytes you send — re-serialising the JSON after signing breaks the signature.
TIMESTAMP The exact value sent in X-TIMESTAMP.
Canonical query string

The query string you sign must be canonical. The URL you actually request does not need to be — the server normalises the incoming query before verifying.

Canonical form, in this order
  • Sort parameters by key, byte-wise ascending (max_price before page).
  • Percent-encode keys and values per RFC 3986: spaces become %20 (never +), and ! ' ( ) * are encoded too.
  • Join as key=value pairs with &.
  • Arrays are indexed the way PHP's http_build_query does it: categories[]=1&categories[]=2 canonicalises to categories%5B0%5D=1&categories%5B1%5D=2.

In PHP this is exactly:

PHP
parse_str($rawQuery, $params);
ksort($params);
$canonical = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
Legacy signatures
Signatures computed without the query string (<METHOD><PATH><BODY><TIMESTAMP>) are still accepted during a transition window. That scheme leaves every query parameter unauthenticated and will be switched off — sign the canonical query.
Constraints
Condition Response
Any of the three headers missing 401 {"message": "Missing authentication headers"}
Unknown or deactivated app_id 401 {"message": "Invalid API key"}
|now − X-TIMESTAMP| > 300 seconds 401 {"message": "Request expired"}
Signature does not match 401 {"message": "Invalid signature"}
Reference signer (PHP)
PHP
$method    = 'GET';
$path      = '/api/third-party/v1/applications/products';
$query     = 'page_size=20&page=1';
$body      = '';
$timestamp = (string) time();

parse_str($query, $params);
ksort($params);
$canonicalQuery = http_build_query($params, '', '&', PHP_QUERY_RFC3986);

$stringToSign = $method . $path
    . ($canonicalQuery === '' ? '' : '?' . $canonicalQuery)
    . $body . $timestamp;
$signature = hash_hmac('sha256', $stringToSign, $appSecret);

$headers = [
    'X-API-KEY: '   . $appId,
    'X-TIMESTAMP: ' . $timestamp,
    'X-SIGNATURE: ' . $signature,
    'Accept: application/json',
];
Reference signer (Node.js)
Node.js
import crypto from 'crypto';

// encodeURIComponent leaves ! ' ( ) * unencoded; PHP's rawurlencode does not.
// Without this, any value containing them fails with "Invalid signature".
const rfc3986 = (s) =>
  encodeURIComponent(String(s)).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());

// params: flat values or arrays, e.g. { page: '1', categories: ['3', '5'] }
function canonicalQuery(params) {
  const pairs = [];
  for (const key of Object.keys(params).sort()) {
    const value = params[key];
    if (Array.isArray(value)) {
      // mirror PHP: categories[] -> categories[0], categories[1]
      value.forEach((v, i) => pairs.push(`${rfc3986(`${key}[${i}]`)}=${rfc3986(v)}`));
    } else {
      pairs.push(`${rfc3986(key)}=${rfc3986(value)}`);
    }
  }
  return pairs.join('&');
}

const method    = 'GET';
const path      = '/api/third-party/v1/applications/products';
const body      = '';                       // for POST: the exact JSON string you send
const timestamp = Math.floor(Date.now() / 1000).toString();
const query     = canonicalQuery({ page_size: '20', page: '1' });

const stringToSign = method + path + (query ? '?' + query : '') + body + timestamp;
const signature = crypto.createHmac('sha256', appSecret).update(stringToSign).digest('hex');
End-to-end cURL example
cURL
APP_ID="<your app_id>"
APP_SECRET="<your app_secret>"
HOST="https://api.gini.iq"

METHOD="GET"
PATH="/api/third-party/v1/applications/categories"
QUERY="page=1&page_size=10"        # already canonical: sorted, RFC 3986
BODY=""
TS=$(date +%s)

STRING_TO_SIGN="${METHOD}${PATH}?${QUERY}${BODY}${TS}"
SIG=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$APP_SECRET" -r | cut -d' ' -f1)

curl -sS "${HOST}${PATH}?${QUERY}" \
  -H "X-API-KEY: $APP_ID" \
  -H "X-TIMESTAMP: $TS" \
  -H "X-SIGNATURE: $SIG" \
  -H 'Accept: application/json'
401 Invalid signature — the usual causes
  • The signed query is not canonical: keys unsorted, + for spaces, !'()* left unencoded, or a stray ? (omit it entirely when there are no parameters).
  • For a POST, the body you signed is not byte-identical to the body you sent.
  • X-TIMESTAMP differs between what you signed and what you sent.
  • Clock skew above 5 minutes.
  • A different app_secret from the one bound to your app_id.