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
- 1Your systemSends stock levels nightly or on every stock movement
- 2Reorder rulesA reorder point and quantity per item
- 3The requestReaches our team as a quotation at your company prices
- 4Back to your systemWebhooks when it is priced, confirmed, shipped, delivered and invoiced
Start in three steps
- 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.
- 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.
- 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" }
}accepted | How many levels we recorded |
rejected | Lines we could not match, with their index: unknown_product, unknown_unit or invalid_qty |
triggered | How many items were at their reorder point and requested |
request | The 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:read | Search products and their units |
orders:read | Read requests, quotations, orders, deliveries and addresses |
requests:write | Send requests and accept priced quotations |
stock:write | Report stock levels and manage reorder rules |
invoices:read | Read invoices and what is due |
webhooks:manage | Add 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 | /products | catalog:read | Products with their units (q, page, limit) |
| GET | /products/{sku} | catalog:read | One product by code or id |
| POST | /requests | requests:write | Send a request (lines of sku or product_id with unit and qty) |
| GET | /requests/{id} | orders:read | A request’s status and its quotation |
| GET | /quotations | orders:read | Quotations with Tawreed Gate and priced ones |
| POST | /quotations/{number}/accept | requests:write | Accept a priced quotation (keys that send directly) |
| GET | /orders | orders:read | Confirmed orders |
| GET | /orders/{number} | orders:read | One quotation or order with its lines, timeline and deliveries |
| GET | /invoices | invoices:read | Invoices with their status and balance |
| GET | /addresses | orders:read | Delivery addresses (address_id for requests) |
| PUT | /stock-levels | stock:write | Report stock; we request what reached its reorder point |
| GET | /reorder-rules | stock:write | All reorder rules |
| POST | /reorder-rules | stock:write | Create or change an item’s rule |
| DELETE | /reorder-rules/{id} | stock:write | Delete a rule |
| GET | /webhooks | webhooks:manage | Your webhook endpoints |
| POST | /webhooks | webhooks:manage | Add an endpoint (its secret is shown once) |
| DELETE | /webhooks/{id} | webhooks:manage | Remove 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.submittedrequest.awaiting_approvalquotation.pricedorder.confirmedorder.cancelleddelivery.outdelivery.doneinvoice.issuedreorder.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" } }| 400 | bad_json | The body is not valid JSON |
| 400 | idempotency_key_required | A POST without an Idempotency-Key header |
| 401 | unauthorized | The key is missing, invalid or revoked |
| 403 | forbidden | The key lacks the scope (or may not accept quotations) |
| 404 | not_found | No such endpoint, request, order or product |
| 409 | not_ready | The company account is still being set up, or the quotation cannot be accepted as it stands |
| 413 | too_large | The body is larger than 256 KB |
| 422 | invalid · invalid_lines | Missing or invalid data (see details) |
| 422 | idempotency_key_reused | The Idempotency-Key was used with a different request |
| 429 | rate_limited | More than 120 calls a minute on this key |
| 503 | disabled | The API is switched off for now |
| 500 | server_error | Our 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

