Fulfillment and Shipping

Fulfillment endpoints cover the path from an accepted order to a package ready to go out: preparation, carrier selection, label generation, and tracking updates. There are two families:

  • Per order (/orders/{orderId}/prepare, /orders/{orderId}/fulfill): built for interactive flows, one order at a time.
  • Batch (/orders/fulfillment/*): built to process many orders in a single call, typically from the merchant dashboard or a scheduled task.

Response envelope

Every successful single-resource response follows { "data": {...} }; lists use { "data": [...], "meta": { "pagination": {...} } }. Errors are always { "error": { "code", "message", "details?" } } with namespace:snake_case codes (e.g. orders:cannot_fulfill).

For post-shipment events (marking as delivered, uploading delivery evidence, attaching an external label) see Delivery and Shipping.

Mark an order as preparing

Marks an order as preparing. Requires specifying which items and how much of each are being prepared in this batch.

orderIdstringrequired

Order ID

itemsarrayrequired

Items to prepare in this batch. Minimum 1 element.

shippingInfoobject

Shipping information to associate with this preparation (optional).

{
  "items": [{ "itemIndex": 0, "quantityToPrepare": 2 }]
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "preparing",
    "_links": {
      "fulfill": "/orders/65f3a1b2c4d5e6f7a8b9c0d1/fulfill",
      "cancel": "/orders/65f3a1b2c4d5e6f7a8b9c0d1/cancel"
    }
  }
}

Required permission: orders:fulfill

Shape of items

Each element identifies an order item by its position, not by SKU:

FieldTypeRequiredDescription
itemIndexnumberYesIndex (≥0) of the item within the order's items array.
quantityToPreparenumberYesQuantity of that item to prepare (> 0).

Shape of shippingInfo (optional)

FieldTypeRequiredDescription
carrierstringYes, if you send shippingInfoCarrier code.
trackingNumberstringYes, if you send shippingInfoTracking number.
trackingUrlstringNoPublic tracking URL.
customCarrierNamestringYes, if carrier is "other"Carrier name when it's not in the catalog.
shippingCostobjectNo{ amount: number, currency: string }.
providerstringNoShipping provider that originated the quote.

items is required

Despite what the name suggests, items is not optional: at least one element with itemIndex and quantityToPrepare is required. Sending an empty array or only a list of SKUs is rejected.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/prepare \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"itemIndex":0,"quantityToPrepare":2}]}'

Fulfill an order

Marks the indicated items as fulfilled. Does not accept tracking information directly.

orderIdstringrequired

Order ID

itemsstring[]required

Identifiers of the items to mark as fulfilled. Minimum 1 element.

{
  "items": ["65f3a1b2c4d5e6f7a8b9c0e0"]
}
204No body
(no content)

Required permission: orders:fulfill

204 No Content — no body

This endpoint always responds with 204 No Content and no body. It does not include trackingNumber or carrier in the request or the response — those fields don't exist on this endpoint. To attach tracking, use POST /orders/fulfillment/tracking or POST /orders/{orderId}/attach-shipment.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/fulfill \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"items":["65f3a1b2c4d5e6f7a8b9c0e0"]}'

Fulfillment capabilities (batch)

Returns, for each order, which fulfillment actions are available and which are blocked.

orderIdsstring[]required

Order IDs to inspect. Minimum 1.

credentialsobject

Map of channelId → credentials, for orchestrated use. You typically don't need this if calling from outside Fenicia.

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1"]
}
200
{
  "data": {
    "capabilities": {
      "65f3a1b2c4d5e6f7a8b9c0d1": {
        "canDownloadLabel": true,
        "labelBlockedReason": null,
        "canUpdateTracking": true,
        "canMarkAsShipped": false,
        "fulfillmentType": "carrier",
        "channelType": "shopify"
      }
    },
    "orderCount": 1
  }
}

Required permission: orders:read

More fields than shown

Each capabilities object carries more flags beyond the ones shown above (around 13 fields per order in total). The ones listed here are confirmed; for the exhaustive list, check with support before depending on an undocumented field.

curl -X POST https://api.fenicia.io/orders/fulfillment/capabilities \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"orderIds":["65f3a1b2c4d5e6f7a8b9c0d1"]}'

Generate shipping labels (batch)

Generates shipping labels for one or more orders in a single call.

orderIdsstring[]required

Order IDs

credentialsobject

Map of channelId → credentials, for orchestrated use.

optionsobject

Generation options, nested (see below).

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1"],
  "options": { "format": "pdf", "size": "medium" }
}
200
{
  "data": {
    "labels": [ { "...": "..." } ],
    "summary": { "total": 2, "success": 2, "failed": 0, "skipped": 0 }
  }
}

Required permission: orders:fulfill

Shape of options (optional)

FieldTypeDescription
format'pdf' | 'zpl' | 'png'Output format of the label.
size'small' | 'medium' | 'large'Label size (relevant for thermal printers).

options is nested

format and size travel inside options, not at the root of the body: { "orderIds": [...], "options": { "format": "zpl", "size": "small" } }.

Shape of labels[] not audited in detail

summary is confirmed exactly as shown. The exact shape of each element in labels[] (and the optional mergedLabel field) was not audited field-by-field against the source code — confirm the exact contract with support before depending on a specific field inside each result.

curl -X POST https://api.fenicia.io/orders/fulfillment/labels \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"orderIds":["65f3a1b2c4d5e6f7a8b9c0d1"],"options":{"format":"pdf","size":"medium"}}'

Update tracking (batch)

Attaches or updates tracking information for one or more orders in a single call.

updatesarrayrequired

Array of updates, one per order (see shape below).

credentialsobject

Map of channelId → credentials, for orchestrated use.

{
  "updates": [
    { "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "tracking": { "trackingNumber": "1Z999AA10123456784", "carrierId": "fedex" } }
  ]
}
200
{
  "data": {
    "updated": 1,
    "failed": 0,
    "results": [
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "success": true }
    ]
  }
}

Required permission: orders:fulfill

Shape of each updates element

{
  "orderId": "65f3a1b2c4d5e6f7a8b9c0d1",
  "tracking": {
    "trackingNumber": "1Z999AA10123456784",
    "carrierId": "fedex",
    "carrierName": "FedEx",
    "trackingUrl": "https://fedex.com/track?n=1Z999AA10123456784"
  }
}

Inside tracking, only trackingNumber is required; carrierId, carrierName, and trackingUrl are optional.

This endpoint processes a batch, not a single order

The body is not { "orderId", "trackingNumber", "carrier" } at the root level — it's an updates[] array, and the tracking information is nested under the tracking key. The field carrier (on its own) does not exist on this endpoint.

curl -X POST https://api.fenicia.io/orders/fulfillment/tracking \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"updates":[{"orderId":"65f3a1b2c4d5e6f7a8b9c0d1","tracking":{"trackingNumber":"1Z999AA10123456784","carrierId":"fedex"}}]}'

Mark as shipped (batch)

Marks multiple orders as shipped in a single call.

orderIdsstring[]required

Order IDs to mark as shipped

credentialsobject

Map of channelId → credentials, for orchestrated use.

shipmentInfoobject

Shipping information to apply to all orders in the batch (see shape below).

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1"],
  "shipmentInfo": { "notifyCustomer": true }
}
200
{
  "data": {
    "shipped": 2,
    "failed": 0,
    "results": [
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "success": true, "markedAsShipped": true, "customerNotified": true },
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d2", "success": true, "trackingUpdated": true }
    ]
  }
}

Required permission: orders:fulfill

Shape of shipmentInfo (optional)

FieldTypeDescription
trackingobjectTracking information to apply.
shippedAtstring (ISO)Ship date/time to record.
notifyCustomerbooleanWhether to send a notification to the customer.

No carrier at the root

The root-level carrier field of the body does not exist. Carrier information, if applicable, goes inside shipmentInfo.tracking.

curl -X POST https://api.fenicia.io/orders/fulfillment/ship \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"orderIds":["65f3a1b2c4d5e6f7a8b9c0d1"],"shipmentInfo":{"notifyCustomer":true}}'

Available carriers

Returns the available carriers, depending on how you query it: by orders, by channel, or the default catalog.

orderIdsstring[]

Order IDs — if you send these, the response is resolved per order.

channelTypestring

Channel type — if you send this (without orderIds), the response is resolved per channel.

credentialsobject

Map of channelId → credentials, for orchestrated use.

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1"]
}
200
{
  "data": {
    "mode": "by-orders"
  }
}

Required permission: orders:read

Response shape discriminated by mode

All parameters are optional. The exact shape of the response depends on the resolved mode (by-orders, by-channel, or default) and was not audited field-by-field for each variant — the mode field is confirmed as the discriminator. Before parsing the response programmatically, validate the shape of each mode with support.

curl -X POST https://api.fenicia.io/orders/fulfillment/carriers \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"orderIds":["65f3a1b2c4d5e6f7a8b9c0d1"]}'

Errors

CodeStatusDescription
orders:not_found404No order with that ID exists in your tenant.
orders:cannot_fulfill422The order is not in a state from which it can be fulfilled.
orders:items_required400The body doesn't include items (or the array is empty).
orders:shipping_info_required400Required shipping information is missing for the operation.
orders:shipping_label_not_available422No label is available to generate/download in the current state.
orders:channel_unsupported422The order's channel doesn't support this fulfillment operation.
sync:integration_error / channels:sync_failed502Failure communicating with the channel's integration (carrier/marketplace).
sync:rate_limited429Rate limit reached against the external provider.
auth:permission_denied403The API key doesn't have the required permission.
auth:invalid_token401The API key is invalid or was revoked.
validation:invalid_input400The body doesn't satisfy the expected schema.

Error format

Every error is returned wrapped: { "error": { "code": "orders:cannot_fulfill", "message": "...", "details": {...} } }. Codes always follow namespace:snake_case.

Next steps