For developers and IT teams

Your system orders by itself

Connect your inventory system or ERP to Tawreed Gate once: it sends stock levels, and when an item drops to the reorder point you set, a request for the quantity you chose reaches us, priced at your company’s prices. Your system hears every step by webhook (priced, confirmed, shipped, delivered, invoiced) and can accept priced quotations itself.

How it works

  1. 1Your systemSends stock levels nightly or on every stock movement
  2. 2Reorder rulesA reorder point and quantity per item
  3. 3The requestReaches our team as a quotation at your company prices
  4. 4Back to your systemWebhooks when it is priced, confirmed, shipped, delivered and invoiced

Start in three steps

  1. 1Create a keyIn your account: Integrations → Keys. Pick the scopes, and whether the key’s requests go straight to Tawreed Gate or wait for your company admin’s approval. The key is shown once, so keep it in your secrets vault.
  2. 2Set reorder rulesPer item: its code (sku) and unit, the reorder point (min_qty) and the quantity we request (reorder_qty). In the account or with POST /reorder-rules.
  3. 3Send stock levelsCall PUT /stock-levels on a schedule. Whatever reached its reorder point goes out together in one request, and the response tells you what was accepted and what was requested.

Report stock levels

curl -X PUT https://tawreed-gate-nine.vercel.app/api/v1/stock-levels \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: stock-2026-10-08T03:00" \
  -d '{
    "levels": [
      { "sku": "BUSD039", "unit": "CTN", "qty": 4 },
      { "sku": "BUSH118", "qty": 40 },
      { "sku": "XYZ-404", "qty": 6 }
    ]
  }'

The response

{
  "accepted": 2,
  "rejected": [ { "index": 2, "error": "unknown_product" } ],
  "triggered": 1,
  "request": { "id": "6b1f0c3e-…", "status": "submitting", "ref": "WEB-2610-4F7Q2K" }
}
acceptedHow many levels we recorded
rejectedLines we could not match, with their index: unknown_product, unknown_unit or invalid_qty
triggeredHow many items were at their reorder point and requested
requestThe request they went out in (null when nothing was due)

Reorder logic

When the quantity your system reports is at or below an item’s reorder point, the item joins a request for the quantity set in its rule. Every item one report triggers goes out together in one request.

Example

On hand, as your system reports
4 cartons
Reorder point
30, so it is due
Reorder quantity
36 cartons
The request
WEB-2610-4F7Q2K · 36 cartons
  • No double orders: while a rule’s request is still open (waiting for approval or being sent), or for 24 hours after it reached us, reporting the same low level again does not trigger another.
  • A key that sends directly puts the request straight through to our team; any other waits for an admin in your account to approve it (event request.awaiting_approval).
  • Our team prices the quotation at your company’s agreed prices and sends it (event quotation.priced). Accept it in the account, or from your system with POST /quotations/{number}/accept on a key that sends directly.
  • Units: send the unit’s code (CTN), its English or Arabic name, or uom_id, or leave it out to use the item’s base sale unit.
curl -X POST https://tawreed-gate-nine.vercel.app/api/v1/reorder-rules \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rule-BUSD039" \
  -d '{ "sku": "BUSD039", "unit": "CTN", "min_qty": 30, "reorder_qty": 36 }'

Authentication and scopes

Send the key on every request in the Authorization: Bearer header. Each key has the scopes you pick when you create it, and only ever sees your own company’s data.

Authorization: Bearer tg_live_…

Scopes

catalog:readSearch products and their units
orders:readRead requests, quotations, orders, deliveries and addresses
requests:writeSend requests and accept priced quotations
stock:writeReport stock levels and manage reorder rules
invoices:readRead invoices and what is due
webhooks:manageAdd and remove webhook endpoints

Endpoints

https://tawreed-gate-nine.vercel.app/api/v1

GET/me—The company, the key’s scopes and where its requests go
GET/productscatalog:readProducts with their units (q, page, limit)
GET/products/{sku}catalog:readOne product by code or id
POST/requestsrequests:writeSend a request (lines of sku or product_id with unit and qty)
GET/requests/{id}orders:readA request’s status and its quotation
GET/quotationsorders:readQuotations with Tawreed Gate and priced ones
POST/quotations/{number}/acceptrequests:writeAccept a priced quotation (keys that send directly)
GET/ordersorders:readConfirmed orders
GET/orders/{number}orders:readOne quotation or order with its lines, timeline and deliveries
GET/invoicesinvoices:readInvoices with their status and balance
GET/addressesorders:readDelivery addresses (address_id for requests)
PUT/stock-levelsstock:writeReport stock; we request what reached its reorder point
GET/reorder-rulesstock:writeAll reorder rules
POST/reorder-rulesstock:writeCreate or change an item’s rule
DELETE/reorder-rules/{id}stock:writeDelete a rule
GET/webhookswebhooks:manageYour webhook endpoints
POST/webhookswebhooks:manageAdd an endpoint (its secret is shown once)
DELETE/webhooks/{id}webhooks:manageRemove an endpoint

