Developer Docs

← Back to Console

API Endpoints

Complete endpoint reference for the Equos Orders API

Base URL

https://api.equos.one/e9v3

Machine-Readable Spec

The full API surface is described by an OpenAPI 3.1 specification at/openapi.yaml — import it into Postman, Insomnia, or an SDK generator. A ready-madePostman collection is also available, including the OAuth client-credentials token request.

Authentication

All endpoints require the following header:

Authorization: Bearer <token>

The token must have scope orders:write.

Rate Limiting

1 request/second per API key with a burst capacity of 10. The limit applies to all /e9v3 endpoints, including reads. See Rate Limiting for details.

Return & Transfer Orders

There are no dedicated endpoints for return and transfer orders. Instead, use the /sync endpoint with the appropriate orderType:

  • Return orders: POST /sync with orderType: "returns" (the singular "return" is also accepted) and locationId
  • Transfer orders: POST /sync with orderType: "transfer" and agentId

Contact Field Reference

Each order requires exactly one contact field. The table below shows an example contact field for each order type — /e9v3/orders/sync only enforces that exactly one of customerId, supplierId, locationId, or agentId is present, not which type pairs with which field:

Order TypeEndpointRequired Contact FieldDescription
sendPOST /sendcustomerIdID of customer receiving the order
receivePOST /receivesupplierId or email/phoneSupplier ID, or email/phone for match-or-create
returnsPOST /synclocationIdID of location returning items (return accepted as an alias)
transferPOST /syncagentIdID of agent receiving the transfer

POST/e9v3/orders/syncCreate order (any type)
🔒 Bearer (scope: orders:write)

Creates a generic order that can be of any type (send, receive, return, transfer). Use this endpoint for integrations where the order type is dynamic.

Request Body
FieldTypeRequiredDescription
orderTypestringRequiredOrder type: send, receive, returns, or transfer (case-insensitive). The singular return is accepted as an alias and canonicalized to returns.
customerIdstringOptionalCustomer ID (use for send orders)
supplierIdstringOptionalSupplier ID (use for receive orders)
locationIdstringOptionalLocation ID (use for internal transfers)
agentIdstringOptionalAgent ID (use for agent-based orders)
itemsarrayRequiredArray of line items (minimum 1 item)
reference1stringOptionalPO number or purchase reference
reference2stringOptionalSO number or sales reference
reference3stringOptionalExternal reference ID
orderDatestring (RFC3339)OptionalOrder date in ISO format (e.g., 2024-01-15T00:00:00Z)
dispatchDatestring (RFC3339)OptionalExpected dispatch date
notesstringOptionalOrder notes or comments (input as string, stored as array)
sourceRecordIdstringOptionalExternal system ID for reconciliation
metadataobjectOptionalFree-form JSON object (string keys, any JSON-serializable values)
Item Schema (items[])
FieldTypeRequiredDescription
skustringRequiredProduct SKU code
productNamestringRequiredProduct name or description
quantitynumberRequiredQuantity (must be > 0, decimals allowed)
metadataobjectOptionalPer-line item metadata
{
  "success": true,
  "jobId": "inbound_1718000000000000000"
}
{
  "success": false,
  "error": "Validation failed",
  "details": [
    {
      "field": "items[0].sku",
      "message": "required field",
      "value": null
    },
    {
      "field": "items[0].quantity",
      "message": "must be greater than 0",
      "value": 0
    }
  ]
}
{
  "error": "invalid token"
}
{
  "error": "rate limit exceeded",
  "retry_after": 0.82,
  "rate_limit": "1 order/second",
  "burst_capacity": 10
}
{
  "success": false,
  "error": "Failed to queue order processing"
}
Notes
  • Exactly one contact field required: customerId, supplierId, locationId, or agentId
  • orderType is case-insensitive: accepts "send", "SEND", "Send", etc.
  • The job is processed asynchronously; track status using the job tracking API
POST/e9v3/orders/sendCreate send order (to customer)
🔒 Bearer (scope: orders:write)

Creates a send order. The orderType is forced to 'send' server-side and should be omitted from the request.

Request body: Same schema as /sync endpoint, but orderType is forced to "send" on the server (the value you send is ignored).

Required contact field: customerId (or other contact field if modified)

