The thing to know first
A400 from v1 can arrive in four different JSON shapes. They come from four different
layers — model binding, field validation, business rules and the controller — and nothing in
the status line distinguishes them. An integration that branches on the status code alone
will mis-read three of the four.
Branch on the body, using the test in Telling them apart.
Status codes
The four bodies
1 · Business rule — message, integer errorCode, projectType
This is the one carrying a stable, structured code you can branch on, and it is what most
refusals on POST /api/v1/marketplace-listings look like.
50 InStockRx, 01 Pharmacy, 11 the error
itself. The ones you are most likely to meet on create:
On
500114 the message text is configured per state rather than fixed, so treat the string
as display-only and branch on the code.2 · Field validation — isValid and errors.$values
Business-rule validators that run inside the command report this way. It is a .NET validation
result serialised with reference handling turned on, which is where $id and $values come
from.
errorCode here is a string, and that is what tells this shape apart from the one above
at a glance. Some rules carry a stable code such as medicationScanRequired or
ndcSellLimitReached; others were never given one and fall back to an internal validator
name, which is not something to branch on. Key off errorCode where you recognise it and
show errorMessage where you don’t — every errorMessage is written to be read by an end
user.
Listing rules lists which rules carry which code.
3 · Request shape — errors keyed by field
Produced before your request reaches the endpoint, when a field is missing or the wrong type.
This is the framework’s own response, and the only body with title and status inside it.
errors is an object keyed by field name with an array of messages per field — not
the $values array of shape 2. Same property name, different structure.
4 · Everything else — error, statusCode, message
Authentication, permission and listing-state problems.
This shape is not only for statuses above 400. It is also returned as a
400 — a
missing pharmacyNpi on a request that needs one comes back this way rather than as shape
3. So a 400 on its own tells you nothing about which body to expect.And one with no body at all — 429
A rate-limit rejection is produced before the request reaches the
API’s error handling, so a 429 has an empty body. Everything it tells you is in the
status line and the Retry-After header.
Telling them apart
Run these in order on the response body. Each test is on a field the other shapes don’t have.Common cases
A 400 that is really a business rule
A 400 that is really a business rule
If the body carries a numeric
errorCode, your request was well-formed and a rule refused
it. Nothing about the payload will fix it — the pharmacy is on probation, restricted, or
the NDC is not one they may list. See Listing rules.A 400 that is really an auth problem
A 400 that is really an auth problem
Request validation runs before authentication. A request that is both malformed and
unauthenticated reports the malformed body, not
401. Fix the body; if you then get
401, it’s the key.409 when updating a listing
409 when updating a listing
Marketplace listings, draft listings and cash offers can only be modified while
Pending. Once purchased, accepted or cancelled,
PUT and DELETE return 409.
Re-read the object and check its status before offering an edit.403 on a pharmacy you expected to have
403 on a pharmacy you expected to have
The NPI isn’t in your group. Call
GET /api/v1/pharmacies/search to see the current
membership — a pharmacy added by NPI stays pending until it registers with InStockRx.429 that arrives in bursts
429 that arrives in bursts
The v1 ceiling is 300 requests per minute counted per calling IP, not per API key, and
the window is fixed rather than rolling. A batch job sharing your egress address draws on
the same allowance. See rate limits.
400 on quantity
400 on quantity
quantity is conditional on fullPackage: exactly 1 when fullPackage is true,
between 0.01 and the drug’s package size when it’s false.Showing errors to your users
message and errorMessage are written to be read by a pharmacist, and we intend to keep
them that way. Passing them straight through is a reasonable default.
The exception is the rules v1 cannot satisfy at all — certification files and 2D scan
requirements. There is no way to complete those through this API today, so a raw error leaves
your user stuck with no next step. Route those to a message pointing at the InStockRx web
interface instead; Listing rules marks which ones they are.
Retries
v1 has no idempotency mechanism. Retrying a failedPOST may create a second listing or
offer.
A 429 is the one safe rejection to retry as-is: it means the request was refused before
anything was written. Wait for Retry-After, then send it again.
Idempotency keys arrive with v2.