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
  • REST · JSON
  • API v1
  • Rate 300/hour
  • Auth Bearer key

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

http
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" } 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
{
  "error": { "code": "validation_error", "message": "Consumer name is required." }
}
Code Meaning Retry?
400Unknown action value, or the request body is not valid JSON (invalid_action, invalid_json).No
401Missing, invalid, revoked, or expired API key (unauthorized).No
402The wallet cannot cover the order (insufficient_wallet_balance). Recharge, then repeat the request.Yes
403Brand is not active, or the key lacks the scope for this action (brand_inactive, forbidden).No
404Referenced record does not exist in this tenancy (not_found).No
405Non-POST request to an action endpoint (method_not_allowed).No
409State 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
413JSON request body exceeds 1 MiB (payload_too_large). Multipart uploads are not affected.No
422Missing or invalid fields, or a business rule failed (validation_error).No
425Transient idempotency race on order create (retryable). Back off and retry with the same idempotency_key.Yes
429Key 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
500Upload 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.

POST /api/v1/orders · create an order
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"
  }'
POST /api/v1/orders · Node.js
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();
POST /api/v1/orders · Python
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()
POST /api/v1/orders · PHP
$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.

200 OK · envelope
{
  "ok": true,
  "data": { "order": { "id": 9001, "fulfillment_status": "pending" } }
}