SubReply

Reference

Error codes

Every endpoint returns the same codes, with the same body shape. This page says what to do with each one.

Full reference

CodeMeaningCommon causeFix
400Bad RequestUnreadable JSON body, missing parameter or value out of range.Check the format of the parameters. The error field names precisely the one causing trouble.
401UnauthorizedAPI key missing, invalid or revoked.Check the Authorization: Bearer sr_live_… header.
402Payment RequiredNot enough credits: empty balance, or below the cost of the requested action.Top up at subreply.io/billing.
422UnprocessableReddit post locked, archived or deleted, or publishing account unavailable.Pick a recent, commentable post. Retrying as-is will change nothing.
500Internal Server ErrorAI pipeline or scraping failure.Retry in 30 s; contact support if the error persists.
503Service UnavailablePublishing service unavailable, or no account available to post.Retry in 60 s.

202 is not an error

POST /api/v1/publish can answer 202: the request went out, the publication is not confirmed, nothing is charged. It is not a failure to retry — see publish a comment.

Error format

Every failed response carries a JSON object with a single field, error, written in French and readable by a human:

Error body
{
  "error": "Message explicite en français"
}

There is no internal error code and no details field: the HTTP status carries the category, the message carries the detail. Never parse the message text to decide on an action — branch on the status.

Handling errors in production

Three rules are enough to keep a workflow standing:

  • Retry 500 and 503, with a growing delay and two attempts at most. They are the only codes that resolve on their own.
  • Stop on 402 and notify: every following call will fail the same way until the balance is topped up.
  • Never retry 400, 401 and 422 — there is something to fix in the request, the key or the targeted post.
Client with retry and credit guard
type ApiError = { error: string };

// Only retry what is worth retrying: 503 (publishing service momentarily
// unavailable) and 500 (AI pipeline or scraping failure).
// A 400, a 401 or a 422 will not fix themselves.
const RETRYABLE = new Set([500, 503]);

async function callSubreply<T>(
  endpoint: string,
  body: unknown,
  attempt = 0,
): Promise<T> {
  const response = await fetch(`https://subreply.io/api/v1/${endpoint}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.SUBREPLY_API_KEY}`,
    },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(180_000),
  });

  if (response.ok) return (await response.json()) as T;

  const { error } = (await response.json()) as ApiError;

  // 402: out of credits. No point insisting — cut the chain and notify,
  // rather than firing off calls that will all fail.
  if (response.status === 402) {
    await notifyOutOfCredits(error);
    throw new Error(`Out of credits: ${error}`);
  }

  if (RETRYABLE.has(response.status) && attempt < 2) {
    // 30 s then 60 s: enough time for a service to come back.
    await new Promise((resolve) => setTimeout(resolve, 30_000 * (attempt + 1)));
    return callSubreply<T>(endpoint, body, attempt + 1);
  }

  throw new Error(`SubReply ${response.status} : ${error}`);
}

Replays cost nothing

Generation and publishing are billed idempotently per post URL: a second attempt on the same post returns credits_used: 0. So a retry after a timeout is free — on /publish, still check the post before replaying, so you do not leave two comments on it.