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.
- Base URL
https://www.gayidvebi.ge/api/v1- Format
- JSON, UTF-8
- OpenAPI
- /api/v1/openapi.json
Quickstart
- The seller creates a key in Settings → Integrations → Custom site (API).
- 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"- 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.
| Field | Type | Description |
|---|---|---|
| id* | string | Your order id (1–64 chars: letters, digits, . _ : -). The same id is never saved twice. |
| number | string | The order number the customer sees (defaults to id). |
| created_at | ISO 8601 | When it was placed (defaults to now). |
| customer.name | string | Full name (or customer.first_name + customer.last_name). |
| customer.phone* | string | Phone in any format: +995 555 12 34 56, 555123456… |
| customer.email | string | Added to the comment if the seller turned it on. |
| customer.personal_id | string | Personal ID number (for the waybill). |
| shipping.city / shipping.address | string | Delivery city and address. |
| shipping.fee | number | Delivery fee in GEL. |
| shipping.paid_by | "customer" | "shop" | Who pays the courier. If omitted: shop when paid online, customer for cash on delivery. |
| payment.method | string | Payment code: cod / cash (on delivery), bank_transfer, or any code of yours — the seller maps it in Settings. |
| payment.title / payment.paid | string / boolean | The method's name and whether it is paid. |
| items[]* | array | 1–200 items. |
| items[].name* | string | Product name without the variation ("T-shirt", not "T-shirt - L"). Matched to the CRM product by name, created if missing. |
| items[].quantity* | integer | Quantity (1 or more). |
| items[].price* | number | Unit price the customer pays. |
| items[].product_id / variant_id / sku | string | Your ids. After the first order we remember which CRM product they are — even if the name changes later. |
| items[].options | object | Variation: {"Size": "L"}. Matched to the CRM's variations (case-insensitive). |
| discount | number | Order-level discount in GEL — spread over the items in proportion. |
| total | number | Order total. If it doesn't match, the seller sees a warning. |
| currency / note / admin_url | string | Currency (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"
}
}| stage | Meaning |
|---|---|
| received | Received, waiting for the seller to confirm ("საიტიდან"). |
| new | Confirmed, being prepared. |
| in_progress | Any other column of the seller (e.g. "shipped"). label has its name. |
| completed | Delivered / completed. |
| cancelled | Cancelled (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.
| error | HTTP | retry | Meaning |
|---|---|---|---|
| missing_api_key / invalid_api_key / unknown_key | 401 | false | The key is missing, malformed or unknown. |
| key_revoked | 401 | false | The key was revoked or replaced — create a new one in Settings. |
| browser_requests_not_allowed | 403 | false | The request came from a browser. Call from your server. |
| plan_not_allowed | 403 | false | The API needs the Pro plan or above. |
| key_in_url | 400 | false | The key is in the URL. Move it to the Authorization header. |
| invalid_json / invalid_idempotency_key / invalid_query / invalid_id | 400 | false | The request is malformed. |
| validation_failed / invalid_phone | 422 | false | Fields are invalid — see field and errors. |
| order_not_found | 404 | false | No order with this id was received from your site. |
| payload_too_large | 413 | false | The body is over 256 KB. |
| rate_limited | 429 | true | Over 120 requests a minute. Wait Retry-After seconds. |
| server_error / not_configured | 500 / 503 | true | Our side — try again later (the same id is safe). |
Questions? Contact us