Send a request

curl -X POST https://tawreed-gate-nine.vercel.app/api/v1/requests \
  -H "Authorization: Bearer $TAWREED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: po-77812" \
  -d '{
    "po_ref": "PO-77812",
    "notes": "Deliver to gate 2",
    "lines": [
      { "sku": "BUSH118", "unit": "EA", "qty": 24 },
      { "sku": "BUSD039", "qty": 10 }
    ]
  }'

→ 201 { "id": "…", "status": "submitting", "website_reference": "WEB-2610-4F7Q2K" }

Webhooks

Add an endpoint that receives events in your account (Integrations → Webhooks) or with POST /webhooks. Each event is POSTed as JSON, signed with the endpoint’s secret. A failed delivery is retried up to 7 more times over about two days; after 15 failures in a row the endpoint is paused and your company admins are emailed. Endpoints must be public https addresses.

Events

  • request.submitted
  • request.awaiting_approval
  • quotation.priced
  • order.confirmed
  • order.cancelled
  • delivery.out
  • delivery.done
  • invoice.issued
  • reorder.triggered

Payload

POST https://erp.example.com/hooks/tawreed
X-Tawreed-Event: quotation.priced
X-Tawreed-Delivery: 3f6c0a3e-…
X-Tawreed-Signature: t=1791450000,v1=5b1f0c…

{
  "id": "evt_8d3c1f9a-…",
  "type": "quotation.priced",
  "created": "2026-10-08T09:12:04.120Z",
  "data": {
    "order": { "id": 4521, "number": "S00142", "website_reference": "WEB-2610-4F7Q2K",
               "amount_total": 880.79, "currency": "SAR" }
  }
}

Verify the signature

The X-Tawreed-Signature header carries t (when it was sent) and v1 (an HMAC-SHA256 of "t.raw_body" with the endpoint secret). Compute it yourself, compare in constant time, and reject requests older than 5 minutes. X-Tawreed-Delivery stays the same for an event across retries: use it to drop duplicates.

Node.js
import crypto from 'node:crypto';

// rawBody: the request body exactly as received (not re-serialised JSON)
export function verifyTawreed(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && expected.length === parts.v1?.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Python
import hashlib, hmac, time

def verify_tawreed(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])
PHP
function tawreed_verify(string $raw, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $p);
    $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
    return abs(time() - (int) $p['t']) < 300 && hash_equals($expected, $p['v1']);
}

Recipes for common systems

They all do the same thing: read item stock from your system and send it to PUT /stock-levels on a schedule. Your system sends Tawreed Gate’s product code (sku) for each item; most systems keep it as the vendor’s item code on the item.

Odoo

A small custom module with a scheduled action (ir.cron) every night. It reads Tawreed Gate’s code from the vendor product code on the item (Purchase → Vendors).

# models/tawreed_sync.py
import requests
from odoo import api, models

TAWREED = "https://tawreed-gate-nine.vercel.app/api/v1"

class TawreedSync(models.AbstractModel):
    _name = "tawreed.sync"
    _description = "Report stock levels to Tawreed Gate"

    @api.model
    def push_stock(self):
        params = self.env["ir.config_parameter"].sudo()
        key = params.get_param("tawreed.api_key")
        vendor = int(params.get_param("tawreed.partner_id"))  # Tawreed Gate as a vendor
        lines = self.env["product.supplierinfo"].search(
            [("partner_id", "=", vendor), ("product_code", "!=", False)])
        levels = []
        for s in lines:
            product = s.product_id or s.product_tmpl_id.product_variant_id
            levels.append({"sku": s.product_code, "qty": max(0, product.qty_available)})
        for i in range(0, len(levels), 2000):
            r = requests.put(f"{TAWREED}/stock-levels",
                             json={"levels": levels[i:i + 2000]},
                             headers={"Authorization": f"Bearer {key}"}, timeout=30)
            r.raise_for_status()
SAP Business One

A scheduled script (Task Scheduler or cron) that reads stock from the Service Layer. Keep Tawreed Gate’s code in a user-defined field on the item (U_TawreedSKU).

import os, requests

SL = "https://your-b1-server:50000/b1s/v1"
TAWREED = "https://tawreed-gate-nine.vercel.app/api/v1"

sl = requests.Session()
sl.post(f"{SL}/Login", json={"CompanyDB": os.environ["B1_DB"],
        "UserName": os.environ["B1_USER"], "Password": os.environ["B1_PASS"]}).raise_for_status()

levels = []
url = f"{SL}/Items?$select=ItemCode,QuantityOnStock,U_TawreedSKU&$filter=U_TawreedSKU ne null"
while url:
    page = sl.get(url).json()
    levels += [{"sku": it["U_TawreedSKU"], "qty": max(0, it["QuantityOnStock"])} for it in page["value"]]
    nxt = page.get("odata.nextLink") or page.get("@odata.nextLink")
    url = f"{SL}/{nxt}" if nxt else None

