Abandoned Carts

An abandoned cart is a purchase process a customer started on one of your connected channels but never converted into an order. Fenicia adds them automatically so you can analyze them, attempt to recover them, or close them as unrecoverable.

Canonical route vs. legacy alias

The canonical route for this domain is /orders/abandoned-carts/*. There is an alias /abandoned-carts/* (without the /orders prefix) that points to the same lambda for compatibility, but it has been deprecated since June 1, 2026 and every call generates a warning in the server logs. Always use /orders/abandoned-carts/* in new integrations.

This domain's envelope differs from the rest of Orders

Every other Orders endpoint returns paginated lists as { data: [...], meta: { pagination: {...} } }. The abandoned carts listing does not follow that pattern. Its response is double-nested:

{
  "data": {
    "data": [{ "_id": "65f3a1b2c4d5e6f7a8b9c0d1", "totalAmount": 1250.5 }],
    "pagination": { "page": 0, "limit": 20, "total": 87, "totalPages": 5, "hasMore": true }
  }
}

That is, pagination is a sibling of data.data, not of data on its own, and there is no meta object. This is a real inconsistency in the API (not a documentation error) — if you write a generic Orders client that assumes the {data, meta:{pagination}} shape, this endpoint will break it. Read it with response.data.data and response.data.pagination.

Permissions and error codes: two different vocabularies

This domain uses two different text conventions that you must not confuse:

  • Permissions (API key scopes) are written with a hyphen: abandoned-carts:read, abandoned-carts:sync, abandoned-carts:recover, abandoned-carts:update, abandoned-carts:send-reminder.
  • Error codes in the catalog are written with an underscore: abandoned_carts:not_found, abandoned_carts:already_recovered, abandoned_carts:already_closed, abandoned_carts:recovery_failed.

They are two independent catalogs (the permissions one lives in PERMISSIONS.ABANDONED_CARTS.*, the errors one in the library's error catalog). This is not a typo on this page — that's literally how they are defined in the API.

Base URL

https://api.fenicia.io

All endpoints require Authorization: Bearer fkapi_....


List abandoned carts

Paginated list of abandoned carts with filters. Requires abandoned-carts:read.

pagenumber

Page number (0-based).

limitnumber

Items per page.

statusstring

Filter by cart status.

statusGroupstring

Filter by visual status group.

recoveryStatusstring

Filter by recovery process status.

channelIdstring

Filter by a specific channel.

channelIdsstring

Filter by several channels. Plural alias of channelId.

originstring

Filter by channel origin/type.

startDatestring

ISO start date of the abandonment range (inclusive).

endDatestring

ISO end date of the abandonment range (inclusive).

minAmountnumber

Minimum cart amount.

maxAmountnumber

Maximum cart amount.

hasBeenContactedboolean

Filters carts that have (or have not) already had a recovery attempt sent.

sortBystring

Sort field.

sortOrderstring

Sort direction (asc | desc).

200
{
  "data": {
    "data": [
      {
        "id": "65f3a1b2c4d5e6f7a8b9c0e1",
        "channelId": "chn_shopify_01",
        "channelType": "shopify",
        "status": "open",
        "recoveryStatus": "not_contacted",
        "totalAmount": 1250.00,
        "currency": "MXN",
        "hasBeenContacted": false,
        "abandonedAt": "2026-04-10T09:15:00.000Z"
      }
    ],
    "pagination": {
      "page": 0,
      "limit": 20,
      "total": 87,
      "totalPages": 5,
      "hasMore": true
    }
  }
}
curl "https://api.fenicia.io/orders/abandoned-carts?limit=20&status=open&hasBeenContacted=false" \
  -H "Authorization: Bearer fkapi_your_api_key"

Search abandoned carts

Free-term search over abandoned carts. Requires abandoned-carts:read.

qstring

Search term.

pagenumber

Page number (0-based).

limitnumber

Items per page.

200
{
  "data": {
    "data": [
      {
        "id": "65f3a1b2c4d5e6f7a8b9c0e1",
        "channelId": "chn_shopify_01",
        "channelType": "shopify",
        "status": "open",
        "recoveryStatus": "not_contacted",
        "totalAmount": 1250.00,
        "currency": "MXN",
        "hasBeenContacted": false,
        "abandonedAt": "2026-04-10T09:15:00.000Z"
      }
    ],
    "pagination": {
      "page": 0,
      "limit": 20,
      "total": 3,
      "totalPages": 1,
      "hasMore": false
    }
  }
}

Same double-nested envelope as the listing

This search uses the same double-nested response shape described above: response.data.data is the array, response.data.pagination is the pagination — there is no meta.


Count carts by status group

Count of abandoned carts grouped by visual status. Requires abandoned-carts:read.

200
{
  "data": {
    "no_action": 42,
    "with_action": 18,
    "recovered": 15,
    "not_recovered": 12,
    "all": 87
  }
}

Real groups: no_action, with_action, recovered, not_recovered, all

no_action are the open carts with no recovery attempt at all; with_action are the contacted ones; not_recovered sums expired + closed; all is the total. These 5 names don't match the statusGroup you accept as a filter in the listing — they're the fixed grouping of this count endpoint.


Count abandoned carts

Returns the total count of abandoned carts matching the filters. Requires abandoned-carts:read.

statusstring

Filter by cart status.

statusGroupstring

Filter by visual status group.

recoveryStatusstring

Filter by recovery process status.

channelIdstring

Filter by channel.

originstring

Filter by channel origin/type.

startDatestring

ISO start date (inclusive).

endDatestring

ISO end date (inclusive).

200
{ "data": { "count": 87 } }

Tip

Unlike what you might expect from other simple count endpoints, this one accepts the same filters as the listing — use it to show a counter that exactly matches a filtered view, without fetching all the records.


Abandoned carts statistics

Aggregated statistics of abandoned carts within a range. Requires abandoned-carts:read.

channelIdstring

Filter by channel.

originstring

Filter by channel origin/type.

startDatestring

ISO start date (inclusive).

endDatestring

ISO end date (inclusive).

200
{
  "data": {
    "totalCarts": 87,
    "totalValue": 128450.50,
    "recoveryRate": 0.18
  }
}
curl "https://api.fenicia.io/orders/abandoned-carts/stats?startDate=2026-04-01&endDate=2026-04-30" \
  -H "Authorization: Bearer fkapi_your_api_key"

Get an abandoned cart

Gets the full detail of an abandoned cart, including its HATEOAS links. Requires abandoned-carts:read.

idstringrequired

Abandoned cart ID.

200
{
  "data": {
    "id": "65f3a1b2c4d5e6f7a8b9c0e1",
    "channelId": "chn_shopify_01",
    "channelType": "shopify",
    "status": "open",
    "recoveryStatus": "not_contacted",
    "totalAmount": 1250.00,
    "currency": "MXN",
    "abandonedAt": "2026-04-10T09:15:00.000Z",
    "_links": {
      "recover": "/orders/abandoned-carts/65f3a1b2c4d5e6f7a8b9c0e1/recover",
      "close": "/orders/abandoned-carts/65f3a1b2c4d5e6f7a8b9c0e1/close"
    }
  }
}
404
{ "error": { "code": "abandoned_carts:not_found", "message": "Abandoned cart not found" } }

Sync abandoned carts

Triggers a sync of abandoned carts from the connected channels. Requires abandoned-carts:sync.

channelIdstring

Limits the sync to a single channel. If omitted, the handler reports it as general (see warning below).

{ "channelId": "chn_shopify_01" }
202
{
  "data": {
    "message": "Abandoned carts sync initiated",
    "status": "processing",
    "channelId": "chn_shopify_01"
  }
}

Placeholder endpoint: it doesn't trigger a real sync yet

Unlike POST /orders/sync, this endpoint's handler is a placeholder: it accepts the request and responds 202, but the channel integrations aren't wired to this route yet — it doesn't trigger any real abandoned cart sync on the channel. channelId is optional; if you omit it, the channelId key simply doesn't appear in the response.


Recover a cart

Marks the cart as recovered or triggers the recovery action. Requires abandoned-carts:recover.

idstringrequired

Abandoned cart ID.

orderIdstring

Internal ID of the order the cart converted into, if it already exists.

externalOrderIdstring

ID of the order on the source channel (Shopify, Amazon, etc.), if it already exists.

{ "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "externalOrderId": "SHOPIFY-10042" }
200
{ "data": { "id": "65f3a1b2c4d5e6f7a8b9c0e1", "status": "recovered" } }
409
{ "error": { "code": "abandoned_carts:already_recovered", "message": "Abandoned cart already recovered" } }
curl -X POST https://api.fenicia.io/orders/abandoned-carts/65f3a1b2c4d5e6f7a8b9c0e1/recover \
  -H "Authorization: Bearer fkapi_your_api_key"

Close a cart

Closes the cart as unrecoverable. Requires abandoned-carts:update.

idstringrequired

Abandoned cart ID.

{}
200
{ "data": { "id": "65f3a1b2c4d5e6f7a8b9c0e1", "status": "closed" } }
409
{ "error": { "code": "abandoned_carts:already_closed", "message": "Abandoned cart already closed" } }

No body required

The handler doesn't read any field from the body — send {} or no body at all, it makes no difference.

curl -X POST https://api.fenicia.io/orders/abandoned-carts/65f3a1b2c4d5e6f7a8b9c0e1/close \
  -H "Authorization: Bearer fkapi_your_api_key"

Record a recovery action

Records a recovery action on the cart (for example, a reminder sent). Requires abandoned-carts:send-reminder.

idstringrequired

Abandoned cart ID.

typestringrequired

Channel of the action: email | sms | whatsapp | notification | manual.

statusstringrequired

Result of the action: scheduled | sent | delivered | failed | clicked | converted.

timestampstring

ISO date of the action. Default: now.

templateIdstring

ID of the message template used.

recoveryUrlstring

Recovery URL sent to the customer (must be a valid URL).

messagestring

Content of the message sent.

userIdstring

ID of the team user who triggered the action.

{
  "type": "whatsapp",
  "status": "sent",
  "templateId": "recordatorio-carrito-v1",
  "recoveryUrl": "https://tienda.example.com/cart/recover/65f3a1b2c4d5e6f7a8b9c0e1"
}
200
{
  "data": {
    "id": "65f3a1b2c4d5e6f7a8b9c0e1",
    "status": "contacted",
    "recoveryStatus": "whatsapp_sent",
    "lastContactedAt": "2026-04-10T10:00:00.000Z"
  }
}
404
{ "error": { "code": "abandoned_carts:not_found", "message": "Abandoned cart not found" } }

recoveryStatus is derived from type + status

You don't send recoveryStatus yourself: the API computes it from your type/status. Only when status is sent or delivered does it become email_sent / sms_sent / whatsapp_sent according to type; in any other case it stays pending. The cart also moves to status: "contacted" as soon as you register the first action.


Update a cart's status

Directly updates the status of an abandoned cart. Requires abandoned-carts:update.

idstringrequired

Abandoned cart ID.

statusstringrequired

New status: open | contacted | recovered | expired | closed.

{ "status": "expired" }
200
{
  "data": {
    "id": "65f3a1b2c4d5e6f7a8b9c0e1",
    "status": "expired",
    "updatedAt": "2026-04-10T10:05:00.000Z"
  }
}
400
{ "error": { "code": "validation:invalid_input", "message": "Invalid status. Must be one of: open, contacted, recovered, expired, closed" } }
404
{ "error": { "code": "abandoned_carts:not_found", "message": "Abandoned cart not found" } }

Use recover/close for transitions with business rules

This endpoint writes the status directly, without the rules applied by POST .../recover (records recoveredAt) or POST .../close. Prefer those dedicated endpoints when the use case matches; use this one only for cases they don't cover (for example, manually marking expired).


Errors

CodeStatusDescription
abandoned_carts:not_found404No abandoned cart with that ID exists in your tenant.
abandoned_carts:already_recovered409The cart has already been marked as recovered.
abandoned_carts:already_closed409The cart has already been closed.
abandoned_carts:recovery_failed500The recovery action failed (for example, sending the reminder).
validation:invalid_input400The body doesn't satisfy the schema — type/status is missing on a recovery action, or status isn't a valid value when updating the status.
validation:invalid_pagination400page or limit has a value out of range.
auth:invalid_token401The API key is invalid or was revoked.
auth:permission_denied403The API key doesn't have the required permission (hyphenated, e.g. abandoned-carts:recover).

Tenant isolation

You can only query and operate abandoned carts belonging to your own tenant. Attempting to access a cart from another tenant returns 404, never cross-tenant information.

Next steps