⚠️ No existence check: /orders/send does not verify that the customerId or the line-item SKUs exist — the order is still created and queued. Unknown references surface later when the job is processed. Only /inventory/sync validates the SKU up front (404 on an unknown SKU).
Request Body
FieldTypeRequiredDescription
orderTypestringRequiredOrder type: send, receive, returns, or transfer (case-insensitive). The singular return is accepted as an alias and canonicalized to returns.
customerIdstringOptionalCustomer ID (use for send orders)
supplierIdstringOptionalSupplier ID (use for receive orders)
locationIdstringOptionalLocation ID (use for internal transfers)
agentIdstringOptionalAgent ID (use for agent-based orders)
itemsarrayRequiredArray of line items (minimum 1 item)
reference1stringOptionalPO number or purchase reference
reference2stringOptionalSO number or sales reference
reference3stringOptionalExternal reference ID
orderDatestring (RFC3339)OptionalOrder date in ISO format (e.g., 2024-01-15T00:00:00Z)
dispatchDatestring (RFC3339)OptionalExpected dispatch date
notesstringOptionalOrder notes or comments (input as string, stored as array)
sourceRecordIdstringOptionalExternal system ID for reconciliation
metadataobjectOptionalFree-form JSON object (string keys, any JSON-serializable values)
Item Schema (items[])
FieldTypeRequiredDescription
skustringRequiredProduct SKU code
productNamestringRequiredProduct name or description
quantitynumberRequiredQuantity (must be > 0, decimals allowed)
metadataobjectOptionalPer-line item metadata
{
  "success": true,
  "jobId": "inbound_1718000000000000000"
}
{
  "success": false,
  "error": "Validation failed",
  "details": [
    {
      "field": "items[0].sku",
      "message": "required field",
      "value": null
    },
    {
      "field": "items[0].quantity",
      "message": "must be greater than 0",
      "value": 0
    }
  ]
}
{
  "error": "invalid token"
}
{
  "error": "rate limit exceeded",
  "retry_after": 0.82,
  "rate_limit": "1 order/second",
  "burst_capacity": 10
}
POST/e9v3/orders/receiveCreate receive order (from supplier)
🔒 Bearer (scope: orders:write)

Creates a receive order with supplier match-or-create capability. Supports resolving suppliers by ID or by email/phone/name.

Request body: Uses ReceiveOrderRequest schema (different from /sync). Supports supplier match-or-create logic.

Supplier contact rule: Must provide at least one of: supplierId, supplierEmail, or supplierPhone. If supplierId is not provided but email or phone is, supplierName is required for new contact creation.

Request Body
FieldTypeRequiredDescription
supplierIdstringOptionalSupplier ID (use if supplier already exists)
supplierEmailstringOptionalSupplier email (used for match-or-create)
supplierPhonestringOptionalSupplier phone (used for match-or-create)
supplierNamestringOptionalSupplier name (required if supplierId not provided)
supplierBusinessNamestringOptionalBusiness name for new contact creation
supplierAddressLine1stringOptionalStreet address (Flutter addressLine1). Recommended with suburb + postcode.
supplierAddressLine2stringOptionalApt, suite, unit (optional; Flutter addressLine2)
supplierSuburbstringOptionalSuburb / city (Flutter suburb)
supplierStatestringOptionalState / territory e.g. QLD, NSW (Flutter state)
supplierPostcodestringOptionalPostcode (Flutter postcode)
externalSupplierIdstringOptionalExternal system ID (dedup key for match-or-create)
itemsarrayRequiredArray of line items (minimum 1 item)
reference1stringOptionalReference 1
reference2stringOptionalReference 2
reference3stringOptionalReference 3
orderDatestring (RFC3339)OptionalOrder date
dispatchDatestring (RFC3339)OptionalDispatch date
notesstringOptionalNotes
sourceRecordIdstringOptionalExternal record ID
metadataobjectOptionalCustom metadata
Item Schema (items[])
FieldTypeRequiredDescription
skustringRequiredProduct SKU code
productNamestringRequiredProduct name or description
quantitynumberRequiredQuantity (must be > 0, decimals allowed)
metadataobjectOptionalPer-line item metadata
{
  "success": true,
  "jobId": "inbound_1718000000000000000"
}
{
  "success": false,
  "error": "Validation failed",
  "details": [
    {
      "field": "items[0].sku",
      "message": "required field",
      "value": null
    },
    {
      "field": "items[0].quantity",
      "message": "must be greater than 0",
      "value": 0
    }
  ]
}
{
  "error": "invalid token"
}
{
  "error": "rate limit exceeded",
  "retry_after": 0.82,
  "rate_limit": "1 order/second",
  "burst_capacity": 10
}
POST/e9v3/orders/send-matchedCreate send order (customer match-or-create)
🔒 Bearer (scope: orders:write)

