State Transitions

Fenicia models an order's lifecycle as a finite state machine: from a given state, only a specific set of destination states are valid. This article documents the complete machine (23 states) and all the endpoints that operate it.

Which endpoint should I use?

Use accept, reject, and cancel for the merchant's common actions — they're optimized for their use case and validate specific business rules (for example, the cancellation reason catalog). Use the generic transition endpoint when you need to move the order to any valid state in the machine that doesn't have a dedicated action.

The complete state machine

23 states. Each row indicates the states you can transition to from the current state; any other destination is rejected with orders:invalid_transition.

Current stateValid transitions to
draftnew, cancelled
new (alias of pending)accepted, cancelled, rejected
pending (alias of new)accepted, cancelled, rejected
acceptedprocessing, preparing, cancelled
processingpackaging, cancelled, fulfilled
preparingfulfilled, cancelled
packagingpickup-pending, fulfilled, cancelled
pickup-pendingin-transit, fulfilled
ready_for_pickupfulfilled
fulfilledin-transit, delivered, partially-delivered
shippedin-transit, delivered
in-transitdelivered, exception, not-delivered, partially-delivered
out_for_deliverydelivered, exception, not-delivered
deliveredreturned
completedreturned
partially-delivereddelivered, returned
partially-fulfilledfulfilled, in-transit, delivered
exceptionin-transit, delivered, not-delivered, cancelled
not-deliveredin-transit, cancelled, returned
problematiccancelled
cancelled(terminal — no outgoing transitions)
rejected(terminal — no outgoing transitions)
returned(terminal — no outgoing transitions)

Terminal states

cancelled, rejected, and returned are terminal states: an order that reaches any of them cannot transition to any other state.

Tip

new and pending are aliases of the same state — they have exactly the same valid transitions. You can use either interchangeably.

Available actions (HATEOAS)

When you retrieve an order, its _links object includes only the actions valid for its current state. The possible actions are: accept, reject, cancel, prepare, fulfill, returns. If an action doesn't appear in _links, it isn't valid for the order's current state.

Accept an order

Accepts a pending order. Requires orders:update permission.

orderIdstringrequired

Order ID.

{}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "accepted",
    "_links": { "self": "/orders/65f3a1b2c4d5e6f7a8b9c0d1" }
  }
}

No body required

The handler doesn't read any field from the request body — send {} or simply omit the body. If the order belongs to a channel that requires confirming the acceptance in the marketplace before accepting locally (today: Walmart), the acceptance is propagated automatically; you don't need to send anything for that.

Required permission: orders:update

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/accept \
  -H "Authorization: Bearer fkapi_your_api_key"

Reject an order

Rejects a pending order. Requires orders:cancel permission.

orderIdstringrequired

Order ID.

reasonstringrequired

One of the 12 canonical cancellation reasons — same catalog used by cancel (see table below).

motivestring

Specific child motive within the chosen reason. Must belong to reason.

notestring

Additional free-text context. Required when reason is other.

requestedBystring

Who requested the rejection: buyer, seller, or platform.

{
  "reason": "out_of_stock"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "rejected",
    "_links": {}
  }
}

Required permission: orders:cancel

Same reason catalog as cancel, strict validation

reject uses the same catalog of 12 canonical reasons as cancel (see the table in the "Cancel an order" section below) and the same rule: note is required when reason is other. Unlike cancel (which accepts extra fields without rejecting them), reject's validator is strict: sending any field other than reason, motive, note, or requestedBy is rejected.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/reject \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"reason": "out_of_stock"}'

Cancel an order

Cancels an order. Requires orders:cancel permission.

orderIdstringrequired

Order ID.

reasonstringrequired

One of the 12 canonical cancellation reasons (see table below).

motivestring

Specific child motive within the chosen reason.

notestring

Additional free-text context. Required when reason is other.

requestedBystring

Who requested the cancellation: buyer, seller, or platform.

{
  "reason": "customer_request",
  "requestedBy": "buyer",
  "note": "El cliente cambió de opinión"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "cancelled",
    "_links": {}
  }
}

Required permission: orders:cancel

Canonical cancellation reasons

reason
out_of_stock
fulfillment_unavailable
customer_request
shipping_issue
payment_declined
fraud_suspected
product_issue
pricing_error
duplicate_order
merchant_decision
system_error
other

note is required when reason is other

If you send reason: "other", the note field becomes required — describe the actual reason for the cancellation.

requestedBy accepts exactly one of these three values: buyer, seller, platform.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/cancel \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "customer_request",
    "requestedBy": "buyer",
    "note": "El cliente cambió de opinión"
  }'

Set status directly

Sets the order's status without going through a named action. Useful when integrating a flow that already knows the exact destination state.

