API v1

Подключение сайта через API

Заказ с вашего сайта одним запросом появляется на доске Gayidvebi.ge (колонка «საიტიდან»). Когда продавец меняет статус, сайт узнаёт об этом через webhook. Также можно читать товары и остатки. Документация ниже — на английском.

Call the API only from your site's server (PHP, Node, Python…) — never from browser JavaScript. The key is a secret: keep it in an environment variable (e.g. GAYIDVEBI_API_KEY).
Base URL
https://www.gayidvebi.ge/api/v1
Format
JSON, UTF-8
OpenAPI
/api/v1/openapi.json

Quickstart

  1. The seller creates a key in Settings → Integrations → Custom site (API).
  2. Check the key — a 200 answer means it works, and Settings shows "connected":
curl https://www.gayidvebi.ge/api/v1/ping \
  -H "Authorization: Bearer gyd_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  1. After checkout, send the order:
<?php
// $orderId, $customer, $cart come from your checkout
$order = [
    'id'       => (string) $orderId,
    'customer' => ['name' => $customer['name'], 'phone' => $customer['phone']],
    'shipping' => ['city' => $customer['city'], 'address' => $customer['address'], 'fee' => 6],
    'payment'  => ['method' => 'cod', 'paid' => false],
    'items'    => array_map(fn ($line) => [
        'product_id' => (string) $line['product_id'],
        'name'       => $line['name'],
        'options'    => $line['options'] ?? [],      // ['ზომა' => 'L']
        'quantity'   => (int) $line['qty'],
        'price'      => (float) $line['unit_price'],
    ], $cart),
];

$ch = curl_init('https://www.gayidvebi.ge/api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('GAYIDVEBI_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($order, JSON_UNESCAPED_UNICODE),
]);
$response = json_decode((string) curl_exec($ch), true);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 201 || $status === 200) {
    // Saved (or it was already saved). $response['order']['crm_id']
} elseif ($status === 0 || !empty($response['retry'])) {
    // No answer, 429 or 5xx: try again later with the same id — never saved twice
} else {
    error_log('Gayidvebi refused the order: ' . json_encode($response));
}

Authentication

Send the header Authorization: Bearer gyd_live_… with every request. A key belongs to one site.

  • Requests from a browser (with an Origin header) are refused — 403.
  • A key in the URL (?key=…) — 400.
  • Limit: 120 requests a minute per key. Over it — 429 with Retry-After.
  • Creating a new key stops the old one at once (401 key_revoked).
  • Use www: gayidvebi.ge (without www) redirects, and some HTTP clients don't repeat a POST after a redirect.

Create an order

POST/api/v1/orders

The order lands in the "საიტიდან" column — the seller hears a sound, then confirms it. Stock is deducted automatically.

FieldTypeDescription
id*stringYour order id (1–64 chars: letters, digits, . _ : -). The same id is never saved twice.
numberstringThe order number the customer sees (defaults to id).
created_atISO 8601When it was placed (defaults to now).
customer.namestringFull name (or customer.first_name + customer.last_name).
customer.phone*stringPhone in any format: +995 555 12 34 56, 555123456…
customer.emailstringAdded to the comment if the seller turned it on.
customer.personal_idstringPersonal ID number (for the waybill).
shipping.city / shipping.addressstringDelivery city and address.
shipping.feenumberDelivery fee in GEL.
shipping.paid_by"customer" | "shop"Who pays the courier. If omitted: shop when paid online, customer for cash on delivery.
payment.methodstringPayment code: cod / cash (on delivery), bank_transfer, or any code of yours — the seller maps it in Settings.
payment.title / payment.paidstring / booleanThe method's name and whether it is paid.
items[]*array1–200 items.
items[].name*stringProduct name without the variation ("T-shirt", not "T-shirt - L"). Matched to the CRM product by name, created if missing.
items[].quantity*integerQuantity (1 or more).
items[].price*numberUnit price the customer pays.
items[].product_id / variant_id / skustringYour ids. After the first order we remember which CRM product they are — even if the name changes later.
items[].optionsobjectVariation: {"Size": "L"}. Matched to the CRM's variations (case-insensitive).
discountnumberOrder-level discount in GEL — spread over the items in proportion.
totalnumberOrder total. If it doesn't match, the seller sees a warning.
currency / note / admin_urlstringCurrency (GEL), the customer's note, a link to the order in your admin.
{
  "id": "1001",
  "number": "A-1001",
  "created_at": "2026-10-15T13:30:00+04:00",
  "customer": {
    "name": "ნინო ბერიძე",
    "phone": "+995 555 12 34 56",
    "email": "nino@example.ge",
    "personal_id": "01001012345"
  },
  "shipping": {
    "city": "თბილისი",
    "address": "ჭავჭავაძის გამზ. 12, ბინა 4",
    "fee": 6,
    "paid_by": "customer"
  },
  "payment": { "method": "cod", "title": "ადგილზე გადახდა", "paid": false },
  "items": [
    {
      "product_id": "202",
      "variant_id": "203",
      "sku": "TS-L",
      "name": "მაისური",
      "options": { "ზომა": "L", "ფერი": "შავი" },
      "quantity": 2,
      "price": 35
    }
  ],
  "discount": 0,
  "total": 76,
  "currency": "GEL",
  "note": "დარეკეთ მისვლამდე",
  "admin_url": "https://shop.ge/admin/orders/1001"
}

