API v1

Connect your site with the API

An order placed on your site appears on the seller's Gayidvebi.ge board (the "საიტიდან" column) with one request. When the seller moves the order on, your site learns the new status by webhook. You can also read products and stock.

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? Contact us