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.
Quick start
Check the service, then verify your key:
curl https://www.oowia.com/api/v1/auth curl -X POST https://www.oowia.com/api/v1/auth \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"action": "verify"}'
Authentication
Every private request carries your key in one of two headers:
Authorization: Bearer oowia_<key> X-Oowia-Api-Key: oowia_<key>
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" }toauth.phpreturns 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.
-
GET
/api/v1/authService health and API metadata
Returns service identity metadata. No credentials required.
-
POST
/api/v1/authVerify an API key and list its scopes
Validates the presented API key and returns the brand context, key label/prefix, resolved scopes, and expiry.
-
POST
/api/v1/designsDesign asset management
Scoped API for design upload, validation, metadata, listing, review submission, archival, and deletion.
-
POST
/api/v1/productsBrand product catalogue
Create, version, list, archive, and delete brand products. Published versions are immutable.
-
POST
/api/v1/ordersOrder lifecycle
Idempotent order placement, listing, detail, approval, and cancellation.
-
POST
/api/v1/trackingOrder tracking timeline
Read-only tracking for an order: fulfillment status, production stage, shipments, courier events, and NDR/RTO state.
-
POST
/api/v1/walletWallet finance
Read-only balance, ledger, recharge status, and invoices; withdrawals need the explicit wallet.withdraw scope.
-
POST
/api/v1/webhooksWebhook subscription management
API-key authenticated wrapper over the shared webhook service. Destinations must be HTTPS.
-
POST
/api/v1/addonsCustom 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.
-
GET
/api/v1/healthService health check
Uptime probe for the instance. Minimal mode needs no credentials; a health token adds the full report, and
deep=1adds the outbound probes.
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.
{
"error": { "code": "validation_error", "message": "Consumer name is required." }
}
400Unknown action value, or the request body is not valid JSON (invalid_action, invalid_json).No401Missing, invalid, revoked, or expired API key (unauthorized).No402The wallet cannot cover the order (insufficient_wallet_balance). Recharge, then repeat the request.Yes403Brand is not active, or the key lacks the scope for this action (brand_inactive, forbidden).No404Referenced record does not exist in this tenancy (not_found).No405Non-POST request to an action endpoint (method_not_allowed).No409State 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.No413JSON request body exceeds 1 MiB (payload_too_large). Multipart uploads are not affected.No422Missing or invalid fields, or a business rule failed (validation_error).No425Transient idempotency race on order create (retryable). Back off and retry with the same idempotency_key.Yes429Key hourly window exhausted: 300 requests per rolling 3600 seconds, or the per-IP failed-auth bucket (60 per hour) is over budget (rate_limited).Yes500Upload could not be stored, or an internal error occurred (upload_failed). It is logged on our side.YesFor 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.
Requests / hour
Per API key, rolling 3600-second window, shared across every endpoint.
When you exceed it
You get rate_limited. Slow down and retry after the window rolls.
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 -X POST https://www.oowia.com/api/v1/orders \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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": "[email protected]", "shipping_address": "221B Baker Street", "shipping_city": "Mumbai", "shipping_state": "Maharashtra", "shipping_postal_code": "400001", "external_order_id": "SALE-8842" }'
const res = await fetch("https://www.oowia.com/api/v1/orders", { method: "POST", headers: { "Authorization": `Bearer ${process.env.OOWIA_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ 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", shipping_address: "221B Baker Street", shipping_city: "Mumbai", shipping_postal_code: "400001" }) }); const body = await res.json();
import requests res = requests.post( "https://www.oowia.com/api/v1/orders", headers={"Authorization": f"Bearer {OOWIA_KEY}"}, json={ "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", "shipping_address": "221B Baker Street", "shipping_city": "Mumbai", "shipping_postal_code": "400001", }, ) body = res.json()
$payload = [ "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", "shipping_address" => "221B Baker Street", "shipping_city" => "Mumbai", "shipping_postal_code" => "400001", ]; $ch = curl_init("https://www.oowia.com/api/v1/orders"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . $oowiaKey, "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_THROW_ON_ERROR)); $body = json_decode((string) curl_exec($ch), true);
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.
{
"ok": true,
"data": { "order": { "id": 9001, "fulfillment_status": "pending" } }
}