Like /e9v3/orders/send, but no pre-existing customerId is required. If customerId is supplied, the order uses that contact as-is even when ship-to details differ. Otherwise supply customerName plus line1, suburb, and postcode: reuse only on an exact name+address match, else create. Designed for storefront and marketplace sources where the customer is only known by their checkout details.

Notes
  • Request body is identical to /e9v3/orders/send; customerId becomes optional
  • If customerId is supplied, the order uses that contact as-is even when ship-to details differ — this is the only mismatch exception
  • Otherwise matching reuses a contact only when name plus customerAddressLine1, customerSuburb, and customerPostcode all match exactly (street abbreviations like st/rd allowed). Incomplete street or suburb+postcode alone never reuses; unmatched customers are created
  • Structured address fields mirror Flutter contacts: customerAddressLine1, customerAddressLine2, customerSuburb, customerState, customerPostcode. Recommended complete set: line1 + suburb + postcode (+ state). Address verification warnings for non-exact matches are planned next.
  • Asynchronous — returns a jobId like the other order writes

Reading Orders & Job Tracking

GET/e9v3/ordersList orders
🔒 Bearer (scope: orders:read)

Lists orders of one type. Query parameters: type (optional, default send; one of send, receive, transfer, returns — the singular return is also accepted and canonicalized to returns), archived (boolean, default false — set true to list archived orders), status, search (order number / contact / reference1), limit (default 100, max 500). status and search are filtered in-memory over the first limit+1 documents; there is no cursor pagination.

GET/e9v3/orders/{orderType}/{orderId}Get order with lines
🔒 Bearer (scope: orders:read)

Returns the order summary plus its line items. Looks in open orders first, then archived.

GET/e9v3/jobs/{jobId}Get job status
🔒 Bearer (scope: orders:read)

Reports a queued write's lifecycle: pending → processing → completed | failed. Failed jobs carry errorMessage; completed jobs may carry a result object (e.g. the created orderId).

Supplier Match-or-Create Logic (Receive Orders)

When creating a receive order via POST /receive, if you don't provide a supplierId, the system will attempt to find or create a matching supplier contact.

Matching Precedence

The system searches for an existing supplier in this order:

  1. External ID: If externalSupplierId is provided, match by externalId field (fastest)
  2. Email: If supplierEmail is provided, match by email address
    • If multiple suppliers match → no match (ambiguous), continue to next method
  3. Phone: If supplierPhone is provided, match by normalized phone (last 10 digits)
    • If multiple suppliers match → no match (ambiguous), continue to next method
  4. Name: If supplierName is provided, match by business name or contact name
    • If multiple suppliers match → no match (ambiguous), continue to next method
  5. Create New: If no match found, create a new supplier contact using provided details

Creating New Suppliers

When no match is found, the system creates a new supplier with:

  • Required: At least supplierName (or supplierBusinessName if providing business entity)
  • Optional: supplierAddressLine1, supplierAddressLine2, supplierSuburb, supplierState, supplierPostcode, supplierEmail, supplierPhone
  • Link: externalSupplierId is stored for future matches on that ID

Date Format Reference

Order dates (orderDate, dispatchDate) must be provided in RFC 3339 format (ISO 8601 with timezone).

Acceptable formats:

  • 2024-01-21T12:00:00Z — UTC (recommended)
  • 2024-01-21T12:00:00+00:00 — UTC with explicit offset
  • 2024-01-21T12:00:00-05:00 — Eastern Standard Time
  • 2024-01-21T12:00:00.123Z — UTC with milliseconds

Important: Always include a timezone (Z for UTC or ±HH:MM offset). Dates without timezone information will be rejected.

Metadata Field Guidance

Both orders and line items support a metadata field for storing custom data:

  • Type: Free-form JSON object (any valid JSON-serializable values)
  • Keys: Must be strings (e.g., "department": "sales")
  • Values: Can be strings, numbers, booleans, arrays, or nested objects
  • Use cases: Track internal IDs, integration tags, cost centers, timestamps, or any custom attributes
  • Persistence: Stored throughout the order processing lifecycle

