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

RouteReturnsUse it when
invoke-sync200 with the finished task, or 202 with a pending one if the work outlives the wait windowThe work is short and you want the result inline
invoke-async202 with a pending taskThe 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.

StateResult
initializing, active, stoppingAccepted
initializing with no version yet409, and it clears once the first build finishes
active with nothing able to serve503, retryable, no task created
stopped, deleting, failed409. 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.