Errors

Failures come back as RFC 7807 problem documents. The machine-readable code lives in the type field — there is no separate code property.

Shape of an error

409 Conflict
{
  "type": "/errors/Reseller.InsufficientQuota",
  "title": "Insufficient quota for the requested product.",
  "status": 409,
  "traceId": "00-8a1b2c3d4e5f...-01",
  "timestamp": 1754380800
}

Branch on type, not on the status code alone — several distinct conditions share a status. The title is written for humans and may be reworded, so do not match on it.

Reading the code
const problem = await response.json().catch(() => null);
const code = problem?.type?.replace("/errors/", "");
Include traceId when you report a problem. It lets us find the exact request in our logs instantly.

Validation errors

Malformed requests use a slightly different shape, with a per-field breakdown:

400 Bad Request
{
  "type": "/errors/validation",
  "title": "Validation Failed",
  "status": 400,
  "detail": "One or more validation errors occurred.",
  "instance": "/billing/partner/licenses",
  "errors": [
    {
      "field": "PlanSlug",
      "code": "InvalidField",
      "message": "PlanSlug is required."
    }
  ],
  "traceId": "00-8a1b2c3d4e5f...-01",
  "timestamp": 1754380800
}

Authentication failures

A 401 has an empty body — no problem document, nothing to parse. Check for it before attempting to read JSON.

Full catalogue

Quota

Reseller.InsufficientQuota 409

Your balance for this product is too low for the whole request. Nothing was consumed. Top up before retrying.

Reseller.InvalidQuantity 400

count must be greater than zero.

Reseller.QuantityTooLarge 400

count exceeds the per-request maximum of 1000. Split the batch.

Recipient

Reseller.TargetUserNotFound 404

No account matches that login. Check it with the customer — retrying alone will not help.

Reseller.TargetUserBanned 409

The recipient is banned and cannot receive a subscription. No quota was consumed.

Product

Billing.ProductNotFound 404

Unknown planSlug. Read the valid values from your balances response rather than hardcoding them.

Billing.ProductNotActive 409

The product exists but is no longer sold. Any quota you hold for it is stranded — get in touch.

Idempotency

Reseller.IdempotencyKeyRequired 400

The field was missing or blank. It is mandatory on every write.

Reseller.IdempotencyKeyConflict 409

This key was already used with a different body. Almost always a key reused across two sales. Nothing was charged.

Reseller.RequestInProgress 409

An identical request is still running. Back off and retry with the same key.

Request shape

Reseller.PlanSlugRequired 400

planSlug was missing or blank.

validation 400

One or more fields failed validation. The errors array names each one.

Server-side

Reseller.PromoGenerationFailed 500

The batch could not be generated and the charge was reversed. No quota was consumed — safe to retry with the same key.

License.Unavailable 500

A downstream service was unreachable. Retry with the same key.

Auth.Unavailable 500

The login could not be resolved right now. Retry with the same key.

InternalError 500

Unexpected failure. Retry with the same key; if it persists, send us the traceId.

What is safe to retry

Retry on network failures, on Reseller.RequestInProgress, and on 5xx — always with the same idempotency key. Every other 4xx is final: the request needs changing before it will succeed.

Where an error says no quota was consumed, that is a guarantee rather than a likelihood. You will not be charged for a request that failed this way.