Example:

"metadata": {
  "costCenter": "DEPT-42",
  "internalId": "order-12345",
  "tags": ["urgent", "vip"],
  "processedAt": "2024-01-21T12:00:00Z"
}

Common Validation Errors

FieldError MessageHow to Fix
orderTyperequired field (if blank)Provide a valid order type: send, receive, returns (or return), transfer
contactexactly one of customerId, supplierId, locationId, or agentId must be providedProvide exactly ONE contact field
itemsat least one line item is requiredInclude at least one item in the items array
items[N].skurequired fieldProvide a SKU for each item
items[N].quantitymust be greater than 0Quantity must be a positive number (decimals allowed)

Authentication Error Codes (401)

ErrorDescription
missing or invalid Authorization headerNo Bearer token provided or header missing
invalid tokenJWT parse/verification failed
token not validJWT validation check failed
invalid token claimsJWT claims malformed
token issuer mismatchToken issuer does not match the expected issuer (https://id.equos.one)
token audience mismatchToken audience does not include https://api.equos.one
token missing sub claimToken does not have a subject claim
unknown applicationToken subject (client_id) not found in system

Permission Error Codes (403)

ErrorDescription
api key revokedAPI key has been revoked
api key expiredAPI key expiration date has passed
insufficient scope: orders:write requiredToken does not have orders:write scope

Job Result Schema

All order endpoints return a jobId and process asynchronously. Track the job's completion status using the job ID returned in the response.

Document Status Lifecycle: pending → processing → completed or failed

  • Pending: Immediately after order creation
  • Completed: When processing succeeds; includes result.orderId and completedAt timestamp
  • Failed: When processing fails; includes an errorMessage string (there is no separate failedAt — the job keeps its createdAt)

See the Examples page for code to poll this document until job completion.


Contacts API

The contacts endpoints allow you to discover customers, suppliers, and other contacts in your account before creating orders. List and search read the published ContactIndex. Get-by-id reads the live contact document (full address, ABN, notes).

Authentication

All contacts endpoints require the contacts:read scope in your API key and Bearer token authentication.

Address Validation

List returns a contact when it has:

  • At least one contact method: email, phone, or mobile
  • Locality: suburb and postcode (street is optional on list)
  • Valid postcode: postcode/suburb combination exists in the Australian postcode database (or the CSV was unavailable — then unverified)

List endpoint validation: Uses the Australian postcode database (fast, no network calls). addressStatus is postcode_only or unverified. Archived contacts and invalid postcodes are excluded. If no ContactIndex is published, list returns 404 "Contact index not published".

Single contact validation: Tries the address server (full address matching) first, then falls back to the postcode database. Returns the contact even when addressStatus is invalid.

List Contacts

GET /e9v3/contacts

Pages the published ContactIndex. Optional search filters by name, business name, or email. limit is a page size (default 100, max 500). Omit cursor for the first page — existing clients that only send search/limit still work. When hasMore is true, pass nextCursor as cursor to continue.

Query Parameters

  • search (optional, string): Filter by contact name, business name, or email (case-insensitive)
  • limit (optional, integer): Page size (default 100, max 500)
  • cursor (optional, string): Previous page's nextCursor. Omit for the first page.

Example Request

GET /e9v3/contacts?search=acme&limit=100
Authorization: Bearer <jwt>

Example Response (200 OK)

{
  "success": true,
  "contacts": [
    {
      "contactId": "cust-123",
      "businessId": "biz-456",
      "name": "Acme Corporation",
      "businessName": "Acme Corporation",
      "email": "orders@acme.com",
      "phone": "+1-555-1234",
      "mobile": "+1-555-5678",
      "fax": "+1-555-9999",
      "website": "acme.com",
      "contactType": "customer",
      "status": "active",
      "priority": "high",
      "address": {
        "line1": "123 Main St",
        "line2": "Suite 200",
        "suburb": "Sydney",
        "state": "NSW",
        "postcode": "2000",
        "country": "Australia"
      },
      "business": {
        "abn": "12345678901",
        "acn": "987654321",
        "isGstRegistered": true
      },
      "metadata": {
        "notes": "Key account",
        "category": "wholesale",
        "tags": ["vip", "bulk-discount"],
        "createdAt": "2024-01-01T10:00:00Z",
        "updatedAt": "2024-01-15T14:30:00Z",
        "createdBy": "admin@equos.one",
        "updatedBy": "user@company.com"
      },
      "integration": {
        "externalId": "shopify-12345",
        "source": "shopify"
      },
      "addressStatus": "postcode_only"
    }
  ],
  "count": 1,
  "hasMore": false
}

Get Single Contact

GET /e9v3/contacts/:contactId

Retrieve full details for a specific contact by ID. Returns all contacts with full address validation details via the addressStatus field (uses addressr for full address matching when available).

Path Parameters

  • contactId (required, string): The contact's unique identifier

Example Request

GET /e9v3/contacts/cust-123
Authorization: Bearer <jwt>

Example Response (200 OK)

{
  "success": true,
  "contacts": [
    {
      "contactId": "cust-123",
      "businessId": "biz-456",
      "name": "Acme Corporation",
      "businessName": "Acme Corporation",
      "email": "orders@acme.com",
      "phone": "+1-555-1234",
      "address": {
        "line1": "123 Main St",
        "suburb": "Sydney",
        "state": "NSW",
        "postcode": "2000",
        "country": "Australia"
      },
      "addressStatus": "validated"
    }
  ],
  "count": 1
}

Example Response (404 Not Found)

{
  "success": false,
  "error": "Contact not found",
  "count": 0
}

Contact Response Fields

All contact responses include the following structure:

Contact Fields

FieldTypeRequiredDescription
contactIdstringYesUnique contact identifier
businessIdstring—Business ID this contact belongs to
namestringYesContact name (primary or business name)
businessNamestring—Official business name
emailstring*Email address (*required or phone/mobile)
phonestring*Phone number (*required or email/mobile)
mobilestring—Mobile number
faxstring—Fax number
websitestring—Website URL
contactTypestringYesType: customer, supplier, location, or agent
statusstringYesStatus: active, inactive, or archived
prioritystring—Priority: low, medium, high, or critical
addressStatusstringYesAddress validation status: validated (addressr full address match), postcode_only (postcode/suburb match), invalid (postcode mismatch), or unverified (validation unavailable)

Address Validation Statuses

StatusMeaning
validatedFull address matched against addressr (Australian address database). Highest confidence.
postcode_onlyPostcode/suburb combination exists in the Australian postcode database. Medium confidence.
invalidPostcode/suburb combination not found in any database. Contact is excluded from list endpoint.
unverifiedAddress validation database unavailable at startup. Contacts returned without validation.

Address Object

FieldTypeRequiredDescription
line1stringYes*Street address line 1 (*required for complete contact)
line2string—Street address line 2
suburbstringYes*Suburb/city (*required for complete contact)
statestring—State/province
postcodestringYes*Postal code (*required for complete contact)
countrystring—Country (default: Australia)

Business Object

FieldTypeDescription
abnstringAustralian Business Number
acnstringAustralian Company Number
isGstRegisteredbooleanWhether the contact is GST registered

Metadata Object

FieldTypeDescription
notesstringContact notes
categorystringContact category
tagsarrayArray of tag strings
rolestringRole (for agents)
createdAtstring (RFC3339)Contact creation timestamp
updatedAtstring (RFC3339)Last update timestamp
createdBystringUser who created the contact
updatedBystringUser who last updated the contact

Integration Object

FieldTypeDescription
externalIdstringExternal system record ID (e.g., Shopify customer ID)
sourcestringSource integration (e.g., "shopify", "airtable")

Shopper Data API (GDPR)

Export or erase what EQUOS holds about one shopper, for Shopify's mandatory privacy webhooks (customers/data_request, customers/redact, shop/redact) or any equivalent request. Everything is limited to the key's business. Each call is synchronous, bounded, idempotent and writes one audit row (key, request id, which inputs were given, matched contact and order ids, statuses and counts; never the personal data).

Matching

Send at least one of externalRef, email, phone. Inputs are ORed; every matched contact is reported with matchedBy.

  • externalRef: exact match of {provider}:{store}:customer:{id} (e.g. shopify:acme.myshopify.com:customer:7012345), the ref send-matched stores. Erased contacts keep a SHA-256 of the ref, so repeat calls by ref report already_erased.
  • email: case-insensitive.
  • phone: digits only, last 10 digits, AU +61 and 0 forms are equal. Fewer than 8 digits and 1300/1800/1900 numbers are rejected (400).

More than 25 matching contacts returns 422 and changes nothing. No match is a 200 (no_match / count: 0).

POST/e9v3/contacts/exportExport a shopper's personal data
🔒 Bearer (scope: contacts:read)

Body: { externalRef?, email?, phone?, requestId?, reason? }. Returns each matched contact (contact record, externalRefs, status) and, when the key may read orders (orders:read: all types, orders:write: send orders), each order's summary, the personal fields stored on it (personalData) and its lines. Read-only.

POST/e9v3/contacts/eraseErase a shopper's personal data
🔒 Bearer (scope: contacts:write)

Body: { externalRef?, email?, phone?, requestId?, reason?, dryRun? }. Blanks the contact's name, email, phone, address fields and notes, removes its external refs and archives it; blanks the shopper's name, email, phone and addresses on their orders (customer or supplier role), on freight consignment receiver details and on API-imported order notes; scrubs settled order jobs and stored webhook payloads. Order numbers, references, status, dates, totals, metadata and lines are kept. Response: outcome erased | already_erased | no_match | would_erase | partial, plus one result per contact. Repeat on partial.

POST/e9v3/contacts/erase/shopErase every shopper of a Shopify shop
🔒 Bearer (scope: contacts:write)

Body: { shop, limit? (1–100, default 25), cursor?, requestId?, reason?, dryRun? }. Erases one page of contacts linked to shopify:{shop}:customer:*; call again with nextCursor until complete is true. A contact also linked to another shop is only unlinked (status unlinked): its refs for this shop are removed and only this shop's orders are blanked.

POST /e9v3/contacts/erase
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "externalRef": "shopify:acme.myshopify.com:customer:7012345",
  "email": "jordan.lee@example.com",
  "requestId": "shopify-webhook-5f0c",
  "reason": "customers/redact"
}

