PrismDraft SDK

Errors

Every answer that is not 2xx throws a PrismDraftError. It carries the API's error code, which is what your code should branch on.

PrismDraftError

The API answers failures with one shape, { "error": { "code", "message", "details" } }. The SDK turns it into an Error with these fields:

Field Type Meaning
status number The HTTP status.
code string One of the codes below.
message string A readable description, in English. Show it; do not parse it.
details Record<string, unknown> | undefined Extra data for some codes, listed below. Absent when there is none.

code is typed string, not a union, so a code added to the API later does not break a switch.

An answer that is not this shape, such as a bare 502 from a gateway, is also a PrismDraftError: its status is the HTTP status, code is INTERNAL_ERROR, and message is the status text.

A 2xx answer whose body is not JSON, such as an HTML page from a proxy in front of the API, is a PrismDraftError as well, for any method: status is the HTTP status (200, say), code is INVALID_RESPONSE, and message is The API returned a non-JSON body. The message holds neither the body nor the URL. The SDK does not retry it. An empty 2xx body is not an error.

Codes

Code Status Means details
UNAUTHORIZED 401 The key is missing, unknown, expired or revoked.
FORBIDDEN 403 The key's role lacks a permission, or the key is not a member of a restricted project. { permission }, or { projectId }
RESOURCE_NOT_FOUND 404 No such resource, no such route, or a resource in another workspace. { id } for a resource
INVALID_REQUEST 400 The request is malformed or a value is refused. Also 413 (a JSON body over 1 MiB) and 415 (a content type the route does not take). { issues } for validation
CONFLICT 409 The state of the resource does not allow it, for example publishing an article that is not approved. varies
INSUFFICIENT_CREDITS 402 The workspace's credit balance does not cover the operation. { required, available }
QUOTA_EXCEEDED 429 A plan quota is used up, or the request rate limit was hit. { metric, limit, used } for a quota
RATE_LIMITED 429 An action was repeated too soon (resending an invitation within a minute), or a daily cap was reached (10 image style runs per workspace in 24 hours). The Retry-After header says how long to wait. { retryAfterSeconds }, plus { limit, used } for a cap
PAYMENT_FAILED 402 A billing operation failed, or billing is not configured on the deployment.
GENERATION_FAILED 502 A generation could not finish, or an AI call could not start. A failed job records it as job.errorCode (pd.generation.getStatus). { reason, capability } when the AI is paused or not set up
INTERNAL_ERROR 500 A server fault, or an answer that was not the API's error shape.
PROVIDER_RATE_LIMITED 429 In the catalogue; no route sends it today.
INVALID_STRIPE_EVENT 400 A Stripe event the API refused. Stripe calls that route, not the SDK.

Two details:

  • 404 is not always "does not exist". A workspace or resource you have no access to answers RESOURCE_NOT_FOUND, the same as one that is not there. FORBIDDEN means you can see it exists but your role may not do this.
  • Validation runs before authentication. A request that is malformed answers 400 even when the key is wrong; fix the request first, then the key.

details

For INVALID_REQUEST, details.issues lists what failed. Each issue has a path (a dotted name such as title or translations.0.name) and a message. A Markdown import that cannot be converted lists { construct, line } instead, and an image that cannot be downloaded sets details.src.

For GENERATION_FAILED, the code has two sources. A run that started and failed is recorded on the job (job.errorCode). The same code is also a synchronous 502 answer when the deployment's AI cannot serve the call at all, before any credit is reserved or job created. It comes from generation start, outline approval, translations, category suggestions, planning ideas, publishing plan suggestions (plans.suggest), briefs, links and social posts, AI edit, and image regeneration. These answers carry details.reason, either ai_paused (an administrator paused the capability) or ai_not_configured (no provider or key is set up), and details.capability, one of text, image, embedding or search. Nothing was charged, so there is nothing to refund. The SDK does not retry a 502 from these calls, because it retries only GET requests; fix the deployment's settings, or ask its administrator, and send the request again.

For FORBIDDEN, details.permission names the missing permission, which is the quickest way to find the role you need.

For CONFLICT, details.reason names the cause when the API has one. article_state_changed means the article changed status while a request was moving it (for example it was approved, unpublished or edited by someone else between the API reading it and writing the new status), so nothing was changed; details.from and details.to give the status it was read in and the one asked for. Read the article again and, if the move still applies, send the request again. The SDK does not retry it for you, because it retries only GET requests.

Handle errors by code

import { PrismDraftError } from "@prism-draft/sdk";

type Outcome = "published" | "not approved yet" | "not allowed" | "no such article";

async function tryPublish(): Promise<Outcome> {
  try {
    await pd.articles.publish({ articleId });
    return "published";
  } catch (error) {
    if (!(error instanceof PrismDraftError)) throw error;
    switch (error.code) {
      case "CONFLICT":
        return "not approved yet"; // only an APPROVED article can be published
      case "FORBIDDEN":
        return "not allowed"; // error.details?.permission says which permission is missing
      case "RESOURCE_NOT_FOUND":
        return "no such article";
      default:
        throw error;
    }
  }
}

Tell an unavailable AI apart from a failed run:

import { PrismDraftError } from "@prism-draft/sdk";

try {
  await pd.generation.start({ articleId });
} catch (error) {
  if (error instanceof PrismDraftError && error.code === "GENERATION_FAILED") {
    const reason = error.details?.reason; // "ai_paused" | "ai_not_configured"
    const capability = error.details?.capability; // "text" | "image" | "embedding" | "search"
    console.log(`AI unavailable: ${String(reason)} (${String(capability)})`);
  } else {
    throw error;
  }
}

Read the validation issues of a refused request:

import { PrismDraftError } from "@prism-draft/sdk";

interface Issue {
  path?: string;
  message?: string;
  construct?: string;
  line?: number;
}

function issuesOf(error: PrismDraftError): Issue[] {
  const issues = error.details?.issues;
  return Array.isArray(issues) ? (issues as Issue[]) : [];
}

try {
  await pd.articles.create({ body: { projectId, title: "" } });
} catch (error) {
  if (error instanceof PrismDraftError && error.code === "INVALID_REQUEST") {
    for (const issue of issuesOf(error)) console.log(`${issue.path}: ${issue.message}`);
  } else {
    throw error;
  }
}

Other things a call can throw

A PrismDraftError means the API answered. Other failures are different errors:

Thrown When
PrismDraftTimeoutError An attempt took longer than timeoutMs. It is an Error with timeoutMs, not a PrismDraftError: the API never answered. See Retries and timeouts.
The signal's reason You aborted the call with your signal. For controller.abort() with no argument that is an AbortError.
The error fetch rejected with The request did not reach the API (DNS, connection reset). A GET is retried first; see Retries and timeouts.
TypeError A constructor option is invalid, or a path parameter is empty, . or ... These are mistakes in your code, raised before any request.
WebhookSignatureError, WebhookPayloadError, UnknownWebhookEventError A webhook delivery you verify. See Webhooks.
import { PrismDraftError, PrismDraftTimeoutError } from "@prism-draft/sdk";

try {
  await pd.articles.get({ articleId }, { timeoutMs: 5_000 });
} catch (error) {
  if (error instanceof PrismDraftTimeoutError) {
    console.log(`no answer within ${error.timeoutMs} ms`);
  } else if (error instanceof PrismDraftError) {
    console.log(error.status, error.code, error.message);
  } else {
    throw error;
  }
}