Skip to content

Errors

When a request fails you get a non-2xx HTTP status and a JSON body. There is no single error envelope: failures arrive in one of two different shapes depending on where in the pipeline the request was rejected. Build your client to read the response body on every status, success or failure, and to tolerate both shapes.

StatusMeaning
200OK. The request succeeded and the body contains the response.
201Created. Used by endpoints that create a new record.
400Bad request. Validation failed, paging exceeded the 10,000 ceiling, or a business rule rejected the call. See “Handled exceptions” below.
401Unauthorized. A required auth header is missing (including X-Clinic-Id), the API keys are missing or mismatched, or the integration is not enabled for the clinic. See “Common failure states” below.
403Forbidden. The keys are valid but lack access to the requested clinic or resource.
404Not found. The resource ID does not exist or is not visible to this integration.
409Conflict. A concurrent modification or duplicate-key condition was detected.
429Too many requests. You exceeded a rate limit.
500Server error. Something went wrong on the Shepherd side. Retry with backoff.
503Service unavailable. Shepherd is briefly offline for maintenance. Retry with backoff.

Both shapes are served as application/json. Which one you get depends on how far the request travelled before it was rejected.

Rules enforced inside the endpoint (business rules, date-range limits, the paging ceiling) return the message as a JSON-encoded string: the body is a quoted string, not an object.

HTTP/1.1 400 Bad Request
Content-Type: application/json
"dateCreatedTo must be within one month of dateCreatedFrom"

Parsing this yields a plain string, so JSON.parse(body) gives you the message directly. Note the surrounding quotes are part of the body; if you print the raw text without parsing, they will show up in your logs.

Requests rejected before the endpoint runs, which mainly means model binding and missing required fields, return an object:

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"message": "The request is invalid.",
"statusCode": 400,
"details": null,
"error": null,
"modelState": null
}

A safe reader handles both: parse the body as JSON, and if the result is a string use it as the message, otherwise read .message.

The API distinguishes between two flavors of 400:

  • Validation errors: the request shape is wrong, a required field is missing, the date range is too wide, or the page math exceeds the 10,000 ceiling. A missing or unbindable field produces the envelope in shape 2; the rest produce the string in shape 1.
  • Handled exceptions: the request was well-formed but a business rule rejected it (for example, posting an invoice line against a closed invoice). These return 400 with the business message in shape 1. They are not transient; retrying without changes will fail again.

Treat 400 as “fix the request, then retry”; do not put a retry loop around it.

Most support requests come down to a handful of conditions. The messages below arrive as bare JSON strings (shape 1 above), so what you get back reads much like the “Response body” column here. The fix is almost always client-side.

Response bodyStatusWhat it meansHow to fix
Missing X-Clinic-Id header401The clinic identifier was not sent. It is an HTTP header, not a body field; a clinicId placed in the JSON body is ignored.Send X-Clinic-Id with the clinic’s UUID on every clinic-scoped call. See Authentication.
Missing X-Integration-Public-Key header / Missing X-Integration-Private-Key header401One of the two key headers is absent.Send both key headers on every request.
Incorrect API keys provided401The public/private pair does not match a known integration, or a key was revoked.Recheck the pair; rotate the key if you suspect exposure.
Integration not enabled for the provided clinic401The keys are valid, but this integration has not been turned on for that clinic.The clinic enables your integration on their side; partners confirm this during onboarding.
(no specific message)403Authenticated, but the keys are not scoped to the clinic or record you asked for.Confirm the clinic is in your grant and that X-Clinic-Id names the right one. For shared data across a group, see Groups and multi-site.
Request must contain either a DateCreated or DateUpdated filter400A list endpoint that requires a date window received none.Send a dateCreatedFrom / dateCreatedTo or dateUpdatedFrom / dateUpdatedTo pair.
dateCreatedTo must be within one month of dateCreatedFrom400The date range exceeds the one-month cap.Keep each window to one calendar month; sweep month by month for longer spans.
(paging message)400page * rpp exceeded the 10,000-item ceiling.Narrow the result set with a filter, then re-paginate. See Conventions.
{"message":"The request is invalid."}400A required field was missing, or a value could not be bound to its type (malformed GUID, unparseable date). No field-level detail is returned.Recheck the endpoint’s required fields and the format of every ID and date you sent. For patient writes see Troubleshooting a rejected write.
Request rate limit exceeded. Please wait before making further requests.429You crossed the per-minute or per-hour rate limit.Back off with jitter and retry; spread bulk work out.

A simple, robust strategy:

  • Do not retry: 400, 401, 403, 404, 409. These are caller errors and a retry just wastes the rate-limit budget.
  • Retry with backoff: 429, 500, 503, network timeouts. Use exponential backoff with jitter. See Rate limits for guidance.

The pattern below works for any endpoint. Inspect ok/IsSuccessStatusCode/getResponseCode, and on failure read the raw body; reading it as text first is safest, since it works whichever shape came back:

Terminal window
# -w prints the HTTP status on a final line, -s silences the progress bar
curl -sS -X POST https://open-api.shepherd.vet/pav2/open-api-clients \
-H "Content-Type: application/json" \
-H "X-Integration-Public-Key: $SHEPHERD_PUBLIC_KEY" \
-H "X-Integration-Private-Key: $SHEPHERD_PRIVATE_KEY" \
-H "X-Clinic-Id: $SHEPHERD_CLINIC_ID" \
-d '{"page":1,"rpp":10}' \
-w "\nHTTP %{http_code}\n"
  • Authentication for the three headers, including the X-Clinic-Id header behind most 401s.
  • Rate limits for the specifics of 429 handling and backoff.
  • Conventions for the 10,000-item paging ceiling that produces a 400.
  • Groups and multi-site for the clinic scoping behind 403s in shared-data groups.