تاجر جني
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
API reference
Webhooks

API documentation

Everything you need to integrate with the Gini v2 Third-Party API.

Webhooks

Configure and manage your single outbound webhook endpoint — the HTTPS URL Gini POSTs real-time product and order events to. The four management endpoints below are HMAC-signed like the rest of the API. The deliveries Gini sends you are not signed: the Authorization header you configure here is what proves they came from Gini. The stored auth_value is never returned, and actions made through the Third-Party API itself never fire events.

Get webhook config

Returns the merchant's current webhook configuration, or a null data payload when no webhook is configured. The stored auth_value credential is deliberately never included.

GET /api/v2.0/third-party/webhook
Signed
Response
{
    "data": {
        "url": "https://your-server.com/webhook",
        "auth_type": "bearer",
        "created_at": "2026-02-10T14:00:00+03:00",
        "updated_at": "2026-02-10T14:00:00+03:00"
    }
}
Notes
  • When no webhook is configured the endpoint still returns HTTP 200 with the body {'data': null}.
  • auth_value is never exposed — the resource returns only url, auth_type, created_at, and updated_at.
  • auth_type is one of: basic, bearer, none.
  • Timestamps are ISO-8601 strings in the Asia/Baghdad offset (+03:00).
Update webhook config

Creates or overwrites the merchant's webhook configuration. There is exactly one webhook per merchant (upserted on client_id), so calling this again replaces the existing config in place.

PUT /api/v2.0/third-party/webhook
Signed
Body parameters
Field Type Required Description
url string Required The endpoint Gini will POST events to. Must be a valid HTTPS URL (url:https) and pass the SSRF safety check: no localhost aliases, no raw-IP hosts (must be a domain name), and must not resolve to a private/reserved/loopback IP.
auth_type string Required Authorization scheme Gini uses when calling your endpoint. One of: basic, bearer, none.
auth_value string Optional The raw credential Gini sends in the Authorization header — a token for bearer, or username:password for basic. Gini base64-encodes the basic form for you, so do not pre-encode it. Required unless auth_type is none; nullable and max 1024 characters. Never returned by any endpoint.
Request body
{
    "url": "https://your-server.com/webhook",
    "auth_type": "bearer",
    "auth_value": "your-secret-token"
}
Response
{
    "data": {
        "url": "https://your-server.com/webhook",
        "auth_type": "bearer",
        "message": "Webhook configuration updated successfully."
    }
}
Notes
  • auth_value is required whenever auth_type is basic or bearer (required_unless:auth_type,none); omit it or send null only when auth_type is none.
  • auth_type header behavior: bearer sends 'Authorization: Bearer {auth_value}' verbatim; basic sends 'Authorization: Basic {base64(auth_value)}', encoding it for you; none sends no Authorization header.
  • The URL SSRF guard is enforced by the SafeWebhookUrl rule at save time and re-checked at fire/test time, since DNS can be repointed to an internal address after saving.
  • Non-HTTPS URLs, raw-IP hosts, localhost aliases, and domains resolving to private/reserved IPs are rejected with a 422 validation error.
  • The update response is a flat data object (url, auth_type, message) and does not include created_at/updated_at — use GET /webhook to read those.
Delete webhook config

Removes the merchant's webhook configuration. Returns 404 when there is nothing to delete.

DELETE /api/v2.0/third-party/webhook
Signed
Response
{
    "data": {
        "message": "Webhook configuration removed."
    }
}
Notes
  • Returns HTTP 404 with {'message': 'No webhook configured.'} when no webhook exists to delete.
  • After deletion, Gini stops sending event payloads until a new webhook is configured via PUT /webhook.
Test webhook

Synchronously fires a synthetic 'test' event at the configured webhook URL and reports the raw HTTP outcome. Unlike queued event deliveries this never retries — it returns the immediate result so you can verify your endpoint.

POST /api/v2.0/third-party/webhook/test
Signed
Response
{
    "data": {
        "success": true,
        "status_code": 200,
        "message": "Test webhook sent successfully."
    }
}
Notes
  • Returns HTTP 422 with {'message': 'No webhook configured. Configure a webhook first.'} when no webhook is set.
  • If the endpoint cannot be reached (unsafe/unresolvable URL or connection failure) it returns HTTP 422 with {'data': {'success': false, 'message': 'Failed to reach webhook endpoint: ...'}}.
  • When the endpoint responds but with a non-2xx status, success is false, status_code carries the real HTTP status, and message is 'Webhook endpoint returned an error.'
  • SSRF safety is re-validated at test time and the request is pinned to the vetted resolved IP (CURLOPT_RESOLVE), mirroring the real delivery path.
  • The test payload delivered to your endpoint is: {'event': 'test', 'message': 'This is a test webhook from Gini API v2.', 'timestamp': ISO-8601 string, 'client_id': merchant id}.
  • Your endpoint should respond with 200 OK within about 10 seconds.
Event payloads

Every event is a POST of a JSON object carrying an event key plus the subject of the event. These are the six events Gini sends.

