Jobs and settlement
Some provider-key calls answer with a result. Many answer with an id, and the result arrives later. This page is how those behave end to end: what binds a job to a seller, how you follow it, and exactly when money moves.
If you only read one thing: a submit is not the end of the transaction. The charge settles when you fetch the result.
Two shapes of call
What binds a job
When a submit comes back with an id, that id is pinned to the one offer that took it.
The binding lives for 27 hours. After that a follow-up answers 404 job_not_found, so collect long jobs
within a day.
Pinning to one offer is not an optimisation. The provider holds that job against that seller's key, so a poll routed to a different seller would ask the wrong account about a job it has never heard of. Routing happens once, at submit.
If a binding cannot be written, the submit still succeeds - the provider already has the job, so you still get your id - but the follow-ups will 404 rather than reach another seller. The failure is logged on our side. It is the safe direction to fail: a job you cannot collect costs you nothing, whereas a poll on the wrong key would be a correctness bug.
Following a job
This is the part that differs by provider, and it is the one thing you have to get right.
| Can they… | Follow-up URLs | What you do |
|---|---|---|
| Wavespeed, ElevenLabs (path style) | Yesrewritten to our route | Yesfollow them as given |
| Fal (header style) | Noreturned untouched | Partialsend the target in x-fal-target-url |
Path-style: just follow the URL
A Wavespeed submit returns data.urls.get pointing at api.wavespeed.ai. Before that reaches you, we replace
it with the same path on our route:
https://api.wavespeed.ai/api/v3/predictions/{id}/result
↓ rewritten, in the submit response you receive
$SURPLUS_BASE_URL/resources/wavespeed/api/v3/predictions/{id}/resultThree fields are rewritten where a provider returns them - status_url, response_url and cancel_url - plus
data.urls.get. Only URLs on the provider's own hosts are touched; output and CDN links are left exactly as
they are, so a signed artifact URL still works.
Follow the URL you were given and everything stays on your key and your offer. Call the provider's host directly and you have no credential there.
Fal: send the target in the header
Fal's SDK already knows how to build …/requests/{id}/status and proxies each target as a header, so rewriting
would fight it. Keep the path empty and move the target:
curl $SURPLUS_BASE_URL/resources/fal \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "x-fal-target-url: https://queue.fal.run/fal-ai/flux/dev/requests/$ID"
When money moves
- SubmitQuoted; hold = quote + 25%
- PollNo hold, no charge
- Result fetchedProvider's reported usage read
- Chargedmin(reported, quote); hold released
Four rules, and all four are in your favour:
- A failed call costs nothing. Not the quote, not a partial - zero. The hold is released.
- The quote is a ceiling. You are charged the lower of two numbers: the provider's reported usage, or the quote. A report above the quote means our estimator was wrong, and that is not your bill - the seller absorbs the difference.
- No report means the quote. For a bounded unit the quote already is the ceiling.
- Polling is free. A status check places no hold and makes no charge. Only the result settles.
The hold carries a 25% buffer over the quote so a call that prices slightly higher than estimated still has room to capture. The buffer is held, never charged - what you pay is rule 2.
A replay is a second generation
The relay carries no idempotency, deliberately. Idempotency-Key is accepted, but replaying a provider-key
call is a second real generation at the provider, and it is billed as one.
This is the opposite of the money-moving endpoints, where a replay is explicitly safe. The difference is that a topup replayed twice should move money once, whereas a generation replayed twice has genuinely produced two images - and refusing to bill for the second would mean the seller pays for it.
So: retry a failed call freely, since a failure costs nothing. Do not retry a call that succeeded.
Job states are not outcomes
Neither live provider reports failure the way you would expect, so we confirm both the same way you should: on the result, not the status.
| Provider | Status field | Reads done at | Real outcome |
|---|---|---|---|
| Fal | status | COMPLETED | The result: 2xx succeeded, 4xx failed. |
| Wavespeed | data.status | completed | The result read. |
statusReads done at · COMPLETEDReal outcome · The result: 2xx succeeded, 4xx failed.data.statusReads done at · completedReal outcome · The result read.A Fal job with invalid input queues happily, reads COMPLETED, and then answers 422 on its result with
x-fal-billable-units: 0. That is why the result is the settlement trigger: it is the first point where the
truth is knowable, and it is free when the answer is "it failed".
Being told when a job finishes
Instead of polling, ask for a webhook. There are two ways, and you can use both.
On the submit itself. Pass your URL in the provider's own webhook parameter, exactly as you would calling the provider directly. Surplus checks it (https, a public host), keeps it for this job, and never forwards it: the provider only ever calls Surplus. The response is the provider's own, plus one header carrying this job's signing secret:
curl -i "$SURPLUS_BASE_URL/resources/wavespeed/api/v3/wavespeed-ai/flux-schnell?webhook=https%3A%2F%2Fexample.com%2Fhooks%2Fjob" \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt":"a red cube"}' # x-surplus-webhook-secret: kmwhsec_… (sent once, only here)
A URL that fails the check is refused with 400 invalid_webhook_url before the job starts, and if we cannot set up its
webhook you get a retryable 503 webhook_unavailable, also before anything starts. In the rare case the job starts but
its webhook could not be set up, the response carries x-surplus-webhook-status: unavailable instead of the secret
(also on a job your workspace subscribes to): poll that job. If you moved over from the provider, the one change is verifying Surplus's signature with this secret
instead of the provider's.
For every job in a workspace. Owners and admins create a subscription once:
curl -X POST $SURPLUS_BASE_URL/webhooks \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/hooks/surplus","event_types":["resource.job.completed"]}'
Either way, each job sends one event when it reaches its outcome, in the same body for every provider:
{
"type": "resource.job.completed",
"created_at": "2026-10-23T12:00:20.000Z",
"data": {
"job_id": "9f2c4b1e3a",
"request_id": "01J9ZZZZZZZZZZZZZZZZZZZZZZ",
"resource": "wavespeed",
"model": "wavespeed/api/v3/wavespeed-ai/flux-schnell",
"status": "succeeded",
"submitted_at": "2026-10-23T12:00:00.000Z",
"completed_at": "2026-10-23T12:00:20.000Z",
"outputs": ["https://…/output.jpeg"],
"error": null,
"charged_micro": 2700
}
}status is succeeded (charged), failed or canceled (nothing charged), or expired (no outcome could be confirmed
within a day; nothing charged). charged_micro is what you paid, in millionths of a dollar. outputs lists the output
URLs where the provider names them; the full result is always one free fetch away through your usual follow-up URL.
error is null, or a Surplus code and message.
Deliveries are signed (x-surplus-signature, HMAC-SHA256 of {t}.{body}), retried when your endpoint fails, and never
follow a redirect. They are at least once: dedupe on request_id, which is unique per job.
How fast it arrives depends on the provider. Wavespeed calls us the moment a job finishes, so the event follows within seconds. On providers that cannot call back, or not yet (Fal, Uncensored), the event goes out when your next poll or our own check (every few minutes) sees the outcome.
This is being enabled. Until it is on for your account, keep polling; a slow poll is a sensible fallback anyway.
Related
- Your first call - the end-to-end path.
- Fal - the queue route and the header style.
- Wavespeed - tasks and the result read.
- Pricing - how a discount is applied and what it comes off.
- Provider key endpoints - the endpoint reference.