Guides

    The request ID lifecycle and its terminal states

    A generation is a request ID plus three states. Know when to stop polling, what a completed payload contains, and what it will never include.

    Versely Team9 min read

    A Versely generation is not "a file that appears". It is a request_id plus a small state machine with three values: generating, completed, failed. Two of those are terminal. The third is the only one that deserves another poll. Everything else you might want to know (the model, the CDN URL, whether a retry is even suggested) hangs off that ID, and only after the row has settled.

    If you poll without a stop condition, you will spend per-key RPM on a job that finished. If you stop on the wrong signal, you will treat an in-flight clip as a failure and generate it again. The lifecycle below is the whole contract. The developer page is where the routes live; this is what the ID you got back actually means.

    What a request ID is

    On a successful POST to /api/v1/generate/image or /video (and the sibling generate routes), the response carries a request ID somewhere in the body. The field name is not one thing. Across providers it shows up as request_id, requestId, task_id, taskId, occasionally job_id. Take the first non-empty string in that set and persist it. That string is the only argument GET /api/v1/status/:requestId will accept.

    The ID is scoped to the calling user, not to the key that submitted it. A status read with a credential for a different account returns 404 with { "success": false, "error": "Generation not found for this request_id" } even when the ID is real. That 404 is not "still starting". It is "not yours, or not a generation row". Another vsk_ key for the same user can poll it.

    Under the hood the ID is stored on one of four tables: images, videos, audios, music. The status endpoint searches them in that order, owned by your user id, and reports type as images / videos / audios / music. You do not pick the table. You do not pass type in. If you submitted a video and the status says videos, you are looking at the right row.

    A batch of several models in one POST is several IDs, not one. Fan-out is real: model may be a string or an array, and each dispatched model gets its own row and its own ID. Store them all. Poll them all. One completed in a set of four is not the batch finishing.

    If the POST body already includes an output URL, persist it and skip the poll. Video responses generally will not. Code for the poll path anyway.

    The three states

    GET /api/v1/status/:requestId returns { status } as one of three strings. There is no queued, no processing, no cancelled on this endpoint.

    generating. The row exists and is_generating is still true. You get type, model (when the table has one; music rows may not), and created_at. You do not get result_url. You do not get error. Poll again.

    completed. is_generating is false and at least one CDN URL was found. You get:

    • result_url: the newest URL, or null if somehow empty (in practice a completed response has one)
    • result_urls: up to five URLs, newest first
    • type, model, created_at

    You do not get error, transient, or retry_suggested. Those keys are omitted, not set to false. Branch on status === "completed", then take the URL. Do not look for a success boolean inside the status object beyond the top-level success: true that means "the status call itself worked".

    failed. is_generating is false and no URL was found. You get a sanitized error string, plus transient (boolean) and retry_suggested (the same boolean). The raw provider reason is not in the payload. It is classified on Versely's side and then discarded from the response on purpose, so you do not see vendor names, hostnames or stack traces. Treat failed as terminal for your worker. The status string is derived (not generating, no URL), not a sticky column, so do not keep polling in hope of a resurrection. If you need the output, that is a new POST.

    HTTP 404 is not a fourth state. It means the lookup found no row. After a POST, a very short 404 can be a race (the write is not visible yet); waiting two seconds and trying once more is reasonable. After that, fail. Spinning on 404 will not create the row.

    You observe Terminal? Poll again? Start a new POST?
    generating No Yes No
    completed Yes No Only if you want a different take
    failed Yes No Only if retry_suggested is true, and it is a new job
    HTTP 404, first time, just after POST Unknown Once, shortly No
    HTTP 404, persistent N/A No No, fix the ID or the key

    What completed contains, and what it does not

    result_url is a Versely CDN host (img.versely.studio, videos.versely.studio, audio.versely.studio). It is the uploaded copy, not a provider cache URL. Download it, or hand it to the next stage, but do not assume it is a permanent cold-storage contract beyond "this is the URL the product itself uses". If you need the bytes in your bucket, copy them.

    result_urls exists because some jobs produce more than one file. The endpoint returns at most five, newest first. If you only store result_url you are storing the first of those. That is usually what you want. If you asked for a set, store the array.

    model is whatever was recorded on the row. For a multi-model POST, each ID has its own model. Do not take the model off ID one and attribute it to ID two.

    created_at is when the row was created, which is when the job was accepted, not when the pixels landed. Do not use it as "finished at". Completed responses do not include a separate completed_at.

    A completed response does not include captions, an EDL, a social post id, or a credit receipt. Status is status. If you need the charge, look at credit history in the product; if you need an edit, that is a different route, on the video editor or the image tool. If you need to publish, that is a social post, and it is a different limiter.

    type is a hint for your own storage, not a file extension. videos still needs you to inspect the URL.

    When to stop

    Stop polling when status is completed or failed. That is the rule. Everything else is commentary.

    Do not stop because your timer fired. Your timer is about your worker, not the generation. Mark your row unknown and sweep it later. The Versely row may still be generating and may still complete. Dub queues have the same trap: a timeout is an ops event, not a provider failure.

    Do not stop because you saw an output field on a different ID in the same batch. Fan-out IDs complete independently.

    Do not continue because failed.error contains the words "try again". That sentence is a sanitized category. The boolean you want is transient (and retry_suggested, which copies it). If you decide to try again, that is a new POST, a new request ID, and another credit charge. The old ID stays failed. There is no "resume this request ID" call on the status endpoint.

    Do not poll from a chat turn if the initiator is unattended. CLI, MCP or API is the initiator question; an unattended worker should hold the ID in a datastore and poll from a cron or a queue consumer, not from a model that will wander off.

    Cadence, once you are polling the right ID: wait a second after POST, then 2s / 4s / 8s, cap around 8 to 10s. Status is not on the generate 30/min limiter, but it is on the API key RPM. A polite loop is how you leave room for the next submit.

    Every accepted generate costs credits. There is no free API allowance, and a status poll will not refund a job you have decided you do not want. Stop polling when you have the answer; do not keep the ID "warm".

    FAQ

    Can a failed ID become completed if I wait longer?

    Do not wait. failed means this poll found no URL. Treat that as the end of the ID for your worker. A late URL attach is not a recovery path you should design for. If you still need the output, submit a new POST and take the new request ID.

    Why is model missing on a music status payload?

    The music table does not always carry a model column the way images and videos do. The status handler reads record.model and will omit or leave it undefined when it is not there. Branch on type and request_id, not on model being present.

    Does completed mean the file is already on my side?

    It means Versely has a CDN URL for it. Bytes are at result_url. If your pipeline needs the file in S3 or on a frame server, that copy is your job. Status will not push it.

    What if POST succeeded but I dropped the request ID?

    Look the row up in the library by time and prompt if you must, but that is a recovery tool, not a design. For image and video, a client-minted batch_id on the original POST is the handle you should have kept: the same value can be used to find the rows again. Without that, you are grepping your own logs. Persist the ID before you return success to your caller.