Delivery and Shipping

These endpoints cover everything that happens after an order has been processed: recording the delivery, confirming it, uploading evidence (photos/documents), attaching information for an external shipment, and managing its cost. For the fulfillment/shipping preparation itself, see Fulfillment.

Response envelope

Every successful single-resource response follows { "data": {...} }. Errors are always { "error": { "code", "message", "details?" } } with namespace:snake_case codes.

Mark as delivered

Marks the order as delivered. The delivery date is optional — if you omit it, the server's time is used.

orderIdstringrequired

Order ID

deliveredAtstring

ISO 8601 date/time of the delivery. If omitted, the moment the request arrives is used.

{
  "deliveredAt": "2026-07-20T18:30:00.000Z"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "delivered",
    "_links": {
      "returns": "/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns"
    }
  }
}

Required permission: orders:update

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/mark-delivered \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"deliveredAt":"2026-07-20T18:30:00.000Z"}'

Confirm delivery

Formally confirms an order's delivery, with optional notes.

orderIdstringrequired

Order ID

notesstring

Notes associated with the delivery confirmation.

{
  "notes": "Recibido por el cliente en recepción"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "delivered",
    "_links": {}
  }
}

Required permission: orders:fulfill

curl -X PUT https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/confirm-delivery \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"notes":"Recibido por el cliente en recepción"}'

Upload delivery evidence

Registers delivery evidence (photos, documents) associated with the order.

orderIdstringrequired

Order ID

evidenceUrlstringrequired

URL of the evidence (photo or document). Must be a valid URL.

evidenceTypestring

Type of evidence (free text, e.g. 'photo', 'signature').

notesstring

Notes associated with the evidence.

{
  "evidenceUrl": "https://fenicia-order-attachments-prod.s3.us-east-2.amazonaws.com/evidence/delivery-photo.jpg",
  "evidenceType": "photo",
  "notes": "Entregado en la puerta principal"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "delivered",
    "_links": {}
  }
}

Required permission: orders:update

It doesn't upload the file — it only registers its URL

This endpoint is NOT a two-step upload flow like order attachments (S3 presigned URL + confirmation). evidenceUrl is the already-existing URL of the evidence (for example, a photo you uploaded yourself to your own storage or via the attachments flow) — this endpoint just associates it with the order; it doesn't host the file.

Attach an external shipping label

Attaches to the order the data of a shipping label generated outside Fenicia (for example, purchased directly with the carrier).

orderIdstringrequired

Order ID

trackingNumberstringrequired

Tracking number of the label.

carrierstringrequired

Carrier code.

trackingUrlstring

Public tracking URL.

{
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "fedex"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "fulfilled",
    "_links": {}
  }
}

Required permission: orders:update

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/upload-guide \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"trackingNumber":"1Z999AA10123456784","carrier":"fedex"}'

Attach shipment information

Attaches to the order the complete information of a shipment: carrier, cost, tracking label, estimated dates, origin/destination, and package dimensions.

orderIdstringrequired

Order ID

trackingNumberstringrequired

Tracking number.

carrierstringrequired

Carrier code.

shipmentIdstring

Shipment ID, if one already exists to update.

waybillIdstring

Waybill ID.

carrierNamestring

Visible carrier name.

serviceNamestring

Shipping service name (e.g. 'Express', 'Ground').

providerstring

Provider that originated the shipment (e.g. carrier aggregator).

providerShipmentIdstring

Shipment ID in the provider's system.

costobject

Shipment cost: { providerCost, feniciaFee, totalCost, currency }.

trackingUrlstring

Public tracking URL.

labelUrlstring

URL of the PDF label.

labelS3Keystring

S3 key where the label lives, if Fenicia stored it.

deliveryDaysnumber

Estimated transit days.

estimatedDeliveryDatestring

Estimated delivery date (ISO 8601).

purchasedAtstring

Date the shipment was purchased (ISO 8601).

printingFormatstring

'standard' or 'thermal'.

originobject

Shipment origin address.

destinationobject

Shipment destination address.

packageobject

Package dimensions: { weight, weightUnit, length, width, height, dimensionUnit }.

{
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "fedex",
  "cost": { "providerCost": 180.00, "feniciaFee": 15.00, "totalCost": 195.00, "currency": "MXN" }
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "fulfilled",
    "fulfillmentMode": "fenicia-shippments",
    "_links": {}
  }
}

Required permission: orders:update

Shape of cost (optional)

FieldTypeDescription
providerCostnumberCost charged by the shipping provider.
feniciaFeenumberFenicia's commission on the shipment.
totalCostnumberTotal cost (providerCost + feniciaFee, unless adjusted).
currencystring3-letter currency code (e.g. MXN).

