# Fulfillment Pre-Processor Integration API **Status:** Pull endpoints live; pushback endpoint rolling out — confirm availability with your OmniCart contact before go-live **Audience:** Fulfillment pre-processor vendor / integration engineers **Last Updated:** 2026-07-23 The fulfillment pre-processor is an external service that, on a cron schedule: 1. **Pulls** orders awaiting fulfillment from OmniCart. 2. Processes them (routing, rate shopping, batching, label prep — vendor-side). 3. **Pushes back** fulfillment results: current fulfillment state and tracking. OmniCart records the pushback as OmniCart fulfillments (tracking labels, shipped/delivered timestamps), which drives the fulfillment dashboard, shipping-confirmation emails, and downstream Konnektive sync. --- ## 1. Instructions ### 1.1 Authentication Use a **secret API key** (server-to-server; recommended over JWT logins — keys don't expire on a timer and can be revoked individually). Create one in the admin dashboard under **Settings → API Key Management** (secret keys start with `sk_`). Send it as HTTP Basic auth — the key is the username, the password is empty: ``` Authorization: Basic base64("sk_YOUR_SECRET_KEY:") ``` ```bash curl -u "sk_YOUR_SECRET_KEY:" https://your-store.omnicart.cc/admin/fulfillment/health ``` ### 1.2 Base URL ``` https://your-store.omnicart.cc # replace with the store domain from onboarding ``` ### 1.3 Cron flow | Step | Endpoint | Purpose | |------|----------|---------| | 1. Pull queue | `GET /admin/fulfillment?fulfillment_filter=awaiting` | Page through orders with unfulfilled items | | 2. Pull detail (optional) | `GET /admin/fulfillment/:id` | Full order: line items + SKUs, quantities, shipping address | | 3. Pushback | `POST /admin/fulfillment/preprocessor/update` | Report fulfillment state + tracking (batch) | **Recommended cadence:** pull every 15–30 minutes; pushback as soon as state changes (or batched on the same cron). Always send `no_cache=true` on the pull — the list endpoint has a 3-minute server-side cache intended for the dashboard UI. ### 1.4 Ground rules - **Prices are decimals, not cents.** `19.99` means $19.99 everywhere in this API. - **Order references:** the human-facing order number is `display_id` (numeric). Pushback also accepts the internal `order_id` (`order_…`) — preferred when you have it. - **Idempotency:** pushback is idempotent per `tracking_number` *within an order* — re-sending a tracking number the order already has never creates a duplicate fulfillment. A re-send that adds new information (e.g. `delivered_at`) updates the existing fulfillment; a re-send with nothing new is also reported `updated`, and the order is additionally flagged for manual review (`needs_review`) so repeated re-sends are visible to operations. Concurrent submissions for the same order are serialized by a per-order lock, so retrying a whole batch after a network failure is safe. - **Split shipments:** every pushback line must list the `items` (SKU + quantity) it covers **unless** the order has exactly one line item. Multi-item orders without an `items` array are rejected — OmniCart will not guess which items shipped. - **Partial fulfillment is supported:** send one pushback entry per physical shipment, each with its own tracking number and item subset. - **Batch limits:** max 10,000 update entries per pushback request (matches the platform's existing bulk-import cap). Pull pagination is `limit`/`offset` — there is no fixed page-size ceiling, but keep pages ≤ 500 for response-time reasons. - **Quantity capping:** a shipped quantity above the item's unfulfilled quantity is **silently capped** to what remains unfulfilled — it is not rejected. Don't rely on OmniCart to catch over-shipment claims; validate quantities on your side. - **Dry run:** send `"dry_run": true` in the pushback body for a no-write preview — entries are matched and per-entry results predicted, but nothing is persisted. Use this during vendor onboarding. (It is a preview, not a full validator.) - **Side effects to know about:** a pushback SKU that doesn't match any order item creates a `needs_review` SKU-alias entry for operations to map; orders whose shipping name is "Test Test" are auto-held and reported `skipped`. ### 1.5 Fulfillment status model The pre-processor reports one of these `status` values per order (lifecycle order): | Status | Meaning | Effect in OmniCart | |--------|---------|--------------------| | `received` | Pre-processor has ingested the order | Stamps metadata only (visible in dashboard) | | `processing` | Picked / packed / label pending | Stamps metadata only | | `shipped` | Handed to carrier — tracking required | Creates/updates fulfillment, sets `shipped_at`, records tracking label, triggers shipping-confirmation email (only when `shipped_at` is within the last 48 hours — late/backfill pushes do not email) | | `delivered` | Carrier confirmed delivery | Sets `delivered_at` on the matching fulfillment | | `exception` | Problem (address, stock, damage) | Flags order for review; include `message` | | `cancelled` | Pre-processor will not fulfill | Flags order; no fulfillment created | `shipped` and `delivered` require a `tracking_number`. `exception` and `cancelled` should include a human-readable `message`. > **Implementation scope:** `shipped`/`delivered` ride on OmniCart's existing, > battle-tested fulfillment engine. The four non-shipping statuses (`received`, > `processing`, `exception`, `cancelled`) are metadata-only stamps implemented in the > pushback endpoint itself — treat them as phase 2 if the vendor can go live with > shipped/delivered only. --- ## 2. API Reference ### 2.1 Pull — list orders awaiting fulfillment ``` GET /admin/fulfillment ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `fulfillment_filter` | string | Use `awaiting` — orders not yet shipped (no shipped fulfillment, not WMS-shipped) that still have line items; excludes on-hold, cancelled, refunded/declined/partial, sync-error, and needs-review orders. Other values: `shipped`, `on_hold`, `cancelled`, `all` | | `limit` | number | Page size (default 20) | | `offset` | number | Page start | | `created_at_gte` / `created_at_lte` | ISO 8601 | Window on order-placed date | | `sales_channel_id` | string | Restrict to a sales channel | | `no_cache` | string | `true` — bypass the 3-minute dashboard cache (**always set this from the cron**) | **Response** — `200 OK` ```json { "orders": [ { "id": "order_01JGXA8Y2K3M4N5P6Q7R8S9T0V", "display_id": 4521, "email": "jane.doe@example.com", "created_at": "2026-07-22T18:41:03.000Z", "currency_code": "usd", "total": 89.94, "status": "pending", "payment_status": "captured", "fulfillment_status": "not_fulfilled", "is_shipped": false, "shipped_at": null, "last_fulfillment_update_at": null, "customer_name": "Jane Doe", "sales_channel": { "id": "sc_01H...", "name": "VNSH Storefront" }, "metadata": { "konnektive_order_id": "25C1D4E9F0" } } ], "count": 137, "limit": 100, "offset": 0 } ``` Page until `offset + limit >= count`. Notes: - Responses include additional dashboard-oriented fields not shown above (`shipping_email_sent`, `confirmation_email_at`, `quickbox_*`, …) — **ignore unknown fields**; don't use strict schema validation. - The list view does **not** expose per-item fulfillment state. To know which items and quantities remain unfulfilled, call the detail endpoint (§2.2). - On failure these endpoints return `500` with `{ "message", "request_id", "stage" }` — log the `request_id` and include it when reporting issues. Responses also carry `X-Request-Id` and `X-Cache` headers useful for debugging. ### 2.2 Pull — order detail (items, SKUs, address) ``` GET /admin/fulfillment/:id # :id = internal order id (order_…) ``` `:id` must be the internal `order_…` id from the list response — a bare numeric `display_id` returns `404`. **Response** — `200 OK` (abridged to the fields the pre-processor needs) ```json { "order": { "id": "order_01JGXA8Y2K3M4N5P6Q7R8S9T0V", "display_id": 4521, "email": "jane.doe@example.com", "currency_code": "usd", "total": 89.94, "subtotal": 79.99, "shipping_total": 9.95, "items": [ { "id": "ordli_01JGXA8Y2K...", "title": "VNSH Holster - 20rd Extended", "variant_sku": "4011-VNSH-20EXT", "quantity": 2, "fulfilled_quantity": 0, "unfulfilled_quantity": 2, "unit_price": 39.99, "requires_shipping": true } ], "shipping_address": { "first_name": "Jane", "last_name": "Doe", "address_1": "123 Main St", "address_2": "Apt 4B", "city": "Austin", "province": "TX", "postal_code": "78701", "country_code": "us", "phone": "+1 512 555 0134" }, "fulfillments": [] } } ``` Only fulfill items where `requires_shipping` is `true` and `unfulfilled_quantity > 0`. ### 2.3 Pushback — report fulfillment state + tracking ``` POST /admin/fulfillment/preprocessor/update Content-Type: application/json ``` **Request body** ```json { "source": "acme-preprocessor", "dry_run": false, "updates": [ { "order_id": "order_01JGXA8Y2K3M4N5P6Q7R8S9T0V", "reference": "4521", "status": "shipped", "tracking_number": "9400111899223334445551", "carrier": "USPS", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223334445551", "shipped_at": "2026-07-23T14:05:00Z", "delivered_at": null, "items": [ { "sku": "4011-VNSH-20EXT", "quantity": 2 } ], "warehouse_order_id": "WH-88123", "message": null, "metadata": {} } ] } ``` **Field reference (per update entry)** | Field | Type | Required | Description | |-------|------|----------|-------------| | `order_id` | string | one of `order_id`/`reference` | Internal OmniCart order id (`order_…`) — preferred | | `reference` | string | one of `order_id`/`reference` | Order `display_id` (numeric string, e.g. `"4521"`) | | `status` | enum | yes | `received` \| `processing` \| `shipped` \| `delivered` \| `exception` \| `cancelled` | | `tracking_number` | string | for `shipped`/`delivered` | Carrier tracking number — also the idempotency key | | `carrier` | string | recommended | e.g. `USPS`, `UPS`, `FedEx`, `DHL`. **Always send it when known.** When omitted, USPS/UPS/FedEx are inferred from the tracking-number format; unrecognized formats (including DHL) default to `USPS` | | `tracking_url` | string | no | Reserved. OmniCart generates the tracking URL from carrier + tracking number; a supplied value is currently ignored | | `shipped_at` | ISO 8601 | no | Handoff-to-carrier time (defaults to now for `shipped`) | | `delivered_at` | ISO 8601 | for `delivered` | Carrier delivery confirmation time | | `items` | array | for multi-item orders | Shipped lines: `{ "sku": string, "quantity": number }`. Omit only on single-item orders | | `warehouse_order_id` | string | no | Pre-processor's internal order reference, stored in order metadata | | `message` | string | for `exception`/`cancelled` | Human-readable reason, shown in the dashboard | | `metadata` | object | no | Free-form key/values merged into fulfillment metadata | **Response** — `200 OK` (per-entry results; the batch never fails atomically) ```json { "success": true, "dry_run": false, "summary": { "created": 1, "updated": 0, "skipped": 0, "errors": 0 }, "results": [ { "reference": "4521", "order_id": "order_01JGXA8Y2K3M4N5P6Q7R8S9T0V", "status": "created", "message": "Fulfillment created with tracking 9400111899223334445551", "tracking_number": "9400111899223334445551" } ] } ``` Per-entry `status` values: `created` (new fulfillment), `updated` (existing fulfillment enriched — e.g. delivery date added — **or** a re-send of a tracking number the order already has; a pure re-send with nothing new also flags the order for manual review), `skipped` (no-op — e.g. a test order auto-held, or a race where the items were already fulfilled), `error` (see `message`; other entries still process). **Error responses** | Code | Meaning | |------|---------| | `400` | Malformed body / missing required fields / >10,000 entries | | `401` | Missing or invalid API key | | `404` | (Per-entry, reported in `results`) order not found | | `422` | (Per-entry) validation failure — e.g. multi-item order without `items`, or SKU not on the order. Note: a quantity above the unfulfilled amount is **not** rejected — it is silently capped (§1.4) | | `429` | Rate limited — retry with backoff | Retry guidance: retry `5xx` and `429` with exponential backoff. Do **not** blind-retry `4xx`. Because pushback is idempotent per tracking number, retrying a whole batch after a network failure is safe. --- ## 3. Examples ### 3.1 Cron pull — first page of the awaiting queue ```bash curl -u "sk_YOUR_SECRET_KEY:" \ "https://your-store.omnicart.cc/admin/fulfillment?fulfillment_filter=awaiting&limit=100&offset=0&no_cache=true" ``` Then fetch details for each order id returned: ```bash curl -u "sk_YOUR_SECRET_KEY:" \ "https://your-store.omnicart.cc/admin/fulfillment/order_01JGXA8Y2K3M4N5P6Q7R8S9T0V" ``` ### 3.2 Pushback — single shipment, whole order shipped ```bash curl -u "sk_YOUR_SECRET_KEY:" \ -X POST "https://your-store.omnicart.cc/admin/fulfillment/preprocessor/update" \ -H "Content-Type: application/json" \ -d '{ "source": "acme-preprocessor", "updates": [ { "reference": "4521", "status": "shipped", "tracking_number": "9400111899223334445551", "carrier": "USPS", "shipped_at": "2026-07-23T14:05:00Z", "items": [ { "sku": "4011-VNSH-20EXT", "quantity": 2 } ] } ] }' ``` ### 3.3 Pushback — split shipment (two boxes, two tracking numbers) Order `#4522` has three line items; item C ships separately: ```json { "source": "acme-preprocessor", "updates": [ { "reference": "4522", "status": "shipped", "tracking_number": "1Z999AA10123456784", "carrier": "UPS", "shipped_at": "2026-07-23T15:10:00Z", "items": [ { "sku": "4011-VNSH-20EXT", "quantity": 1 }, { "sku": "4012-VNSH-BELT", "quantity": 1 } ] }, { "reference": "4522", "status": "shipped", "tracking_number": "1Z999AA10123456791", "carrier": "UPS", "shipped_at": "2026-07-23T15:10:00Z", "items": [ { "sku": "4020-VNSH-MAGPOUCH", "quantity": 2 } ] } ] } ``` ### 3.4 Pushback — mixed batch: delivery confirmation + exception ```json { "source": "acme-preprocessor", "updates": [ { "reference": "4521", "status": "delivered", "tracking_number": "9400111899223334445551", "delivered_at": "2026-07-25T19:42:00Z" }, { "reference": "4530", "status": "exception", "message": "Address undeliverable — apartment number missing. Holding at warehouse." } ] } ``` Response: ```json { "success": true, "dry_run": false, "summary": { "created": 0, "updated": 2, "skipped": 0, "errors": 0 }, "results": [ { "reference": "4521", "status": "updated", "message": "Delivery date recorded on existing fulfillment", "tracking_number": "9400111899223334445551" }, { "reference": "4530", "status": "updated", "message": "Order flagged for review: Address undeliverable — apartment number missing. Holding at warehouse." } ] } ``` Note on `delivered`: send the `shipped` pushback (same tracking number) first. A `delivered` entry whose tracking number is unknown on the order creates the fulfillment and marks it delivered in one step — valid, but you lose the distinct ship event. ---