200 OK
{
  "success": true,
  "outcome": "erased",
  "requestId": "shopify-webhook-5f0c",
  "auditId": "813",
  "dryRun": false,
  "matched": 1,
  "contacts": [
    {
      "contactId": "con_8f3a2b1c",
      "matchedBy": ["email", "externalRef"],
      "status": "erased",
      "ordersMatched": 3,
      "ordersErased": 3,
      "ordersRemaining": false,
      "jobsScrubbed": 1,
      "jobsInFlight": 0,
      "refsRemoved": 1
    }
  ],
  "totals": { "contacts": 1, "ordersErased": 3, "jobsScrubbed": 1, "webhookLogsRedacted": 4, "shopifyDeliveriesRedacted": 2 }
}

Inventory API

Read stock levels, and declare absolute on-hand quantities from an external source of truth.

GET/e9v3/inventoryList inventory
🔒 Bearer (scope: inventory:read)

Lists active inventory items with availableQuantity and totalQuantity. Query parameters: search (SKU / product name / category / barcode), limit (default 100, max 500 per page), cursor (optional — omit for the first page). Existing clients that only send search/limit still get the first page; hasMore and nextCursor are additive. Loop while hasMore is true, passing nextCursor as cursor, to fetch the full catalog.

GET/e9v3/inventory/{itemId}Get inventory item
🔒 Bearer (scope: inventory:read)