Shape of package (optional)

FieldTypeDescription
weightnumberPackage weight.
weightUnitstringWeight unit (e.g. kg).
length / width / heightnumberDimensions.
dimensionUnitstringDimension unit (e.g. cm).

carrier === 'other'

If carrier is "other", check whether your integration requires a descriptive name — use carrierName for that.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/attach-shipment \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "trackingNumber": "1Z999AA10123456784",
    "carrier": "fedex",
    "cost": { "providerCost": 180.00, "feniciaFee": 15.00, "totalCost": 195.00, "currency": "MXN" }
  }'

Shipment cost

Updates or sets the cost of a specific shipment within the order.

orderIdstringrequired

Order ID

shipmentIdstringrequired

Shipment ID within the order

amountnumberrequired

Amount paid for the shipment (≥ 0).

currencystringrequired

3-letter ISO 4217 currency (e.g. MXN).

{
  "amount": 195.00,
  "currency": "MXN"
}
200
{
  "data": {
    "orderId": "65f3a1b2c4d5e6f7a8b9c0d1",
    "shipmentId": "65f3a1b2c4d5e6f7a8b9c0e2",
    "shippingCost": { "amount": 195.00, "currency": "MXN" }
  }
}

Required permission: orders:update

This endpoint is only the cost — it doesn't replace attach-shipment

PUT .../shipping-cost only captures or updates the amount paid for a shipment that already exists on the order (identified by shipmentId); it doesn't create the shipment nor accept attach-shipment's other fields (label, carrier, dates). Each operation emits a domain event that lambda-finance-events reflects as an Expense in @fenicia/finance-service.

Removes the cost previously assigned to a shipment on the order.

orderIdstringrequired

Order ID

shipmentIdstringrequired

Shipment ID within the order

200
{
  "data": {
    "orderId": "65f3a1b2c4d5e6f7a8b9c0d1",
    "shipmentId": "65f3a1b2c4d5e6f7a8b9c0e2",
    "removed": true
  }
}

Required permission: orders:update

Idempotent: removed indicates whether there was a prior cost

If the shipment already had no cost assigned, the call still responds 200 with removed: false — that's not an error, it's intentional idempotency (event consumers may deliver the same removal twice). A shipmentId that doesn't exist on the order does return orders:shipment_not_found (404).

curl -X DELETE https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/shipments/65f3a1b2c4d5e6f7a8b9c0e2/shipping-cost \
  -H "Authorization: Bearer fkapi_your_api_key"

Download an order's shipping labels

Returns presigned S3 URLs to download the order's shipping labels. Each PDF is persisted as an order attachment (the first time) or served from S3 cache on subsequent calls.

orderIdstringrequired

Order ID

200
{
  "data": {
    "orderId": "65f3a1b2c4d5e6f7a8b9c0d1",
    "totalLabels": 1,
    "labels": [
      {
        "identifier": "T1-2026-04-27-00042",
        "downloadUrl": "https://fenicia-order-attachments-prod.s3.us-east-2.amazonaws.com/attachments/...?X-Amz-Signature=...",
        "filename": "t1-T1-2026-04-27-00042_shipping_label.pdf",
        "contentType": "application/pdf",
        "size": 51234,
        "expiresIn": 900,
        "source": "live",
        "carrier": "T1-via-Estafeta",
        "attachmentId": "65f3a1b2c4d5e6f7a8b9c0e5"
      }
    ],
    "processingTimeMs": 432
  }
}

Required permission: orders:read

The PDF never travels as base64

It's persisted as an order attachment in S3 and the API responds with a presigned URL (TTL ~15 min). Download the PDF directly from that URL — you don't need your API key for that second step. Repeated calls for the same order reuse the cached attachment.

LABEL_URL=$(curl -s https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/download-shipping-label \
  -H "Authorization: Bearer fkapi_your_api_key" \
  | jq -r '.data.labels[0].downloadUrl')
 
curl -o label.pdf "$LABEL_URL"

Tip

For high-volume batch printing, use POST /orders/fulfillment/labels (see Fulfillment) with several order IDs at once.

Errors

CodeStatusDescription
orders:not_found404No order exists with that ID in your tenant.
orders:cannot_confirm_delivery422The order isn't in a state from which delivery can be confirmed.
orders:shipment_not_found404No shipment exists with that ID within the order.
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.
validation:invalid_input / validation:missing_field400The body doesn't satisfy the expected schema, or a required field is missing.
auth:permission_denied403The API key doesn't have the required permission.
auth:invalid_token401The API key is invalid or has been revoked.

Next steps