For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
my.pivotal.appGet an API key
Get StartedGuidesAPI ReferenceSDKsChangelog
Get StartedGuidesAPI ReferenceSDKsChangelog
  • Welcome
    • Introduction
    • Quickstart
    • Authentication
  • Concepts
    • Rate limits
    • Errors
    • Pagination
    • Versioning
    • Idempotency
LogoLogo
my.pivotal.appGet an API key
On this page
  • Error types
  • Common codes
  • How to branch in client code
  • What you won’t see
  • Validation error details
  • Reporting issues
Concepts

Errors

One envelope, one set of types, predictable status codes.
|View as Markdown|Open in Claude|
Was this page helpful?
Edit this page
Previous

Rate limits

Next

Pagination

Built with

Every non-2xx response from the Pivotal API lands in the same envelope:

1{
2 "error": {
3 "type": "invalid_request_error",
4 "code": "missing_field",
5 "message": "name is required",
6 "field": "name"
7 }
8}
  • type is one of seven values (table below). Branch on this in your client.
  • code is a stable machine-readable string. Safe to use in switch/case logic.
  • message is a sentence intended for a human — log it, surface it in admin tools, but don’t string-match against it.
  • field is set only when the error points at a specific request property (validation, unique-violation).

Error types

TypeHTTPWhat it means
invalid_request_error400The request didn’t validate. Look at code and field.
authentication_error401API key missing, malformed, or revoked.
permission_error403Key is valid but lacks permission for this resource. (Not used yet — reserved.)
not_found404The resource doesn’t exist, or belongs to another workspace.
conflict409Unique-constraint violation (e.g. slug_taken, email_taken).
rate_limit_error429You hit the per-key bucket. See Rate limits.
internal_error500Pivotal blew up. Retry with backoff; tell us if it persists.

Common codes

The set isn’t huge. These show up most:

CodeWhereMeaning
missing_field400A required field wasn’t sent. field tells you which.
invalid_field400A field is present but failed validation (wrong type, bad format, enum miss).
invalid_request400The body shape is off (malformed JSON, wrong top-level shape).
missing_api_key401No Authorization header.
invalid_api_key401Header present but the key isn’t recognized.
api_key_revoked401Key existed but was revoked.
customer_not_found404The customer id (display_id or cuid) doesn’t resolve.
contact_not_found404Same shape for contacts.
onboarding_not_found404Same shape for onboardings.
slug_taken409Another customer in this workspace already uses that slug.
shopify_domain_taken409Another customer in this workspace already claims that domain. The error code references the underlying column name; the field on this error is shopify_domain.
email_taken409Another contact under the same customer already uses that email.
rate_limited429Bucket empty. Honor Retry-After.
internal_error500Generic 500. Retry with backoff.
http_exception4xxCatch-all for unexpected validation failures. Rare.

How to branch in client code

Match on type for control flow, then on code if you need precision:

1const res = await fetch(url, { headers, body });
2if (!res.ok) {
3 const { error } = await res.json();
4 switch (error.type) {
5 case "invalid_request_error":
6 throw new ValidationError(error.field, error.message);
7 case "authentication_error":
8 throw new AuthError(error.code);
9 case "conflict":
10 // e.g. retry with a different slug
11 if (error.code === "slug_taken") return suggestNewSlug();
12 throw new ConflictError(error.message);
13 case "rate_limit_error": {
14 const wait = Number(res.headers.get("Retry-After") ?? 1);
15 await sleep(wait * 1000);
16 return retry();
17 }
18 case "not_found":
19 return null;
20 default:
21 throw new ApiError(error);
22 }
23}

What you won’t see

  • HTML error pages — every error route returns JSON.
  • Bare { "message": "..." } — always wrapped in { "error": {...} }.
  • Status codes that disagree with the type — 404 always pairs with not_found, 409 with conflict, etc. If you see them out of sync, that’s a bug — open a ticket.

Validation error details

Zod validation errors carry their own code (the Zod issue code, e.g. invalid_type, too_small) and the field path. The message is taken straight from Zod — readable, but treat it as English copy, not a parseable format.

Example: posting a customer without name:

1{
2 "error": {
3 "type": "invalid_request_error",
4 "code": "invalid_type",
5 "message": "Required",
6 "field": "name"
7 }
8}

Posting a non-existent enum value to status:

1{
2 "error": {
3 "type": "invalid_request_error",
4 "code": "invalid_enum_value",
5 "message": "Invalid enum value. Expected 'active' | 'onboarding' | 'at_risk' | 'churned' | 'low_touch', received 'pending'",
6 "field": "status"
7 }
8}

Reporting issues

If you hit a 5xx more than once on a single request, capture the response headers (especially X-Request-Id once we ship it) and send them to help@pivotal.app. Include the request body if it doesn’t contain secrets.