docs
/
Client Integration

Errors

The error envelope, the codes you must handle, and what each one actually means.

Every failure comes back in the same shape:

{
  "code": "missing_orgid",
  "statusCode": 400,
  "error": "Organization ID is required. Pass orgid as a header, query param, or body field.",
  "path": "/",
  "method": "GET",
  "timeStamp": "2026-09-11T09:16:21.511Z"
}

Branch on code. It is stable. error is written for a human and will be reworded.

try {
  await appmint('client/affiliate/me', { customerToken });
} catch (err) {
  if (err.code === 'missing_authorization_header') return redirectToSignIn();
  if (err.status === 429) return retryAfterBackoff();
  throw err;
}

Codes worth handling

CodeStatusMeaningWhat to do
missing_orgid400No tenant on the requestSend orgid. A configuration bug, not a user error
missing_authorization_header401Customer route without a tokenSign the customer in
—401Token expired or rejectedRefresh once, replay once, then sign in
—403Authenticated but not permittedDo not retry
—404No such recordDistinguish "gone" from "never existed" before deleting local state
—409Conflict — usually a uniqueness clashSurface the field; a referral code taken mid-write lands here
—422Validation failedShow the message; the body says which field
—429Rate limitedBack off, honour Retry-After
—5xxServer faultRetry idempotent reads; never blind-retry a payment

Validation is strict

The global pipe runs with forbidNonWhitelisted: true, so an undeclared property rejects the whole request rather than being ignored.

This bites on read-modify-write: fetch a record, change one field, send it back, and the platform fields (sk, pk, createdate, version) plus computed ones (calculatedPrice) are not accepted by the write DTO.

// Fails with 400
const { data: product } = await appmint(`storefront/product/${sku}`);
product.data.price = 42;
await appmint('…', { method: 'POST', body: product });

// Works — send only what the write accepts
await appmint('…', { method: 'POST', body: { sku, price: 42 } });

enableImplicitConversion does coerce "5" to 5 where the DTO says number, so numeric strings from form inputs are fine.

Failures that are not errors

Some outcomes return 200 with a verdict in the body. Check the field, not the status.

A bad coupon — the cart still prices, the total is unchanged:

{ "valid": false, "reason": "not_found", "message": "Discount code not found", "total": 1049.25 }

An unservable destination — served: false is the honest answer, never a guessed rate:

{ "served": false, "options": [], "currency": "USD" }

Priced but unquotable — somewhere is covered, but not right now:

{
  "served": false,
  "options": [],
  "problems": [
    { "config": "live", "reason": "live: Cannot calculate shipping: \"SKU1\" is missing parcel dimensions (length/width/height). Set dimensions on the product or use a fixed shipping rate." }
  ]
}

problems[] names the offending configuration and why. Show it — "we can't ship there" and "this product has no dimensions set" need different actions from different people.

Proxy your errors faithfully

If you put your own server in front (and you should — see The server proxy), forward the upstream status:

if (axiosError.response) {
  return NextResponse.json(
    { error: axiosError.response.data, status: axiosError.response.status },
    { status: axiosError.response.status },
  );
}

Collapsing everything into 500 turns a missing record into an outage and hides the 422 message the user needed to read.

Fail loudly when you cannot reach AppEngine. A cached application token that goes bad in a way that is not a 401 leaves a process unable to call the API for its whole lifetime. If it keeps answering 200 with a degraded page, nothing alerts and the damage is measured in days. Return 503 so a health check pulls it out.