Directly sets an order's status. Requires orders:update permission.

orderIdstringrequired

Order ID.

statusstringrequired

Target status.

{
  "status": "preparing"
}
204No body
(no content)

Required permission: orders:update

204 No Content — no body

Unlike accept/reject/cancel/transition, this endpoint responds with 204 No Content: it doesn't validate the transition against the state machine (don't confuse it with transition, which does validate) and it doesn't return the updated order. If you need the resulting order, do a GET /orders/{orderId} afterward.

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

Generic transition

Transitions an order to the given state, validating the state machine. Requires orders:update permission.

orderIdstringrequired

Order ID.

toStatusstringrequired

Target status. Must be a valid transition from the current state.

reasonstring

Reason for the transition, for the activity log. Only required when the specific transition demands it (e.g. some transitions to cancelled/rejected).

detailsstring

Additional free-text detail for the activity log.

flowProfilestring

Flow profile the transition is validated against: basic, intermediate (default), or wms-full.

{
  "toStatus": "preparing"
}
200
{
  "data": {
    "order": { "_id": "65f3a1b2c4d5e6f7a8b9c0d1", "orderStatus": "preparing" },
    "previousStatus": "accepted",
    "newStatus": "preparing",
    "hooks": []
  }
}

Required permission: orders:update

reason isn't required in general

This endpoint's transport schema only requires toStatus. reason isn't required unless the specific transition demands it (for example, transitioning to cancelled or rejected from certain states) — in that case, omitting it returns validation:invalid_input.

The response does NOT carry _links, and the order is nested under order

Unlike accept/reject/cancel (which return { data: { ...order, _links } }), this endpoint returns { data: { order, previousStatus, newStatus, hooks } } — the complete order lives under the order key, without _links, alongside the previous/new status and the list of hooks that fired (e.g. inventory adjustments).

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/transition \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"toStatus": "preparing"}'

Bulk transition

Applies the same transition to several orders in a single call.

Transitions several orders at once. Requires orders:update permission.

orderIdsarrayrequired

IDs of the orders to transition.

toStatusstringrequired

Target status for all orders.

reasonstring

Reason applied to the batch. Not required by the schema.

detailsobject

Additional batch metadata.

flowProfilestring

Optional flow profile for the transition.

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1", "65f3a1b2c4d5e6f7a8b9c0d2"],
  "toStatus": "accepted"
}
200
{
  "data": {
    "success": true,
    "bulkOperationId": "bulk_1721500000000_abc123",
    "summary": { "total": 2, "successful": 2, "failed": 0, "skipped": 0 },
    "results": [
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "success": true, "previousStatus": "pending", "newStatus": "accepted" },
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d2", "success": true, "previousStatus": "pending", "newStatus": "accepted" }
    ],
    "targetStatus": "accepted",
    "performedAt": "2026-07-20T18:30:00.000Z",
    "performedBy": { "userId": "65f0a0b0c0d0e0f0a0b0c0d0" }
  }
}

Required permission: orders:update

An empty orderIds fails the whole operation

orderIds must have at least one element — an empty array rejects the entire call with validation:invalid_input; there's no partial result in that case. With at least one element, each order is processed individually: one can fail (invalid transition, not found) without aborting the rest — check results[] for the per-order detail.

curl -X POST https://api.fenicia.io/orders/bulk/transition \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1", "65f3a1b2c4d5e6f7a8b9c0d2"],
    "toStatus": "accepted"
  }'

Get an order's valid transitions

Returns the states an order can transition to from its current state. Requires orders:read permission.

orderIdstringrequired

Order ID.

flowProfilestring

Flow profile the valid transitions are computed against: basic, intermediate (default), or wms-full.

200
{
  "data": {
    "currentStatus": "accepted",
    "validTargets": ["processing", "preparing", "cancelled"]
  }
}

Required permission: orders:read

This endpoint saves you from having to reimplement the state machine table in your client: it returns directly the set of valid states for the queried order. It's documented in detail in Get an order, along with the rest of the resource's HATEOAS links.

Errors

CodeStatusDescription
orders:invalid_transition422The requested toStatus/status isn't a valid transition from the order's current state.
orders:not_found404No order exists with that ID in your tenant.
orders:already_accepted409The order was already accepted previously.
orders:already_cancelled409The order is already cancelled.
orders:already_fulfilled409The order was already fulfilled.
orders:cannot_cancel422The order is in a state that doesn't allow cancellation.
validation:invalid_input400The body doesn't satisfy the schema (for example, reason outside the catalog of 12 values).
auth:invalid_token401The API key is invalid or has been revoked.
auth:permission_denied403The API key doesn't have the required scope.

See the full error catalog for the rest of the domain's codes.

Next steps