Invoking an app
Call an endpoint synchronously or asynchronously, read the task back, and know which failures are worth retrying.
Introduction
Every call to an app is a task, whether you wait for it or come back later. The route you choose decides only whether the connection stays open.
POST /v1/apps/{appId}/invoke-sync/{endpointPath}
POST /v1/apps/{appId}/invoke-async/{endpointPath}Everything the platform needs is in the URL and the method: the app, the endpoint, and whether you are waiting. The method itself is fixed: every invocation is a POST.
The envelope
The body is an envelope with exactly two members, and both are required.
{
"taskId": "8f14e45f-ceea-467a-9a2f-6d2b1f2c0b3e",
"payload": { "prompt": "a red bicycle" }
}Your own fields go inside payload, where they can never collide with a platform field. The endpoint's schema describes the payload alone, so the envelope can grow a member later and your app keeps working.
The envelope is closed. A stray member at the top level is rejected rather than ignored, which is what keeps that promise true.
Making the call
import { createClient } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const task = await client.invoke({
appId: 'image-tools',
endpointPath: 'generate',
payload: {
prompt: 'a red bicycle'
}
})import asyncio
import os
from runware import Runware
async def main():
client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
try:
task = await client.invoke(
"image-tools",
"generate",
{
"prompt": "a red bicycle"
},
)
finally:
await client.close()
asyncio.run(main())curl https://api.serverless.runware.ai/v1/apps/image-tools/invoke-async/generate \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"prompt": "a red bicycle"
}
}'echo '{"prompt":"a red bicycle"}' | runware serverless apps invoke image-tools generate -f -{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"prompt": "a red bicycle"
}
}Both SDKs generate the task id and poll a 202 through to the result, so the call above covers everything on this page. The CLI prints the id instead, and takes --wait when you want it to poll for you.
The task id is yours
You generate taskId yourself, as a canonical lowercase UUID, and you know it before the call returns. Two things follow.
A lost response costs you nothing. Send the same id again and you get back the task it already names rather than a second run, so a retry after a dropped connection cannot be charged twice.
Reusing an id for different work returns the first task, not the new one. Keeping each id unique is your job.
A resubmitted task id is answered with the task it already names even when that task belongs to a different app. The response carries the owning appId, so poll under that one.
What comes back
Both routes return a Task.
{
"id": "8f14e45f-ceea-467a-9a2f-6d2b1f2c0b3e",
"appId": "image-tools",
"status": "completed",
"endpointPath": "generate",
"createdAt": "...",
"output": { "image": "iVBOR..." },
"error": null
}id, status, appId, endpointPath and createdAt are always present. output and error are both optional and both nullable, so status is the field to branch on.
output is whatever your handler returned, and it does not have to be an object. An array, a string, a number or a boolean comes back as it was, so a container app that answers with a bare value is read the same way as one that answers with a map.
Choosing a route
| Route | Returns | Use it when |
invoke-sync | 200 with the finished task, or 202 with a pending one if the work outlives the wait window | The work is short and you want the result inline |
invoke-async | 202 with a pending task | The work is long, or you are submitting many and collecting later |
A 202 from invoke-sync is not a failure and not a timeout. The task is queued or running, and the work continues. You have its id, so poll for the result exactly as you would an asynchronous one. Do not resubmit, because that is how you pay for the same GPU work twice.
Asking for the synchronous route looks like this.
import { createClient } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const task = await client.invoke({
appId: 'image-tools',
endpointPath: 'generate',
payload: {
prompt: 'a red bicycle'
}
}, {
deliveryMethod: 'sync'
})import asyncio
import os
from runware import InvokeOptions, Runware
async def main():
client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
try:
task = await client.invoke(
"image-tools",
"generate",
{
"prompt": "a red bicycle"
},
options=InvokeOptions(delivery_method="sync"),
)
finally:
await client.close()
asyncio.run(main())curl https://api.serverless.runware.ai/v1/apps/image-tools/invoke-sync/generate \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"prompt": "a red bicycle"
}
}'echo '{"prompt":"a red bicycle"}' | runware serverless apps invoke image-tools generate --sync -f -{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"prompt": "a red bicycle"
}
}Both SDKs and the CLI take the asynchronous route unless told otherwise, so this is the only call on the page that names one.
Both routes hand back the same shape, so one code path serves both.
Reading a task back
GET /v1/apps/{appId}/tasks/{taskId}Poll until status is terminal. GET /v1/apps/{appId}/tasks lists recent tasks for an app when you have lost an id or want to see what ran.
A task that reaches failed carries the reason in error as a plain string with no code and no structured cause, so there is nothing to switch on. Surface it to whoever has to debug the handler.
When an app cannot take work
A call to an undeclared endpoint is rejected before anything is queued, so an unknown path creates no task and costs you nothing.
Beyond that, the app's own state decides.
| State | Result |
initializing, active, stopping | Accepted |
initializing with no version yet | 409, and it clears once the first build finishes |
active with nothing able to serve | 503, retryable, no task created |
stopped, deleting, failed | 409. Resume a stopped app before invoking it |
503 is the only one worth retrying blindly. No task is minted, so a retry is not a duplicate. Everything else needs you to change something first.
Full detail on every failure shape is in Errors.