AWRA OpsHub Search

Developer API · v1

API Reference

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.

Quickstart

  1. In AWRA OpsHub, open Settings → API & Webhooks and create a key. Choose the service Developer API and tick only the scopes the integration needs.
  2. Copy the token immediately. It is shown once — only a SHA-256 hash is stored, so it cannot be recovered later. If you lose it, rotate the key.
  3. Confirm it works by calling /me:
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.

Authentication

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.

Organizations

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

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.

ScopeAreaGrants 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.

Conventions

Base URL

https://awraops.com/api/v1

Response shape

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 }
}

Parameters on every list endpoint

ParameterTypeDescription
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.

Data types

  • Money is a string, always with two decimals — "139200.00". Parsing money as a JSON number invites floating-point drift; treat these as decimals in your language of choice.
  • Timestamps are ISO 8601 with the organization's offset — "2026-07-14T11:05:00+03:00".
  • Item IDs are UUID strings; customer, invoice, vendor, and purchase-order IDs are integers. Store them as given rather than casting.
  • Trashed records are omitted. Items, customers, invoices, vendors, and purchase orders the organization has moved to trash do not appear in any response. Categories and warehouses are deleted outright and simply stop being listed.

Syncing efficiently

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.

Errors

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key is missing the scope required for this endpoint. Required: sales:read."
  }
}
StatusCodeMeaning
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.

Rate limits

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.

Endpoint reference

Every endpoint is read-only. All paths are relative to https://awraops.com/api/v1.

Identity

any valid key

Confirms a key works and shows which organization it is bound to. Call this first when wiring up a new integration.

GET /me

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
    }
  }
}

Inventory

inventory:read

Stock positions and the reference data around them. Stock figures are the current on-hand quantity across the organization.

GET /items

List items with current stock levels.

Query parameters

NameTypeDescription
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 }
}
GET /items/{id}

Fetch a single item by its ID.

Example response

{ "data": { "id": "11111111-2222-…", "sku": "SKU-EXAMPLE-001", "name": "Example Widget", "stock": 681, "…": "…" } }
GET /categories

List item categories. Returns the full set, unpaginated.

Example response

{ "data": [ { "id": 6, "name": "Electronics" }, { "id": 4, "name": "Food" } ] }
GET /warehouses

List warehouses. Returns the full set, unpaginated.

Example response

{ "data": [ { "id": 1, "name": "Main Warehouse" }, { "id": 4, "name": "Field Store" } ] }

Sales

sales:read

Customers and the invoices raised against them — the surface most accounting and BI integrations need.

GET /customers

List customers with balances and credit limits.

Query parameters

NameTypeDescription
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 }
}
GET /customers/{id}

Fetch a single customer by ID.

Example response

{ "data": { "id": 2, "company_name": "Example Trading Ltd", "balance": "128400.00", "…": "…" } }
GET /invoices

List customer invoices with totals and outstanding balances.

Query parameters

NameTypeDescription
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 }
}
GET /invoices/{id}

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"
      }
    ]
  }
}

Procurement

procurement:read

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.

GET /vendors

List vendors.

Query parameters

NameTypeDescription
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 }
}
GET /vendors/{id}

Fetch a single vendor by ID.

Example response

{ "data": { "id": 5, "name": "Example Supplies", "is_active": true, "…": "…" } }
GET /purchase-orders

List purchase orders with payment and shipping state.

Query parameters

NameTypeDescription
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 }
}
GET /purchase-orders/{id}

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" }
    ]
  }
}

Public stock API

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.

EndpointReturns
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.

Inbound webhooks

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.

Notifications and workflow triggers

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.

Ops Pulse events

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.

Signatures and retries

  • If the credential has a signing secret, the signature header is mandatory — a missing or wrong signature is rejected with 401. Compute the HMAC over the exact raw request body, before any reserialization. The sha256= prefix is optional.
  • Send an Idempotency-Key (or X-Awra-Idempotency-Key) so a retry after a timeout cannot create a duplicate. A repeated key replays the original stored response and comes back marked X-Awra-Replayed: true.
  • Both endpoints accept up to 60 requests per minute.

What is not part of the public API

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.

Versioning and change policy

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.

Examples

Page through every unpaid invoice — PHP

<?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"]);

Incremental stock sync — JavaScript

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);

Low stock check — cURL

curl "https://awraops.com/api/v1/items?low_stock=true&per_page=100" \
  -H "Authorization: Bearer $AWRA_API_KEY" \
  -H "Accept: application/json"

Need something this reference does not cover?

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.