Ask AwraIQ about features, pricing, onboarding, login, integrations, security, demos, mobile apps, automation, reports, or support.
Developer API · v1
A read API over your organization's inventory, sales, and procurement records — authenticated with an API key you issue yourself, scoped to exactly the areas you choose, and safe to hand to a third-party developer.
curl https://awraops.com/api/v1/me \ -H "Authorization: Bearer awra_your_key_here" \ -H "Accept: application/json"
A 200 response naming your organization means the key is live. A 401 means the token is wrong, revoked, or expired.
Every request carries the key as a bearer token:
Authorization: Bearer awra_your_key_here
If your proxy or gateway already reserves the Authorization header, send the key as X-Api-Key instead. The two are equivalent; use one or the other.
There is no login step, no token refresh, and no expiry unless you set one when creating the key. Keys can be rotated or revoked at any time from the same settings screen, and revocation takes effect on the next request.
Treat the key like a password
Anyone holding it can read everything its scopes allow. Keep it in a secrets manager, never in source control or browser JavaScript, and give each integration its own key so you can revoke one without breaking the others.
AWRA OpsHub hosts many organizations, and every record belongs to exactly one of them. The API resolves your organization from the key itself. When you create a key, it is permanently bound to the organization you created it in.
This is why you will not find an organization ID anywhere in these docs — not in a URL, not in a header, not in a request body. There is nothing to pass and nothing to get wrong. A key cannot be pointed at another organization, because the binding is not something the client supplies.
Records outside your organization return 404 not_found — the same response as an ID that does not exist at all. The API never reveals whether an ID is in use somewhere else.
Scopes are chosen when the key is created and cannot be widened afterwards — to change them, issue a new key. Grant the narrowest set that does the job.
| Scope | Area | Grants access to |
|---|---|---|
| developer:read | Read everything | Umbrella scope. Satisfies every resource scope below. Convenient for a trusted internal integration; prefer narrow scopes for third parties. |
| inventory:read | Inventory | Items, categories, warehouses. |
| sales:read | Sales | Customers and customer invoices, including invoice line items. |
| procurement:read | Procurement | Vendors and purchase orders, including order line items. |
Calling an endpoint your key is not scoped for returns 403 insufficient_scope. Retrying will not help — the key needs to be reissued. /me is reachable by any valid key regardless of scope, so a narrowly-scoped integration can still verify itself.
https://awraops.com/api/v1
Single records return under data. Lists add meta and links. Errors return an error object instead of data — check for the key, not the shape.
{
"data": [ … ],
"meta": { "page": 1, "per_page": 25, "total": 37, "total_pages": 2 },
"links": { "next": "…?page=2", "prev": null }
} | Parameter | Type | Description |
|---|---|---|
| page | integer | Page number. Defaults to 1. |
| per_page | integer | Results per page. Defaults to 25, capped at 100. Values below 1 fall back to the default. |
| updated_since | ISO 8601 | Only records changed at or after this timestamp — use it for incremental sync instead of re-reading everything. An unparseable value returns 422 rather than being ignored. |
Store the timestamp of your last successful run and pass it as updated_since on the next one. You will get only what changed, which keeps you well inside the rate limit as the dataset grows. Note that this reflects changes, not deletions — reconcile with a full read periodically if you need to detect removed records.
{
"error": {
"code": "insufficient_scope",
"message": "This API key is missing the scope required for this endpoint. Required: sales:read."
}
} | Status | Code | Meaning |
|---|---|---|
| 200 | — | Success. |
| 401 | unauthorized | No bearer token, or the key is unknown, revoked, or past its expiry date. Not retryable — check the credential. |
| 403 | insufficient_scope | The key is valid but lacks the scope this endpoint requires. Mint a key with the right scope; retrying will not help. |
| 404 | not_found | No such record in your organization. Also returned for records that exist but belong to another organization — the API never confirms that an ID exists elsewhere. |
| 422 | invalid_parameter | A query parameter was malformed. The message names the offending parameter. |
| 429 | — | Rate limit exceeded. Back off and retry after the window resets. |
Retry 429 and 5xx with exponential backoff. Do not retry 401, 403, 404, or 422 — those need a change on your side.
120 requests per minute per API key. The budget is counted per key, so one integration cannot starve another — and issuing separate keys per integration gives each its own allowance.
Exceeding it returns 429. Standard X-RateLimit-Limit and X-RateLimit-Remaining headers accompany responses, and Retry-After tells you how long to wait.
Every endpoint is read-only. All paths are relative to https://awraops.com/api/v1.
Confirms a key works and shows which organization it is bound to. Call this first when wiring up a new integration.
Introspect the calling API key: the organization it belongs to, its scopes, and its expiry.
Example response
{
"data": {
"organization": { "id": "1", "name": "Example Organization" },
"key": {
"name": "Warehouse sync",
"prefix": "awra_1A6Kw",
"scopes": ["inventory:read"],
"created_at": "2026-08-03T18:35:26+03:00",
"expires_at": null
}
}
} Stock positions and the reference data around them. Stock figures are the current on-hand quantity across the organization.
List items with current stock levels.
Query parameters
| Name | Type | Description |
|---|---|---|
| search | string | Matches name, SKU (barcode), or description. |
| category | string | Exact category name. |
| low_stock | boolean | When true, only items whose stock has fallen to or below their reorder point. |
Plus the shared page, per_page, and updated_since parameters.
Example response
{
"data": [
{
"id": "11111111-2222-3333-4444-555555555555",
"sku": "SKU-EXAMPLE-001",
"name": "Example Widget",
"description": "Illustrative item description",
"category": "Office Supplies",
"stock": 681,
"reorder_point": 100,
"safety_stock": 40,
"lead_time_days": 7,
"tracking_mode": "none",
"buying_price": "540.00",
"selling_price": "700.00",
"last_movement_at": "2026-08-01T09:12:44+03:00",
"created_at": "2026-05-02T10:00:00+03:00",
"updated_at": "2026-08-01T09:12:44+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 37, "total_pages": 2 },
"links": { "next": "https://app.example.com/api/v1/items?page=2", "prev": null }
} Fetch a single item by its ID.
Example response
{ "data": { "id": "11111111-2222-…", "sku": "SKU-EXAMPLE-001", "name": "Example Widget", "stock": 681, "…": "…" } } List item categories. Returns the full set, unpaginated.
Example response
{ "data": [ { "id": 6, "name": "Electronics" }, { "id": 4, "name": "Food" } ] } List warehouses. Returns the full set, unpaginated.
Example response
{ "data": [ { "id": 1, "name": "Main Warehouse" }, { "id": 4, "name": "Field Store" } ] } Customers and the invoices raised against them — the surface most accounting and BI integrations need.
List customers with balances and credit limits.
Query parameters
| Name | Type | Description |
|---|---|---|
| search | string | Matches company name, first or last name, email, or phone. |
| status | string | Exact status value. |
Plus the shared page, per_page, and updated_since parameters.
Example response
{
"data": [
{
"id": 2,
"uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"company_name": "Example Trading Ltd",
"first_name": "Jane",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+254700000000",
"country": "Kenya",
"address": "Nairobi",
"currency_code": "KES",
"credit_limit": "500000.00",
"balance": "128400.00",
"status": "active",
"created_at": "2026-04-11T08:20:00+03:00",
"updated_at": "2026-07-30T16:02:11+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 2, "total_pages": 1 },
"links": { "next": null, "prev": null }
} Fetch a single customer by ID.
Example response
{ "data": { "id": 2, "company_name": "Example Trading Ltd", "balance": "128400.00", "…": "…" } } List customer invoices with totals and outstanding balances.
Query parameters
| Name | Type | Description |
|---|---|---|
| status | string | Exact invoice status, e.g. draft, sent, paid. |
| customer_id | integer | Only invoices for this customer. |
| issued_from | date | Invoice date on or after this date (YYYY-MM-DD). |
| issued_to | date | Invoice date on or before this date (YYYY-MM-DD). |
| unpaid | boolean | When true, only invoices with a balance still due. |
Plus the shared page, per_page, and updated_since parameters.
Example response
{
"data": [
{
"id": 10,
"invoice_number": "INV-2026-000010",
"status": "draft",
"customer": { "id": 1, "name": "Example Trading Ltd" },
"invoice_date": "2026-07-14T00:00:00+03:00",
"due_date": "2026-08-13T00:00:00+03:00",
"currency_code": "KES",
"subtotal": "120000.00",
"discount_total": "0.00",
"tax_amount": "19200.00",
"total": "139200.00",
"amount_paid": "0.00",
"balance_due": "139200.00",
"created_at": "2026-07-14T11:05:00+03:00",
"updated_at": "2026-07-14T11:05:00+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 10, "total_pages": 1 },
"links": { "next": null, "prev": null }
} Fetch a single invoice, including its line items.
Example response
{
"data": {
"id": 10,
"invoice_number": "INV-2026-000010",
"status": "draft",
"total": "139200.00",
"balance_due": "139200.00",
"line_items": [
{
"item_id": "11111111-2222-3333-4444-555555555555",
"description": "Example Widget",
"quantity": 200,
"unit_price": "600.00",
"discount": "0.00",
"tax_rate": 16,
"line_total": "120000.00"
}
]
}
} Vendors and purchase orders. Vendor banking and mobile-money details are deliberately excluded from every response — a read key is for reporting and sync, not for harvesting payout destinations.
List vendors.
Query parameters
| Name | Type | Description |
|---|---|---|
| search | string | Matches name, company, email, or phone. |
| is_active | boolean | Filter by active flag. |
| preferred_only | boolean | When true, only vendors marked preferred. |
Plus the shared page, per_page, and updated_since parameters.
Example response
{
"data": [
{
"id": 5,
"name": "Example Supplies",
"company": "Example Supplies Ltd",
"email": "[email protected]",
"phone": "254700000000",
"country": "Kenya",
"address": "Nairobi",
"is_active": true,
"is_preferred": false,
"is_blacklisted": false,
"created_at": "2026-03-02T09:00:00+03:00",
"updated_at": "2026-07-19T14:41:00+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 8, "total_pages": 1 },
"links": { "next": null, "prev": null }
} Fetch a single vendor by ID.
Example response
{ "data": { "id": 5, "name": "Example Supplies", "is_active": true, "…": "…" } } List purchase orders with payment and shipping state.
Query parameters
| Name | Type | Description |
|---|---|---|
| status | string | Exact order status. |
| payment_status | string | Exact payment status, e.g. unpaid, partial, paid. |
| vendor_id | integer | Only orders for this vendor. |
| ordered_from | date | Order date on or after this date (YYYY-MM-DD). |
| ordered_to | date | Order date on or before this date (YYYY-MM-DD). |
Plus the shared page, per_page, and updated_since parameters.
Example response
{
"data": [
{
"id": 5,
"po_number": "PO-2026-000003",
"status": "paid",
"payment_status": "paid",
"shipping_status": "delivered",
"vendor": { "id": 1, "name": "Example Supplies Ltd" },
"order_date": "2026-08-02T14:50:40+03:00",
"delivery_date": "2026-08-09T00:00:00+03:00",
"total_amount": "84000.00",
"amount_paid": "84000.00",
"balance_due": "0.00",
"tracking_number": "TRK-99120",
"created_at": "2026-08-02T14:50:40+03:00",
"updated_at": "2026-08-02T15:10:02+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 5, "total_pages": 1 },
"links": { "next": null, "prev": null }
} Fetch a single purchase order, including its line items.
Example response
{
"data": {
"id": 5,
"po_number": "PO-2026-000003",
"status": "paid",
"total_amount": "84000.00",
"line_items": [
{ "item_id": "11111111-2222-…", "quantity": 120, "price": "700.00", "total_cost": "84000.00" }
]
}
} A separate, deliberately minimal surface for storefronts and price-check widgets that need stock availability without being trusted with the rest of your data. It predates the Developer API and uses its own credential type and header.
Create a key with the service Public Stock API and the scope stock:read, then send it as X-Public-Token. As with the Developer API, the organization is carried by the key.
| Endpoint | Returns |
|---|---|
| GET /api/public/stock/{sku} | SKU, name, and current stock level for one item. 404 if the SKU is unknown. |
| GET /api/public/items/search | Paginated item matches. Requires a query parameter of 2–100 characters. |
| GET /api/public/categories | All item categories as id and name. |
curl "https://awraops.com/api/public/stock/SKU-EXAMPLE-001" \ -H "X-Public-Token: your_public_stock_token"
These endpoints respond in the legacy flat shape rather than the data envelope used by /api/v1. Rate limit: 60 requests per minute.
The Developer API is read-only. To push something into AWRA OpsHub from an external system — an alert, or an event that should trigger a workflow — use an inbound webhook. Configure the endpoint and its signing secret in Settings → API & Webhooks.
POST /api/inbound-webhooks
X-Awra-Inbound-Token: your_inbound_token
X-Awra-Signature: sha256=<hmac-sha256 of the raw body, keyed with your secret>
Idempotency-Key: 3f9c1b7a-…
{
"title": "Payment received",
"message": "KES 42,000 settled against INV-2026-000010",
"url": "https://your-system.example/payments/8823",
"type": "info",
"event": "payment.received",
"data": { "invoice": "INV-2026-000010", "amount": 42000 }
} message is required unless you supply event. Naming an event also fires any workflows the organization has configured for it, with data passed through to them; workflows run asynchronously, so the call returns without waiting for them.
For operational telemetry, POST /api/ops-pulse/webhook accepts an X-OpsPulse-Token, an X-OpsPulse-Signature, and a body of event_type and message (both required), plus optional severity, source, and payload.
AWRA OpsHub's mobile apps and linked barcode scanners talk to a much larger internal HTTP surface. It is authenticated differently — a session token issued to the app at sign-in, not an API key — and it is not a public contract: those routes change whenever the apps change, without notice or a version bump.
Please do not build against it, even if you can observe it. Anything documented on this page is supported and versioned; anything not documented here is not. If you need data or an action this reference does not cover, get in touch — that is a gap worth closing properly rather than working around.
The version is in the path — currently /api/v1. Within a version we will add endpoints, add fields to responses, and add optional parameters. We will not remove or rename a field, change a field's type, or change what an existing parameter does.
So write a tolerant client: ignore fields you do not recognize rather than failing on them. Anything that would break a well-behaved client ships as /api/v2, with the previous version kept running while you migrate.
<?php
$key = getenv("AWRA_API_KEY");
$page = 1;
do {
$url = "https://awraops.com/api/v1/invoices?unpaid=true&per_page=100&page={$page}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$key}", "Accept: application/json"],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($body["data"] as $invoice) {
echo $invoice["invoice_number"], " owes ", $invoice["balance_due"], PHP_EOL;
}
$page++;
} while ($page <= $body["meta"]["total_pages"]); const res = await fetch(
"https://awraops.com/api/v1/items?updated_since=" + encodeURIComponent(lastRunIso),
{ headers: { Authorization: "Bearer " + process.env.AWRA_API_KEY, Accept: "application/json" } }
);
if (res.status === 429) throw new Error("Rate limited — back off and retry");
if (!res.ok) throw new Error((await res.json()).error.message);
const { data, meta } = await res.json();
console.log(data.length + " of " + meta.total + " items changed since " + lastRunIso); curl "https://awraops.com/api/v1/items?low_stock=true&per_page=100" \ -H "Authorization: Bearer $AWRA_API_KEY" \ -H "Accept: application/json"
The Developer API is read-only by design and covers inventory, sales, and procurement. If you need another module, a write operation, or an outbound webhook for a specific event, tell us what you are building — it helps us prioritise what to open up next.