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
| Code | Meaning | Common cause | Fix |
|---|---|---|---|
400 | Bad Request | Unreadable JSON body, missing parameter or value out of range. | Check the format of the parameters. The error field names precisely the one causing trouble. |
401 | Unauthorized | API key missing, invalid or revoked. | Check the Authorization: Bearer sr_live_… header. |
402 | Payment Required | Not enough credits: empty balance, or below the cost of the requested action. | Top up at subreply.io/billing. |
422 | Unprocessable | Reddit post locked, archived or deleted, or publishing account unavailable. | Pick a recent, commentable post. Retrying as-is will change nothing. |
500 | Internal Server Error | AI pipeline or scraping failure. | Retry in 30 s; contact support if the error persists. |
503 | Service Unavailable | Publishing 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.