Event Fires when Body
order_created A customer places an order with you. { event, order, user }
order_updated An order of yours changes status. { event, order }
product_created One of your products is created. { event, product }
product_updated One of your products is edited. { event, product }
product_deleted One of your products is deleted. { event, product }
product_bulk_delete_finished A bulk delete you sent to POST /products/bulk-delete has finished. { event, bulk_delete }
order_created
{
    "event": "order_created",
    "order": {
        "id": 5021,
        "number": "5021",
        "status": "pending",
        "client": {
            "id": 42
        },
        "items": []
    },
    "user": {
        "id": 918,
        "name": "Ahmed",
        "phone": "07700000000",
        "is_verified": 1
    }
}
order_updated
{
    "event": "order_updated",
    "order": {
        "id": 5021,
        "number": "5021",
        "status": "delivered",
        "client": {
            "id": 42
        },
        "items": []
    }
}
product_created
{
    "event": "product_created",
    "product": {
        "id": 771,
        "name": "My Product",
        "sku": "SKU-771",
        "price": 15000,
        "final_price": 13500,
        "quantity": 10,
        "category": {
            "id": 3,
            "name": "Electronics"
        },
        "discountable": {
            "percentage": 10,
            "code": "EID10"
        }
    }
}
Reading the payload
  • The order object is the same shape you get from GET /orders/{id}, plus a client block describing your own merchant account. Use that endpoint as the field reference rather than reproducing it here.
  • user is sent only on order_created, never on order_updated. Its phone is the order address contact phone, which can differ from the customer account phone.
  • The product object mirrors the product resource, so product_created, product_updated and product_deleted all carry the same fields.
  • product_deleted is built from a snapshot taken before the row is removed, so the payload is still complete.
  • bulk_delete carries the id POST /products/bulk-delete answered with, the deleted count and the SKUs that were not deleted. See Bulk delete products for the full shape and the reasons.
  • Treat both objects as append-only: new keys can appear without notice, so match on the keys you need instead of asserting an exact shape.
Delivery, retries and suppression
  • Delivery is a POST with a 10 second timeout and redirects disabled. Respond 2xx as soon as you have queued the work — do not process synchronously.
  • The Authorization header follows your webhook config: bearer sends Bearer {auth_value} verbatim, basic sends Basic {base64(auth_value)}, none sends no header at all.
  • Failures retry up to 3 times with a 30s, 120s, then 600s backoff. Connection errors and 5xx responses are retried.
  • A 4xx response is treated as your misconfiguration (bad auth, wrong route) and is not retried — it is logged once and dropped.
  • Each attempt connects to the IP that passed the SSRF check, pinned via CURLOPT_RESOLVE, so a host that later repoints to a private address cannot be reached.
  • Writes you make through this Third-Party API never fire events back at you. Only changes originating elsewhere — the dashboard, a customer action, a delivery callback — reach your endpoint.
  • The one exception is product_bulk_delete_finished: POST /products/bulk-delete answers before anything is deleted, so this event is how you learn the result.
  • Events are not ordered or deduplicated. Make your handler idempotent and key it on the order or product id, or on the bulk_delete id.
Verifying a delivery came from Gini

Traffic runs in both directions, and the two directions are secured differently. Requests you send Gini are signed. Deliveries Gini sends you are not — they are authenticated by the credential you configured above, and nothing else.

Direction What proves the request is genuine
Your request to Gini An HMAC-SHA256 signature over METHOD + PATH + BODY + TIMESTAMP, sent as X-SIGNATURE alongside X-API-KEY and X-TIMESTAMP. See Authentication.
Gini delivery to your endpoint The Authorization header built from your auth_type and auth_value. There is no signature header and no timestamp header on a delivery.
Receiving endpoint — PHP
// The token you saved as auth_value, with auth_type set to bearer.
$expected = 'Bearer ' . config('services.gini.webhook_token');

// Constant-time compare — a plain === leaks the token one byte at a time.
if (! hash_equals($expected, (string) $request->header('Authorization'))) {
    abort(401);
}

// Answer fast, process later: Gini gives up after 10 seconds.
ProcessGiniWebhook::dispatch($request->json()->all());

return response()->noContent();
Rules for your receiving endpoint
  • A delivery carries no X-SIGNATURE and no X-TIMESTAMP. Do not port your outbound signing code into your receiver to check one — it would reject every delivery Gini sends.
  • Prefer bearer with a long random secret you generate yourself. That token is the whole of your endpoint security, so treat it exactly as you would a password.
  • Compare the Authorization header in constant time — hash_equals in PHP, crypto.timingSafeEqual in Node, hmac.compare_digest in Python. A plain equality check leaks the token one byte at a time to anyone who can measure your response.
  • Reject anything without the exact header you configured. Return 401 and do no work.
  • auth_type none leaves your endpoint open to anyone who learns the URL. Use it only where the URL is unguessable and the payload is not sensitive.
  • Treat a payload as a notification, not as authority. Before anything that moves money or stock, re-read the order or product through the API and act on what the API returns.
  • If you have integrated with Gini elsewhere and remember an X-SIGNATURE on incoming webhooks, that belongs to a different product — the application webhook. It is never sent to a Third-Party API merchant endpoint.