for i in range(0, len(levels), 2000):
    requests.put(f"{TAWREED}/stock-levels", json={"levels": levels[i:i + 2000]},
                 headers={"Authorization": f"Bearer {os.environ['TAWREED_KEY']}"},
                 timeout=30).raise_for_status()
Dynamics 365 Business Central

No code, with Power Automate (the HTTP action needs a premium licence). Use a field that holds the item’s Tawreed Gate code.

1. Trigger      Recurrence: every day at 03:00
2. Action       Business Central → List items (V3)
                Filter: type eq 'Inventory'
3. Action       Data Operation → Select
                From: value
                Map:  sku = <the field holding the Tawreed Gate code>
                      qty = item()?['inventory']
4. Action       HTTP
                Method:  PUT
                URI:     https://tawreed-gate-nine.vercel.app/api/v1/stock-levels
                Headers: Authorization = Bearer <your key>
                         Content-Type  = application/json
                Body:    { "levels": @{body('Select')} }
NetSuite

A scheduled SuiteScript 2.1; it reads Tawreed Gate’s code from the vendor code (Vendor Name/Code). Keep the key in a script parameter.

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/https', 'N/search', 'N/runtime', 'N/log'], (https, search, runtime, log) => ({
  execute() {
    const key = runtime.getCurrentScript().getParameter({ name: 'custscript_tawreed_key' });
    const levels = [];
    search.create({
      type: search.Type.INVENTORY_ITEM,
      filters: [['isinactive', 'is', 'F'], 'AND', ['vendorname', 'isnotempty', '']],
      columns: ['vendorname', 'quantityonhand'],
    }).run().each((r) => {
      levels.push({ sku: r.getValue('vendorname'), qty: Math.max(0, Number(r.getValue('quantityonhand')) || 0) });
      return levels.length < 2000;
    });
    const res = https.put({
      url: 'https://tawreed-gate-nine.vercel.app/api/v1/stock-levels',
      body: JSON.stringify({ levels }),
      headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
    });
    log.audit('Tawreed Gate', `${res.code} ${res.body.slice(0, 300)}`);
  },
}));
Zoho Inventory

A scheduled Deluge custom function, with a custom field that holds the Tawreed Gate code.

items = zoho.inventory.getRecords("items", ORGANIZATION_ID);
levels = List();
for each it in items.get("items")
{
    sku = it.get("cf_tawreed_sku");
    if(sku != null && sku != "")
    {
        levels.add({"sku": sku, "qty": it.get("stock_on_hand")});
    }
}
resp = invokeurl
[
    url: "https://tawreed-gate-nine.vercel.app/api/v1/stock-levels"
    type: PUT
    parameters: {"levels": levels}.toString()
    headers: {"Authorization": "Bearer " + TAWREED_KEY, "Content-Type": "application/json"}
];
info resp;
n8n · Make · Zapier

A schedule, then an HTTP node, and a webhook node to receive our events.

Schedule trigger      every night at 03:00
Your source           Google Sheets / database / ERP node → rows with the Tawreed code and quantity
HTTP Request          PUT https://tawreed-gate-nine.vercel.app/api/v1/stock-levels
                      Header auth: Authorization = Bearer <your key>
                      JSON body:   { "levels": [ { "sku": "{{code}}", "qty": {{qty}} } ] }
Webhook trigger       paste its URL in your account → Integrations → Webhooks
                      (verify X-Tawreed-Signature before acting on it)

Errors and limits

Every error comes back in one shape, with a stable code to build your logic on.

{ "error": { "code": "forbidden", "message": "This key lacks the stock:write scope" } }
400bad_jsonThe body is not valid JSON
400idempotency_key_requiredA POST without an Idempotency-Key header
401unauthorizedThe key is missing, invalid or revoked
403forbiddenThe key lacks the scope (or may not accept quotations)
404not_foundNo such endpoint, request, order or product
409not_readyThe company account is still being set up, or the quotation cannot be accepted as it stands
413too_largeThe body is larger than 256 KB
422invalid · invalid_linesMissing or invalid data (see details)
422idempotency_key_reusedThe Idempotency-Key was used with a different request
429rate_limitedMore than 120 calls a minute on this key
503disabledThe API is switched off for now
500server_errorOur fault; retry with the same Idempotency-Key

Idempotency keys

Every POST needs an Idempotency-Key header, and PUT /stock-levels takes one too. If you repeat a request with the same key (after a network drop, say), we return the first response with Idempotent-Replayed: true and never send anything twice. Keys are kept for 7 days.

Limits

120 calls a minute per key (X-RateLimit-Limit and X-RateLimit-Remaining headers), 2,000 levels per stock report, 300 lines per request, and 256 KB per body.

Need a hand with the connection? Our team sets up a session with you and tests it together. WhatsApp