Skip to main content
MarketplaceProvider Keys — jobs and settlement

Provider Keys — 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

Finishes in the responseElevenLabs sound and music, Fal on fal.run — charged the actual cost before you get the bytes
Finishes as a jobWavespeed tasks, Fal on queue.fal.run — the submit answers with an id and owes the quote

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
Providerfal, wavespeed
Job idrequest_id on Fal, data.id on Wavespeed
Your accounta job is never visible to another buyer
One offerthe seller whose key actually ran it
Three things identify a job: the provider, the id, and your account. A follow-up that does not match all three is a 404 — not another seller's key.

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.

Who rewrites your follow-up URLs
Can they…Follow-up URLsWhat you do
Wavespeed, ElevenLabs (path style)Yesrewritten to our routeYesfollow them as given
Fal (header style)Noreturned untouchedPartialsend the target in x-fal-target-url
Path-style providers return their own URLs and we rewrite them before you see them. Fal's are left alone because its SDK builds them itself and sends each target as a header.

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}/result

Three 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:

bash
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

A job's money, step by step
  1. SubmitQuoted; hold = quote + 25%
  2. PollNo hold, no charge
  3. Result fetchedProvider's reported usage read
  4. Chargedmin(reported, quote); hold released
The hold is the quote plus a 25% buffer. Polling is free. The charge lands once, at the result, and it can never exceed what was quoted.

Four rules, and all four are in your favour:

  1. A failed call costs nothing. Not the quote, not a partial — zero. The hold is released.
  2. 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.
  3. No report means the quote. For a bounded unit the quote already is the ceiling.
  4. 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.

Reading a job's real outcome
Submit
IN_QUEUE / IN_PROGRESS
Poll status
COMPLETED / completed
Reads done
back to “Poll status” — this loop can run many times
Fetch the result2xx succeeded and is charged; 4xx failed and is free
On Fal a failed job also reads COMPLETED, with no error on the status. The result is the only honest signal, and it is what settlement reads too.
FalStatus field · statusReads done at · COMPLETEDReal outcome · The result: 2xx succeeded, 4xx failed.
WavespeedStatus field · 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".