Guides

    Transient or permanent: classifying failures

    A failed generation is terminal for that ID. Use the transient flag to decide if a new job is worth the credits, and stop retrying work that cannot succeed.

    Versely Team8 min read

    Not every failure deserves a retry, and on Versely a retry is never "the same request ID, please try again". Once GET /api/v1/status/:requestId says failed, that ID is done. A second attempt is a new POST, a new ID, and another credit charge. The only question left is whether that new POST has any chance of succeeding.

    The status payload answers it with two booleans that always agree: transient and retry_suggested. Read those. Do not parse the sanitized error string for the word "again" and call that a policy. The error text is written for a human. The flags are written for a client.

    Read the flags, not the prose

    A failed status body looks like this:

    {
      "success": true,
      "status": "failed",
      "type": "videos",
      "model": "…",
      "result_url": null,
      "result_urls": [],
      "created_at": "…",
      "error": "The service is experiencing high demand. Please try again in a few minutes.",
      "transient": true,
      "retry_suggested": true
    }
    

    success: true means the status call worked. The generation did not. Branch on status, then on transient.

    How transient is decided: Versely stashes the upstream failure reason for an hour and classifies the raw text. The classifier is narrow on purpose. It is true only when that raw reason contains try again, internal error, or temporary. Anything else, including an empty stash, is false. The response then replaces the raw text with a sanitized category so you never see provider names, hostnames or stack traces.

    That split is the whole trick. The flags run on the raw reason. The sentence you see does not. Several sanitized categories include "Please try again" as UX copy (timeouts, high demand, empty output, even some service issues). If you regex the sentence you will retry permanent failures. If you trust transient, you will not.

    retry_suggested is not a second opinion. It is set to the same value as transient. You can read either; you should not invent a disagreement between them.

    After about an hour the stash expires. A later status read of the same failed ID can still say failed with a generic error and transient: false, because there is no raw reason left to classify. Classify on the first failed poll, persist the booleans on your side, and do not re-interpret them an hour later.

    Failures you should not retry

    These are permanent for the request you sent. A new POST with the same body will fail the same way, and it will cost credits again. Every generation on Versely costs credits; there is no free API allowance to burn on a loop.

    The input is the problem. HTTP 400 on the original POST: missing model, a model that is not on any provider, an image-required model with no image, a body the validator rejected. Status never mints an ID for these. Fix the body.

    Content safety. The sanitized error tells you to adjust the prompt or the media. The raw classifier does not treat a policy rejection as try again / internal error / temporary, so transient should be false. Changing nothing and resubmitting is how you pay for the same refusal. Rewrite the prompt, swap the reference, or pick a different shot. The diagnostic tree for generations that come back wrong is the creative version of this; the API version is "do not resubmit the identical payload".

    Auth and scope. 401 (missing, invalid, revoked or expired key) and 403 (key lacks the scope the path requires, or the credit middleware found a zero balance). Waiting does not mint a key. A generate call needs the generate scope; a social post needs post. Fix the credential.

    Insufficient credits. The credit middleware rejects an empty balance with { "success": false, "error": "Insufficient credits" }. Individual jobs can also refuse when the balance cannot cover that job. Neither case is a rate limit, and neither is transient. Check the balance, top up from pricing, then send a new request. Retrying into a zero balance is a tight loop with a 100% fail rate.

    A failed ID with transient: false. That is the status endpoint telling you the upstream reason did not look like a blip. Do not A/B test that conclusion by spending three more charges.

    Your own 404 after a stable miss. Wrong ID, wrong user, ID never persisted. A new generate will not find the old one either.

    A 429 is not on this list. A 429 means the call was not accepted. There is no ID, no charge, and a retryAfter in seconds. Wait, then retry the same HTTP request. That is pacing, not a generation retry.

    Failures you may retry as a new job

    Only when you have a failed status with transient: true (or a POST that never created an ID because the transport died, which is a different path).

    Transient generation failure. New POST, new ID, same payload, after a short wait. Cap it. Two retries is a policy; ten is a leak. Persist that you already retried so a second worker does not help.

    High demand / internal / temporary in the raw reason. That is exactly what the classifier is for. Space the new POST by a minute, not by 50 ms. You are competing with the same upstream pressure that produced the first failure.

    Empty output, when flagged transient. Sometimes a provider returns success-shaped nothing. If transient is true, one more attempt is fair. If it is false, stop and inspect the prompt and the model rather than buying another blank.

    Network failure on the POST itself, before you have an ID. You do not yet know whether the server accepted the job. That is not a transient flag (you have no status body). It is a duplicate-submit risk, and it is the reason image and video generate accept a client batch_id. Without that, prefer "look the job up" over "send it again". The companion problem to this post is not retrying; it is not charging twice.

    A retry is also the wrong tool when the generation succeeded and you simply do not like the clip. That is a new creative take, budgeted as one. Reroll rates is how you plan for those. Edit or redo a previous generation is how you do it in the product without pretending the first ID failed.

    Credits and the cost of guessing

    The expensive client is not the one that retries a transient job once. It is the one that treats every error string as a green light. Three patterns show up in real logs:

    1. Regex on "try again". Sanitized copy uses that phrase for categories the classifier did not mark transient. You resubmit a bad voice id, a bad aspect ratio, a safety rejection. Each one charges.
    2. Retrying the ID. PUT something, or POST with the same request_id in the body, hoping to resume. There is no such call. The old ID stays failed. If you also send a new generate, you now have two charges, one of them wasted on a misunderstanding.
    3. No cap. A worker that retries until success will retry until the balance is empty. Put a maximum on transient retries per job you track on your side, record it next to the request ID, and dead-letter the rest.

    Tracking credits per client deliverable is easier if failed-and-retried jobs are first-class rows in your own table: original ID, flags, retry ID, charge accepted. Guessing from the credit ledger after the fact is how an agency writes off a day of generations as "API issues".

    When the output shipped and was wrong in a content sense, that is not this classifier either. What to do when a generated asset ships wrong is the ops path. transient will not be true because a logo was misspelled.

    Rule you can implement in one if:

    if (body.status === "failed") {
      stopPolling(requestId);
      if (body.transient && retryCount < 2) {
        enqueueNewGenerate(originalPayload); // new POST, new ID, new charge
      } else {
        deadLetter(requestId, body.error);
      }
    }
    

    That is the whole policy. The flags are the classifier. Your retry count is the budget.

    FAQ

    The error says "please try again" but transient is false. Which do I trust?

    transient. The sentence is a sanitized category. The boolean ran on the raw upstream reason before that category was chosen. If they disagree, the boolean is the one built for clients.

    Is a 500 on POST the same as a transient generation failure?

    No. A 500 on POST means the HTTP call itself failed. You may not have an ID. Treat it as an unknown submit: if you sent a batch_id on image or video, retrying the same POST is absorbed; if you did not, do not blindly resubmit. A transient failed status means the job was accepted, charged, and settled as a failure. Different recoveries.

    Will retry_suggested ever be true when transient is false?

    Not on this endpoint. retry_suggested is assigned transient. If you need a more interesting policy (retry empty-output even when the flag is false, never retry safety even if a provider said "temporary"), that is your policy, on top of the flags, not a disagreement the API will express.

    Does a transient retry reuse the credits from the failed job?

    No. A failed generation that settled is a settled charge, refunds included only when Versely's own billing path refunds it. Your client's retry is a new generate. Budget it as one. There is no unpaid allowance to absorb the experiment.