Returns a single inventory item with stock levels. The response uses the SAME shape as the list endpoint — { success, inventory: [item], count: 1 } — there is no top-level item object. An unknown itemId returns 404 { success: false, error: 'Inventory item not found' }.

POST/e9v3/inventory/syncDeclare on-hand quantity
🔒 Bearer (scope: inventory:write)

Sets (not adjusts) the absolute on-hand quantity for a SKU. Applied through the platform's transactional inventory pipeline as an asynchronous job — returns 200 { success: true, jobId }. Body: { sku, quantity, sourceRecordId?, metadata? }.

Notes
  • Validates the SKU: an unknown SKU returns 404 (unlike the order writes, which do not check SKUs up front)
  • Idempotent: retries with the same sourceRecordId are deduplicated via a deterministic job id
  • Absolute, not delta: quantity is the new on-hand count, not an adjustment
POST/e9v3/inventory/itemsUpsert catalog item
🔒 Bearer (scope: inventory:write)

Creates or updates a catalog item keyed by SKU (Firestore Inventory + Postgres metadata). Does not change stock levels. Idempotent on sourceRecordId+SKU. Returns 409 when the SKU belongs to another sourceRecordId. SKU max length 256. active:false archives the item.

POST/e9v3/inventory/items/batchBatch upsert catalog items
🔒 Bearer (scope: inventory:write)

