Skip to main content
When a request fails, the Wibble API returns a non-2xx HTTP status and a JSON body describing what went wrong. The HTTP status tells you the broad category; the error.code field gives you a stable identifier to branch on in your code.

The error envelope

Every error response shares the same shape: an error object with a machine-readable code and a human-readable message.
{
  "error": {
    "code": "not_found",
    "message": "Job not found."
  }
}
Branch on error.code, not on message. Messages are written for humans and may change; codes are part of the contract. Some errors add fields alongside error at the top level of the body. For example, a 402 insufficient_words response includes your current balance and the words_required for the request:
{
  "error": {
    "code": "insufficient_words",
    "message": "This request needs 240 API words, but the account only has 120 remaining."
  },
  "balance": 120,
  "words_required": 240
}
The table below lists which codes carry extra fields, and the examples further down show their full bodies.

Error reference

StatusCodeWhen it happensExtra fields
400bad_requestThe request body was not valid JSON.
400validation_errorA field failed schema validation, including a rejected webhook_url.
400word_limit_exceededtext exceeds the 2,000-word limit.
401unauthorizedThe API key is missing, invalid, expired, or revoked, or lacks the humanize scope.
402insufficient_wordsYour word balance cannot cover the request.balance, words_required
404not_foundNo job with that id exists on your account.
409idempotency_conflictAn Idempotency-Key was reused with a different request body within 24 hours.existing_job_id, expires_at
413payload_too_largeThe request body is over 128,000 bytes.
429rate_limitedYou exceeded the per-key rate limit (20 POST/min, 120 GET/min).retry_after_seconds, reset_at
429too_many_concurrent_jobsYou have more than 5 active jobs at once.active_jobs, limit
500enqueue_failedThe job was created but could not be enqueued. Reserved words are refunded automatically.
Both 429 codes also send a Retry-After header. The 401 unauthorized response does not include rate-limit headers.

Per-code detail

The request body could not be parsed as JSON. Check for trailing commas, unquoted keys, or a missing Content-Type: application/json header. Fix the body before resending; retrying the same payload returns the same error.
A field is missing or has the wrong type or value. This also covers a webhook_url that is rejected for not being HTTPS, not being publicly reachable, containing embedded credentials, or resolving to a private or loopback address. Correct the field and resend.
text is over the 2,000-word limit for a single request. Split the input into smaller chunks and submit them as separate jobs.
The Authorization header is missing or the key is invalid, expired, revoked, or lacks the humanize scope. Confirm you are sending Authorization: Bearer wib_live_... and that the key is active in the dashboard. This response carries no rate-limit headers.
Your balance cannot cover the request. The body includes your current balance and the words_required. Buy more API words, then resend. See Words and billing.
No job with the given id exists on your account. Check the id, and confirm you are using the same account that submitted the job. Job ids are scoped to the account that created them.
You reused an Idempotency-Key with a different request body within the 24-hour retention window. The body includes the existing_job_id that the key is already bound to and its expires_at. Use a fresh key for a new request, or resend the original body. See Idempotency.
The request body is over the 128,000-byte limit. This can happen before the word limit is reached if the text contains many multi-byte characters. Reduce the body size and resend.
You exceeded the per-key rate limit (20 POST per minute, 120 GET per minute). The body includes retry_after_seconds and reset_at, and the response carries a Retry-After header. Wait, then retry. See Rate limiting.
You have more than 5 active jobs at once. The body includes active_jobs and limit, and the response carries a Retry-After header with a value of 10. Wait for jobs to finish, then submit again.
The job was created but could not be enqueued for processing. Reserved words are refunded automatically, so you are not charged. This is transient; retry the submission after a short delay.

Handling guidance

Sort errors into two groups: those that are safe to retry as-is, and those that need a change to the request before it can succeed.

Safe to retry

These are transient. Retry the same request after waiting.
  • 429 rate_limited and 429 too_many_concurrent_jobs — back off and retry. Honor the Retry-After header. See Rate limiting.
  • 500 enqueue_failed — the reserved words were refunded, so a retry does not double-charge you. Retry after a short delay.
When you retry a POST /humanize, send the same Idempotency-Key you used on the first attempt. A repeated request with an identical body returns the original job instead of creating a duplicate. See Idempotency.

Needs a change

Retrying these without changing the request returns the same error. Fix the cause first.
  • 400 bad_request / validation_error / word_limit_exceeded — correct the body: fix the JSON, the failing field, or split text that is over 2,000 words.
  • 401 unauthorized — fix authentication: use an active key with the humanize scope.
  • 402 insufficient_words — buy more API words, then resend. See Words and billing.
  • 409 idempotency_conflict — use a fresh Idempotency-Key, or resend the original body for that key. See Idempotency.
  • 404 not_found — check the job id and the account.
  • 413 payload_too_large — reduce the request body below 128,000 bytes.

Rate limiting

The API enforces 20 POST /humanize requests per minute and 120 GET /humanize/{id} requests per minute, per key. A separate limit caps you at 5 active jobs at once. When you exceed a limit, the response is 429 with a Retry-After header giving the number of seconds to wait before retrying. Rate-limited responses also include these headers:
HeaderMeaning
Retry-AfterSeconds to wait before the next request.
X-RateLimit-LimitThe request ceiling for the current window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetWhen the window resets, as an ISO 8601 timestamp.
Read Retry-After and wait at least that long before retrying. If the header is absent, fall back to exponential backoff with jitter — for example, start at one second and double the delay on each attempt, adding a small random offset so concurrent clients do not retry in lockstep. To stay under the limit in the first place, watch X-RateLimit-Remaining and slow down as it approaches zero.
Reading rate-limit headers
import time

def submit_with_retry(session, payload, max_attempts=5):
    for attempt in range(max_attempts):
        response = session.post(
            "https://wibble.ai/api/v1/humanize",
            headers={"Authorization": "Bearer wib_live_..."},
            json=payload,
        )

        if response.status_code != 429:
            return response

        # Prefer Retry-After; fall back to exponential backoff with jitter.
        retry_after = response.headers.get("Retry-After")
        wait = float(retry_after) if retry_after else min(2 ** attempt, 30) + 0.5
        time.sleep(wait)

    raise RuntimeError("Rate limited after retrying")

Example bodies

{
  "error": {
    "code": "insufficient_words",
    "message": "This request needs 240 API words, but the account only has 120 remaining."
  },
  "balance": 120,
  "words_required": 240
}

Words and billing

How reservations, charges, and refunds work, and what insufficient_words means for your balance.

Idempotency

Retry submissions safely and avoid idempotency_conflict.