#  *Talk to the *floor*, via API.* 

Your brand, programmable. Designs, products, orders, tracking, wallet, webhooks and addons - one scoped REST API under `/api/v1`. Every action is a `POST` carrying an `action` field, every answer is `JSON`.

 Base URL `https://www.oowia.com/api/v1` copy 

- REST · JSON
- API **v1**
- Rate **300/hour**
- Auth **Bearer key**
 
 

 

 Reference

 - [*01*Overview](#overview)
- [*02*Authentication](#auth)
- [*03*Endpoints](#endpoints)
- [*04*Errors](#errors)
- [*05*Rate limits](#limits)
- [*06*Webhooks](#webhooks)
- [*07*Code samples](#samples)
 
      ## *Overview*

 The Oowia API is a **scoped REST** service for your own brand tenancy - designs, products, orders, tracking, wallet, webhooks and addons. Every endpoint lives under `/api/v1` on your Oowia instance (production: `https://www.oowia.com/api/v1`). All actions are **POST** requests carrying an `action` field as JSON or form-encoded; design file uploads use `multipart/form-data`. `GET` is accepted only on `auth.php` for service metadata and on `health` for the health check.

Success is always `{ "ok": true, "data": ... }` over HTTP 200. Failures are `{ "error": { "code", "message" } }` with a non-2xx status. Machine-readable truth: `/api/docs/openapi.json`. Readable guide: `/api/docs/reference.md`.

 **Reading this reference** - the auth metadata route is public. Every other action needs an API key from your dashboard (Developer → API keys) with the scope listed on its card. 

### Quick start

Check the service, then verify your key:

 terminal copy 

 ```
<span class="c-kw">curl</span> https://www.oowia.com/api/v1/auth
<span class="c-kw">curl -X POST</span> https://www.oowia.com/api/v1/auth \
  <span class="c-kw">-H</span> <span class="c-str">"Authorization: Bearer <span class="c-var">YOUR_API_KEY</span>"</span> \
  <span class="c-kw">-H</span> <span class="c-str">"Content-Type: application/json"</span> \
  <span class="c-kw">-d</span> <span class="c-str">'{"action": "verify"}'</span>
```

 

    ## *Authentication*

 Every private request carries your key in one of two headers:

 http copy 

 ```
Authorization: Bearer <span class="c-var">oowia_<key></span>
X-Oowia-Api-Key: <span class="c-var">oowia_<key></span>
```

 

Keys are issued per brand, stored hashed (sha-256), revocable, and optionally expiring. Each key carries scopes such as `orders.create` or `wallet.read` - a key can only reach the actions its scopes cover. Browser sessions are never accepted as API authority.

- Send the key in a header - never as a query string.
- `POST { "action": "verify" }` to `auth.php` returns your brand, key label, prefix and resolved scopes.
- Missing, invalid, revoked or expired key → `401 unauthorized`. Inactive brand → `403 brand_inactive`. Wrong scope → `403 forbidden`.
 
    ## *Endpoints*

 The full surface, rendered from the live OpenAPI spec - one card per route. Every card below is server-rendered; there is no fallback copy.

 1. GET `/api/v1/auth` 
    
    ### Service health and API metadata
    
    Returns service identity metadata. No credentials required.
    
    Public access1 GET route
2. POST `/api/v1/auth` 
    
    ### Verify an API key and list its scopes
    
    Validates the presented API key and returns the brand context, key label/prefix, resolved scopes, and expiry.
    
    API key access1 action
3. POST `/api/v1/designs` 
    
    ### Design asset management
    
    Scoped API for design upload, validation, metadata, listing, review submission, archival, and deletion.
    
    API key access8 actions
4. POST `/api/v1/products` 
    
    ### Brand product catalogue
    
    Create, version, list, archive, and delete brand products. Published versions are immutable.
    
    API key access8 actions
5. POST `/api/v1/orders` 
    
    ### Order lifecycle
    
    Idempotent order placement, listing, detail, approval, and cancellation.
    
    API key access5 actions
6. POST `/api/v1/tracking` 
    
    ### Order tracking timeline
    
    Read-only tracking for an order: fulfillment status, production stage, shipments, courier events, and NDR/RTO state.
    
    API key access1 action
7. POST `/api/v1/wallet` 
    
    ### Wallet finance
    
    Read-only balance, ledger, recharge status, and invoices; withdrawals need the explicit wallet.withdraw scope.
    
    API key access5 actions
8. POST `/api/v1/webhooks` 
    
    ### Webhook subscription management
    
    API-key authenticated wrapper over the shared webhook service. Destinations must be HTTPS.
    
    API key access6 actions
9. POST `/api/v1/addons` 
    
    ### Custom Branding Options catalogue and stock orders
    
    Branding options are pre-paid: buy stock in MOQ multiples through branding stock orders charged once from the wallet.
    
    API key access3 actions
10. GET `/api/v1/health` 
    
    ### Service health check
    
    Uptime probe for the instance. Minimal mode needs no credentials; a health token adds the full report, and `deep=1` adds the outbound probes.
    
    Public access1 GET route
 
    ## *Errors*

 Errors are never silent. Every non-2xx response returns one shape - `{ "error": { "code", "message" } }` as JSON when you send `Accept: application/json`, otherwise the same status with a plain-text body.

 422 validation\_error copy 

 ```
{
  <span class="c-key">"error"</span>: { <span class="c-key">"code"</span>: <span class="c-str">"validation_error"</span>, <span class="c-key">"message"</span>: <span class="c-str">"Consumer name is required."</span> }
}
```

 

 Code Meaning Retry? 

`400`Unknown action value, or the request body is not valid JSON (invalid\_action, invalid\_json).**No**

`401`Missing, invalid, revoked, or expired API key (unauthorized).**No**

`402`The wallet cannot cover the order (insufficient\_wallet\_balance). Recharge, then repeat the request.**Yes**

`403`Brand is not active, or the key lacks the scope for this action (brand\_inactive, forbidden).**No**

`404`Referenced record does not exist in this tenancy (not\_found).**No**

`405`Non-POST request to an action endpoint (method\_not\_allowed).**No**

`409`State conflict: quota\_exceeded, moderation\_failed, state\_conflict, approval\_failed, stock\_unavailable, inventory\_conflict, addon\_conflict, cancellation\_failed, payment\_failed, withdrawal\_failed, withdrawal\_duplicate, duplicate\_subscription.**No**

`413`JSON request body exceeds 1 MiB (payload\_too\_large). Multipart uploads are not affected.**No**

`422`Missing or invalid fields, or a business rule failed (validation\_error).**No**

`425`Transient idempotency race on order create (retryable). Back off and retry with the same idempotency\_key.**Yes**

`429`Key hourly window exhausted: 300 requests per rolling 3600 seconds, or the per-IP failed-auth bucket (60 per hour) is over budget (rate\_limited).**Yes**

`500`Upload could not be stored, or an internal error occurred (upload\_failed). It is logged on our side.**Yes**

 

For retryable errors, back off and retry. `stock_unavailable` carries `error.items[]` naming each out-of-stock order line (JSON responses only).

    ## *Rate limits*

 Each key is limited to **300 requests per rolling 3600-second window**, counted per API key across all endpoints. Exceeding it returns `429 rate_limited`.

 300#### Requests / hour

Per API key, rolling 3600-second window, shared across every endpoint.

 

 429#### When you exceed it

You get `rate_limited`. Slow down and retry after the window rolls.

 

 1 key#### One bucket

Limits are keyed per API key, not per endpoint. Use separate keys to isolate workloads.

 

 

    ## *Webhooks*

 Instead of polling, subscribe an HTTPS URL to an event and we push. Manage subscriptions via `POST /api/v1/webhooks` (`list, create, set-status, rotate-secret, test, deliveries`, all requiring the `webhooks.manage` scope). Destinations must be HTTPS; signing secrets are stored encrypted and shown exactly once. Test deliveries send `webhook.test`.

Every delivery is a JSON `POST` carrying `X-Oowia-Signature` (`t=<timestamp>,v1=<hex hmac_sha256>` over `<timestamp>.<raw body>`) and `X-Oowia-Event-Id`. Recompute the HMAC with your secret and compare in constant time, then reject deliveries whose `t` is outside your tolerance window: the timestamp is inside the signed string, so it cannot be altered without breaking the HMAC.

 `order.created`#### Order placed

Order placed, queued for review.

 

 `order.approved`#### Order approved

Order approved and charged to the wallet.

 

 `order.cancelled`#### Order cancelled

Order cancelled.

 

 `order.status_changed`#### Status changed

Fulfillment status changed.

 

 `order.shipped`#### Order shipped

Handed to the courier with a tracking number.

 

 `order.delivered`#### Order delivered

Marked delivered.

 

 `order.ndr`#### Delivery issue

Non-delivery attempt logged.

 

 `order.rto`#### Returned to origin

Returned to origin.

 

 `order.needs_attention`#### Needs attention

Order needs brand attention.

 

 `wallet.recharge`#### Wallet recharged

Wallet recharged.

 

 `wallet.credit`#### Wallet credited

Wallet ledger gained a credit entry.

 

 `wallet.debit`#### Wallet debited

Wallet ledger gained a debit entry.

 

 `wallet.withdrawal`#### Wallet withdrawal

Wallet recorded a withdrawal.

 

 `wallet.low_balance`#### Balance low

Balance dropped below the low-balance threshold.

 

 `support.claim_submitted`#### Claim submitted

Support claim submitted.

 

 `support.claim_reviewed`#### Claim reviewed

Support claim received a decision.

 

 `shipping.ndr_brand_instructed`#### Instruction requested

Brand instruction requested on an NDR.

 

 

    ## *Code samples*

 Placing an order end to end - pick your language. Same `POST` + `action` shape everywhere; only the wrapper changes.

 cURL Node.js Python PHP 

 POST /api/v1/orders · create an order copy 

  ```
<span class="c-kw">curl -X POST</span> https://www.oowia.com/api/v1/orders \
  <span class="c-kw">-H</span> <span class="c-str">"Authorization: Bearer <span class="c-var">YOUR_API_KEY</span>"</span> \
  <span class="c-kw">-H</span> <span class="c-str">"Content-Type: application/json"</span> \
  <span class="c-kw">-d</span> <span class="c-str">'{
    "action": "create",
    "idempotency_key": "order-2026-0001",
    "shipping_service": "surface",
    "payment_type": "prepaid",
    "lines": [{ "blank_product_variant_id": 4112, "quantity": 2,
      "placements": [{ "design_asset_id": 1024, "surface": "front" }] }],
    "consumer_name": "Asha Rao",
    "consumer_email": "asha@example.com",
    "shipping_address": "221B Baker Street",
    "shipping_city": "Mumbai",
    "shipping_state": "Maharashtra",
    "shipping_postal_code": "400001",
    "external_order_id": "SALE-8842"
  }'</span>
```

 

 POST /api/v1/orders · Node.js copy 

 ```
<span class="c-kw">const</span> res = <span class="c-kw">await</span> fetch(<span class="c-str">"https://www.oowia.com/api/v1/orders"</span>, {
  method: <span class="c-str">"POST"</span>,
  headers: {
    <span class="c-str">"Authorization"</span>: <span class="c-str">`Bearer ${process.env.OOWIA_KEY}`</span>,
    <span class="c-str">"Content-Type"</span>: <span class="c-str">"application/json"</span>
  },
  body: JSON.stringify({
    action: <span class="c-str">"create"</span>,
    idempotency_key: <span class="c-str">"order-2026-0001"</span>,
    shipping_service: <span class="c-str">"surface"</span>,
    payment_type: <span class="c-str">"prepaid"</span>,
    lines: [{ blank_product_variant_id: 4112, quantity: 2,
      placements: [{ design_asset_id: 1024, surface: <span class="c-str">"front"</span> }] }],
    consumer_name: <span class="c-str">"Asha Rao"</span>,
    shipping_address: <span class="c-str">"221B Baker Street"</span>,
    shipping_city: <span class="c-str">"Mumbai"</span>,
    shipping_postal_code: <span class="c-str">"400001"</span>
  })
});
<span class="c-kw">const</span> body = <span class="c-kw">await</span> res.json();
```

 

 POST /api/v1/orders · Python copy 

 ```
<span class="c-kw">import</span> requests

res = requests.post(
    <span class="c-str">"https://www.oowia.com/api/v1/orders"</span>,
    headers={<span class="c-str">"Authorization"</span>: <span class="c-str">f"Bearer {OOWIA_KEY}"</span>},
    json={
        <span class="c-str">"action"</span>: <span class="c-str">"create"</span>,
        <span class="c-str">"idempotency_key"</span>: <span class="c-str">"order-2026-0001"</span>,
        <span class="c-str">"shipping_service"</span>: <span class="c-str">"surface"</span>,
        <span class="c-str">"payment_type"</span>: <span class="c-str">"prepaid"</span>,
        <span class="c-str">"lines"</span>: [{<span class="c-str">"blank_product_variant_id"</span>: 4112, <span class="c-str">"quantity"</span>: 2,
            <span class="c-str">"placements"</span>: [{<span class="c-str">"design_asset_id"</span>: 1024, <span class="c-str">"surface"</span>: <span class="c-str">"front"</span>}]}],
        <span class="c-str">"consumer_name"</span>: <span class="c-str">"Asha Rao"</span>,
        <span class="c-str">"shipping_address"</span>: <span class="c-str">"221B Baker Street"</span>,
        <span class="c-str">"shipping_city"</span>: <span class="c-str">"Mumbai"</span>,
        <span class="c-str">"shipping_postal_code"</span>: <span class="c-str">"400001"</span>,
    },
)
body = res.json()
```

 

 POST /api/v1/orders · PHP copy 

 ```
$payload = [
  <span class="c-str">"action"</span> => <span class="c-str">"create"</span>,
  <span class="c-str">"idempotency_key"</span> => <span class="c-str">"order-2026-0001"</span>,
  <span class="c-str">"shipping_service"</span> => <span class="c-str">"surface"</span>,
  <span class="c-str">"payment_type"</span> => <span class="c-str">"prepaid"</span>,
  <span class="c-str">"lines"</span> => [[<span class="c-str">"blank_product_variant_id"</span> => 4112, <span class="c-str">"quantity"</span> => 2,
    <span class="c-str">"placements"</span> => [[<span class="c-str">"design_asset_id"</span> => 1024, <span class="c-str">"surface"</span> => <span class="c-str">"front"</span>]]]],
  <span class="c-str">"consumer_name"</span> => <span class="c-str">"Asha Rao"</span>,
  <span class="c-str">"shipping_address"</span> => <span class="c-str">"221B Baker Street"</span>,
  <span class="c-str">"shipping_city"</span> => <span class="c-str">"Mumbai"</span>,
  <span class="c-str">"shipping_postal_code"</span> => <span class="c-str">"400001"</span>,
];
$ch = curl_init(<span class="c-str">"https://www.oowia.com/api/v1/orders"</span>);
curl_setopt($ch, CURLOPT_POST, <span class="c-num">true</span>);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, <span class="c-num">true</span>);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
  <span class="c-str">"Authorization: Bearer "</span> . $oowiaKey,
  <span class="c-str">"Content-Type: application/json"</span>,
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_THROW_ON_ERROR));
$body = json_decode((string) curl_exec($ch), <span class="c-num">true</span>);
```

 

### The response

A successful action returns HTTP `200` with the `{ "ok": true, "data" }` envelope. Order creates return `data.order` with items, pricing lines, events, holds and order addons.

 200 OK · envelope copy 

 ```
{
  <span class="c-key">"ok"</span>: <span class="c-num">true</span>,
  <span class="c-key">"data"</span>: { <span class="c-key">"order"</span>: { <span class="c-key">"id"</span>: 9001, <span class="c-key">"fulfillment_status"</span>: <span class="c-str">"pending"</span> } }
}
```
