Inventory adjustments

Adjustments correct a SKU's stock at a location, with an auditable business reason (shrinkage, damage, count correction, etc.). All endpoints for this resource require the inventory:adjust permission.

The adjustment applies the stock change WHEN CREATED, not when approved

Contrary to what the approve/reject naming suggests, creating an adjustment already modified the stock. Approval is, in practice today, a no-op on stock — it only changes the status field. Rejection does have an effect: it reverts the stock. Read the full "Write semantics" section before building your approval flow.

List adjustments

Lists the tenant's adjustments, with optional filters

pagenumber

Page, 1-indexed. Default: 1.

limitnumber

Items per page. Default: 20.

statusstring

Filter by status: pending_review, approved, rejected.

locationIdstring

Filter by location.

skustring

Filter by SKU.

reasonstring

Filter by reason (see the enum below).

200
{
  "adjustments": [
    {
      "_id": "670a1b2c3d4e5f6a7b8c9d0e",
      "locationId": "loc_abc123",
      "sku": "CAM-ROJO-M",
      "oldQuantity": 42,
      "newQuantity": 40,
      "adjustmentQuantity": -2,
      "reason": "damage",
      "status": "pending_review"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 6, "pages": 1 }
}

Query an adjustment

Queries an adjustment by ID

idstringrequired

Adjustment ID.

200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "sku": "CAM-ROJO-M", "status": "pending_review" }
404
{ "code": "not-found" }

Adjustments summary

Aggregated adjustment totals over a date range

startDatestring

ISO 8601 start date.

endDatestring

ISO 8601 end date.

200
{
  "totalAdjustments": 34,
  "positiveAdjustments": 12,
  "negativeAdjustments": 22,
  "totalQuantityAdjusted": -187,
  "totalCostImpact": 0
}

Create an adjustment

Creates an adjustment and applies the stock change immediately

locationIdstringrequired

Affected location.

skustringrequired

SKU to adjust.

newQuantitynumberrequired

New absolute quantity (not a delta).

reasonstringrequired

One of: count_correction, damage, theft, expired, found, return, supplier_error, restock, shrinkage, sample, other.

notesstring

Free-text note.

productSkustring

Parent product SKU, if applicable (see automatic resolution below).

{
  "locationId": "loc_abc123",
  "sku": "CAM-ROJO-M",
  "newQuantity": 40,
  "reason": "damage",
  "notes": "2 piezas dañadas en almacén"
}
201
{
  "_id": "670a1b2c3d4e5f6a7b8c9d0e",
  "locationId": "loc_abc123",
  "sku": "CAM-ROJO-M",
  "oldQuantity": 42,
  "newQuantity": 40,
  "adjustmentQuantity": -2,
  "reason": "damage",
  "status": "pending_review"
}
400
{ "code": "bad-request/invalid-reason" }

Required permission: inventory:adjust

reason accepts 11 values — 'sale' is NOT one of them

The public endpoint validates reason against this exact list: count_correction, damage, theft, expired, found, return, supplier_error, restock, shrinkage, sample, other. The value sale exists in the data model but is reserved for the automatic decrement the system generates when processing an order — passing reason: "sale" to this endpoint responds 400 bad-request/invalid-reason.

Key points about creation:

  • You do not set oldQuantity yourself. The server always reads the current stock from the inventory record at the moment the adjustment is created, ignoring any oldQuantity you send in the body.
  • adjustmentQuantity is calculated server-side as newQuantity − oldQuantity.
  • If you omit productSku, the server resolves it in this order: (1) the productSku already present on the inventory record for that SKU/location, (2) a lookup in the product catalog, (3) falls back to using the sku itself as a last resort.

Write semantics — "apply first, review later"

This is the most important business pattern of this resource:

  1. Creation (POST /inventory/adjustments) applies the stock change immediately. The record is born with status: 'pending_review', but the stock has already been modified at that point — not when someone approves it. This POST does trigger the Inventory Updated event (and a possible Low Stock Alert), because internally it uses the same write path as PUT for stock.
  2. approve on a pending_review adjustment (today's normal flow) does not touch stock again. It only changes status → approved and stamps the audit fields (approvedBy, approvedAt). The stock already moved in step 1.
  3. reject does have an effect on stock: it reverts it to oldQuantity (triggering another Inventory Updated event for the reversal), and it also automatically creates a second adjustment record: reason: 'count_correction', sourceType: 'reversal', sourceId pointing to the original adjustment, adjustedBy: 'system', already in approved status.

Rejecting an adjustment creates TWO records, not one

If you list adjustments expecting to see only the one you rejected, you'll see two: the original (now status: 'rejected') and a synthetic reversal adjustment generated by the system. Both count in GET /inventory/adjustments and in /summary.

Approve

Marks the adjustment as approved (does not move stock — see above)

idstringrequired

Adjustment ID.

No body required — the handler never reads event.body.

{}
200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "status": "approved" }
404
{ "code": "not-found" }

Reject (reverts the stock)

Reverts stock to oldQuantity and creates a reversal adjustment

idstringrequired

Adjustment ID.

rejectionReasonstringrequired

Rejection reason (or the alternate field `reason`).

{ "rejectionReason": "Conteo físico no coincide con el reporte" }
200
{ "_id": "670a1b2c3d4e5f6a7b8c9d0e", "status": "rejected" }
400
{ "code": "bad-request/missing-reason" }
404
{ "code": "not-found" }

Example — create and check the impact

curl -X POST 'https://api.fenicia.io/inventory/adjustments' \
  -H 'Authorization: Bearer fkapi_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "locationId": "loc_abc123",
    "sku": "CAM-ROJO-M",
    "newQuantity": 40,
    "reason": "damage",
    "notes": "2 piezas dañadas en almacén"
  }'

Errors

CodeStatusDescription
bad-request/missing-tenant401The tenant could not be resolved from the API key.
bad-request/missing-location400locationId is missing.
bad-request/missing-sku400sku is missing.
bad-request/missing-quantity400newQuantity is missing.
bad-request/missing-reason400reason is missing (create) or rejectionReason/reason is missing (reject).
bad-request/invalid-reason400reason is not one of the 11 accepted values.
not-found404No adjustment exists with that ID.
internal-error500Internal error — includes the case where newQuantity would result in negative stock (see negative stock).

Next steps