Retries are safe

If there's no answer, or a 429/5xx (retry: true), send the same order again. The same id is never saved twice — you get 200 with result: "duplicate". You may also send an Idempotency-Key header (8–100 chars).

Validate without saving

POST /api/v1/orders?dry_run=1 checks every field and shows the CRM order it would create, without saving anything. Invalid fields come back like this:

HTTP/1.1 422 Unprocessable Entity
{
  "ok": false,
  "error": "validation_failed",
  "message": "Some fields are invalid. See `errors`.",
  "field": "customer.phone",
  "errors": [
    { "field": "customer.phone", "code": "required", "message": "customer.phone is required" },
    { "field": "items[0].quantity", "code": "invalid_value", "message": "items[0].quantity must be a whole number from 1 to 100000" }
  ],
  "retry": false,
  "server_time": 1760520601
}

Order status

GET/api/v1/orders/{id}

{id} is your order id. Returns the same order object as on create. For status changes, webhooks are better than polling.

curl https://www.gayidvebi.ge/api/v1/orders/1001 \
  -H "Authorization: Bearer gyd_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Cancel an order

POST/api/v1/orders/{id}/cancel

If the order is still in "საიტიდან", it is cancelled and the stock comes back. If the seller already confirmed it, a "⚠ cancelled on the site" note is added and the seller decides. The answer has the current status.

curl -X POST https://www.gayidvebi.ge/api/v1/orders/1001/cancel \
  -H "Authorization: Bearer gyd_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "customer" }'

Products & stock

GET/api/v1/products

The seller's products: price, variations and available stock (never cost). Parameters: limit (1–200, default 100), cursor (next_cursor of the previous answer), updated_since (ISO date — changed ones only). You may cache the answer for 60 seconds.

curl "https://www.gayidvebi.ge/api/v1/products?limit=100" \
  -H "Authorization: Bearer gyd_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Webhooks

The seller enters your https URL in Settings and gets a signing secret (whsec_…). Every time the status of an order received through the API changes, that URL gets a POST. The format is Standard Webhooks (standardwebhooks.com): use their libraries or the code below.

  • order.status_changed — the status changed (confirming it too).
  • order.deleted — the seller deleted the order.
  • test — the "Send test" button in Settings.
POST /your/webhook/url HTTP/1.1
Content-Type: application/json
webhook-id: msg_5b0c3a1e9d2f4c7b8a6e1d0c9b8a7f6e
webhook-timestamp: 1760520601
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
  "type": "order.status_changed",
  "timestamp": "2026-10-15T09:30:01.123Z",
  "data": {
    "id": "1001",
    "number": "A-1001",
    "crm_id": "5b0c…-…",
    "status":          { "id": "shipped", "label": "გაგზავნილი", "stage": "in_progress" },
    "previous_status": { "id": "pending", "label": "ახალი",      "stage": "new" },
    "changed_at": "2026-10-15T09:30:01.123Z"
  }
}
stageMeaning
receivedReceived, waiting for the seller to confirm ("საიტიდან").
newConfirmed, being prepared.
in_progressAny other column of the seller (e.g. "shipped"). label has its name.
completedDelivered / completed.
cancelledCancelled (or returned).

status.id and label are the seller's own columns and can change — program against stage.

Verify the signature

<?php
$secret = getenv('GAYIDVEBI_WEBHOOK_SECRET');           // whsec_...
$id     = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts     = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$body   = file_get_contents('php://input');              // the raw body, not re-encoded

$key      = base64_decode(substr($secret, strlen('whsec_')));
$expected = base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true));
$valid    = false;
foreach (explode(' ', $header) as $part) {
    [$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
    if ($version === 'v1' && hash_equals($expected, $signature)) $valid = true;
}
if (!$valid || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// Retries reuse the same webhook-id: skip an id you already handled
if ($event['type'] === 'order.status_changed') {
    $orderId = $event['data']['id'];                       // your order id
    $stage   = $event['data']['status']['stage'];          // received|new|in_progress|completed|cancelled
    // … update the order on your site
}
http_response_code(200);
  • Answer 2xx within 10 seconds. On any other answer or a timeout we retry after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h.
  • Retries keep the same webhook-id — skip an id you already handled.
  • Order isn't guaranteed: compare data.changed_at and skip older ones, or read GET /orders/{id}.
  • Redirects (3xx) are not followed; the URL must be public https.

Errors

Errors look like {"ok": false, "error": "<code>", "message": "…", "retry": false}. retry: true means trying again later can help.

errorHTTPretryMeaning
missing_api_key / invalid_api_key / unknown_key401falseThe key is missing, malformed or unknown.
key_revoked401falseThe key was revoked or replaced — create a new one in Settings.
browser_requests_not_allowed403falseThe request came from a browser. Call from your server.
plan_not_allowed403falseThe API needs the Pro plan or above.
key_in_url400falseThe key is in the URL. Move it to the Authorization header.
invalid_json / invalid_idempotency_key / invalid_query / invalid_id400falseThe request is malformed.
validation_failed / invalid_phone422falseFields are invalid — see field and errors.
order_not_found404falseNo order with this id was received from your site.
payload_too_large413falseThe body is over 256 KB.
rate_limited429trueOver 120 requests a minute. Wait Retry-After seconds.
server_error / not_configured500 / 503trueOur side — try again later (the same id is safe).

Questions? Напишите нам