---
title: Two endpoints, two shapes | Runware Docs
url: https://runware.ai/docs/serverless/examples/image-tools
description: One app whose endpoints take request shapes with nothing in common, so no single schema covers both.
relatedDocuments:
  - https://runware.ai/docs/serverless/writing-a-model
  - https://runware.ai/docs/serverless/invoking
---
## Introduction

`generate` takes a prompt and `upscale` takes an image, so **no single request schema covers both**. Each handler's signature becomes its own, and **the platform routes by path**.

What they do share is the app: one queue, one worker pool, one GPU type. That is set on the app rather than in the file, which is why nothing here names hardware or scaling. **Two endpoints that genuinely need different GPUs are two apps.**

## The app

**model.py**:

```python
"""Two endpoints on one app, with request shapes that have nothing in common."""

from __future__ import annotations

from runware_serverless import endpoint, serve

PIXEL_PNG = (
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
)
"""A 1x1 white PNG, base64. Stands in for generated pixels."""

BASE_SIZE = 64
DEFAULT_STEPS = 4
DEFAULT_SCALE = 2

@serve
class ImageTools:
    def load(self) -> None:
        pass

    @endpoint
    def generate(self, prompt: str, steps: int = DEFAULT_STEPS) -> dict[str, object]:
        # An omitted optional field arrives as an explicit null, so the default
        # is applied here as well as in the signature.
        steps = DEFAULT_STEPS if steps is None else steps
        return {
            "handler": "generate",
            "prompt": prompt,
            "steps": steps,
            "width": BASE_SIZE,
            "height": BASE_SIZE,
            "image": PIXEL_PNG,
        }

    @endpoint
    def upscale(self, image: str, scale: int = DEFAULT_SCALE) -> dict[str, object]:
        scale = DEFAULT_SCALE if scale is None else scale
        return {
            "handler": "upscale",
            "scale": scale,
            "width": BASE_SIZE * scale,
            "height": BASE_SIZE * scale,
            "image": image,
        }
```

## Deploying it

```bash
runware serverless deploy model.py --id image-tools --gpu-type l40s
```

## Invoking it

Both endpoints are on the same app, so **only the last segment of the path changes**.

**TypeScript**:

```typescript
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'
  }
})
```

**Python**:

```python
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**:

```bash
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"
    }
  }'
```

**CLI**:

```bash
echo '{"prompt":"a red bicycle"}' | runware serverless apps invoke image-tools generate -f -
```

**JSON**:

```json
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payload": {
    "prompt": "a red bicycle"
  }
}
```

`upscale` takes an image instead, and **nothing in its body carries over** from the call above.

**TypeScript**:

```typescript
import { createClient } from '@runware/sdk'

const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })

const task = await client.invoke({
  appId: 'image-tools',
  endpointPath: 'upscale',
  payload: {
    image: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=',
    scale: 2
  }
})
```

**Python**:

```python
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",
            "upscale",
            {
                "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=",
                "scale": 2
            },
        )
    finally:
        await client.close()

asyncio.run(main())
```

**cURL**:

```bash
curl https://api.serverless.runware.ai/v1/apps/image-tools/invoke-async/upscale \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payload": {
      "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=",
      "scale": 2
    }
  }'
```

**CLI**:

```bash
cat <<'EOF' | runware serverless apps invoke image-tools upscale -f -
{
  "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=",
  "scale": 2
}
EOF
```

**JSON**:

```json
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payload": {
    "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=",
    "scale": 2
  }
}
```

`steps` and `scale` both have defaults in their signature, so you can leave them out. An omitted optional field **arrives as an explicit null** rather than as a missing key, which is why each handler applies its default a second time.

## Testing it before you deploy

**test\_model.py**:

```python
"""Each endpoint is a method, so each test is a call."""

from model import BASE_SIZE, PIXEL_PNG, ImageTools

def test_generate_reports_its_own_handler() -> None:
    model = ImageTools()
    model.load()

    result = model.generate("a red bicycle")

    assert result["handler"] == "generate"
    assert result["steps"] == 4
    assert result["image"] == PIXEL_PNG

def test_upscale_takes_a_different_shape_entirely() -> None:
    model = ImageTools()
    model.load()

    result = model.upscale(PIXEL_PNG, scale=4)

    assert result["handler"] == "upscale"
    assert result["width"] == BASE_SIZE * 4

def test_each_handler_applies_its_own_default() -> None:
    model = ImageTools()
    model.load()

    assert model.generate("a red bicycle", steps=None)["steps"] == 4
    assert model.upscale(PIXEL_PNG, scale=None)["scale"] == 2
```

The whole example is at [`image-tools/`](https://github.com/Runware/runware-serverless-examples/tree/main/image-tools), with a README of its own.