Up to 50 items per request with per-item results (partial success). Same fields as the single upsert route.

POST/e9v3/inventory/items/{sku}/archiveArchive catalog item
🔒 Bearer (scope: inventory:write)

Archives a catalog item by SKU (InventoryArchive job). Never deletes stock or hard-deletes documents.

Catalog API

Serve the published, quantity-free product feed for storefronts and static site generators — with feed-tier prices merged in by effective SKU.

GET/e9v3/catalogGet a catalog feed batch
🔒 Bearer (scope: inventory:read)

Returns one batch of the published B2B catalog snapshot (500 items per batch). Query parameter: batch (zero-based, default 0; non-integer or negative → 400). This is a snapshot read — it does not touch live inventory. Iterate batch=0..version.batchCount-1 to page the whole feed.

Notes
  • Effective SKU: each item's sku is the parent SKU; variantSku (when present) is the orderable variant. The price join key is variantSku ?? sku.
  • Prices: when a public feed tier is configured (Console → Pricing → Public feed tier) and published, price and gst are merged per item and a top-level pricing block names the tier. Omitted when no tier is set or it is unpublished.
  • ETag / revalidation: responses carry a strong ETag; send it back in If-None-Match to get 304 Not Modified (empty body) when the feed is unchanged. Cache-Control: no-cache.
  • 404 bodies: { success: false, error: "Catalog not published" } when nothing has been published; { success: false, error: "Batch out of range" } when batch >= version.batchCount.
  • Rebuilds: subscribe to the catalog.published and prices.published webhooks to re-pull the feed when it changes.

Manufacturing API

GET/e9v3/manufacturingList manufacturing tasks
🔒 Bearer (scope: orders:read)

Lists manufacturing tasks of one type. Query parameters: type (optional, default create; one of create, assembly, quality, batch), archived (boolean, default false — set true to list archived tasks), status (filter by derived status: pending, finalized, committed, completed, voided), search (task number / description), limit (default 100, max 500). status and search are filtered in-memory over the first limit+1 documents; there is no cursor pagination.

GET/e9v3/manufacturing/{taskType}/{taskId}Get manufacturing task
🔒 Bearer (scope: orders:read)

Returns a single manufacturing task.

Address API

GET/e9v3/addresses/searchSearch Australian addresses
🔒 Bearer (scope: contacts:read)

Validates and autocompletes Australian addresses against G-NAF. Query parameter: q (the partial address).

Secure Documents API

Upload a document and get back a hosted, shareable link. Synchronous — returns the URL directly, with no job to poll.

POST/e9v3/secure_doc_deliveryUpload a document for secure delivery
🔒 Bearer (scope: orders:write)

Uploads one or more documents as multipart/form-data and returns a hosted, shareable link. Unlike the order and inventory writes, this is synchronous: it responds 201 { document_url } directly — there is no job to poll. Document upload reuses the orders:write scope.

Form Fields (multipart/form-data)
  • files (binary, required, repeatable) — one or more files to upload; repeat the part to send several in one request. A legacy single file part is also accepted.
  • order_id (text, optional) — order reference; labels the stored document (Order {order_id}) and is recorded as its task number.
Constraints
  • At least one file is required.
  • PDF and CSV only — any other type is rejected with 400 invalid_file_type.
  • Combined size ≤ 32 MiB across all parts (SDD_MAX_UPLOAD_BYTES).
Success (201 Created)
{
  "document_url": "https://docs.equos.one/r/a1b2c3"
}

document_url is absolute ({publicBaseURL}/r/{shortCode}) when a public base URL is configured, otherwise root-relative (/r/{shortCode}).

Errors

Error bodies use the shape { error, code }.

  • 400: parse_error (malformed multipart), file_required (no file part), invalid_file_type (a non-PDF/CSV file)
  • 401: missing or invalid Bearer token
  • 429: rate limit exceeded (1 request/sec per key, burst 10)
  • 500: shortcode_error, create_link_error, file_open_error, upload_error