> ## Documentation Index
> Fetch the complete documentation index at: https://docs.widerouter.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 비동기 작업 API

> 생성 작업을 제출하고 1초 이내에 작업 ID를 받은 다음, 이미지가 준비되면 폴링하거나 콜백을 수신합니다.

비동기 작업 API는 두 개의 엔드포인트와 하나의 요청 엔벨로프로 구성됩니다. 작업을 제출하면 즉시 ID를 받고, 생성 작업은 WideRouter 측에서 수행됩니다. 모델이 작업하는 동안 요청에 연결된 상태를 유지할 필요가 없습니다.

<Info>
  HTTP 연결을 20–50초 동안 열어 두어야 하는 경우에는 이 API를 사용하십시오. 이미 정상적으로 작동하는 동기식 통합이 있다면 마이그레이션하기 전에
  [동기식 또는 비동기식 선택](#choosing-sync-or-async)을 참조하십시오.
</Info>

## 엔드포인트

| 메서드    | 경로                   | 용도                          |
| ------ | -------------------- | --------------------------- |
| `POST` | `/v1/tasks/submit`   | 작업을 생성합니다. 작업 ID를 즉시 반환합니다. |
| `GET`  | `/v1/task/{task_id}` | 작업 상태를 읽고, 완료되면 출력을 읽습니다.   |

생성 시 경로는 복수형이고 **조회 시에는 단수형**이라는 점에 유의하십시오. 목록 조회, 취소 또는 삭제 엔드포인트는 없습니다.

## 작업 생성

요청 본문은 항상 동일한 세 키로 구성된 엔벌로프입니다.
`model`, `input` 및 선택 사항인 `callback_url`입니다.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.widerouter.com/v1/tasks/submit \
    -H "Authorization: Bearer $WIDEROUTER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "gemini-3-pro-image",
      "input": {
        "prompt": "a small ceramic teapot on a light-grey studio backdrop",
        "aspect_ratio": "16:9",
        "image_size": "2K"
      }
    }'
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.post(
      "https://api.widerouter.com/v1/tasks/submit",
      headers={"Authorization": f"Bearer {os.environ['WIDEROUTER_API_KEY']}"},
      json={
          "model": "gemini-3-pro-image",
          "input": {
              "prompt": "a small ceramic teapot on a light-grey studio backdrop",
              "aspect_ratio": "16:9",
              "image_size": "2K",
          },
      },
      timeout=30,
  )
  task_id = resp.json()["id"]
  ```

  ```javascript Node theme={null}
  const resp = await fetch("https://api.widerouter.com/v1/tasks/submit", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WIDEROUTER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gemini-3-pro-image",
      input: {
        prompt: "a small ceramic teapot on a light-grey studio backdrop",
        aspect_ratio: "16:9",
        image_size: "2K",
      },
    }),
  });
  const { id } = await resp.json();
  ```
</CodeGroup>

응답은 세 필드로 구성되며 1초도 걸리지 않아 도착합니다.

```json theme={null}
{
  "id": "task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI",
  "status": "queued",
  "created_at": 1788104112
}
```

### 엔벌로프 필드

| 필드             | 유형  | 필수 여부 | 참고                                            |
| -------------- | --- | ----- | --------------------------------------------- |
| `model`        | 문자열 | 예     | [지원 모델](#supported-models)을 참조하십시오.           |
| `input`        | 객체  | 예     | JSON 객체여야 합니다. 문자열 또는 배열은 거부됩니다.              |
| `callback_url` | 문자열 | 아니요   | `https` URL이어야 합니다. [콜백](#callbacks)을 참조하십시오. |

현재 엔벌로프 수준의 알 수 없는 키는 허용되고 무시됩니다. 하지만 이에
의존하지 말고 생성 매개변수를 해당 위치에 넣지 마십시오. 생성 매개변수는 `input`에
포함해야 합니다.

### `input` 객체

`input`에는 생성 매개변수가 포함되며, **허용되는 필드는 모델에 따라 다릅니다**. 엔벌로프는
고정되어 있지만 내부 내용은 고정되어 있지 않습니다.

`input` 내부의 유효성 검사는 엄격하게 수행됩니다. 모델이 인식하지 못하는 키는 모두
즉시 거부되므로 오타를 조용히 넘어가지 않고 명확하게 확인할 수 있습니다.

```json theme={null}
{ "error": { "code": "invalid_params", "message": "unknown field", "param": "input.negative_prompt" } }
```

<Card title="Nano Banana 시리즈" icon="layers" href="/ko/models/nano-banana/overview">
  전체 필드 목록, 해상도 등급, 가로세로 비율 및 이미지 편집입니다.
</Card>

## 결과 폴링

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.widerouter.com/v1/task/task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI \
    -H "Authorization: Bearer $WIDEROUTER_API_KEY"
  ```

  ```python Python theme={null}
  import os, time, requests

  def wait(task_id, timeout=600):
      headers = {"Authorization": f"Bearer {os.environ['WIDEROUTER_API_KEY']}"}
      deadline = time.time() + timeout
      while time.time() < deadline:
          task = requests.get(
              f"https://api.widerouter.com/v1/task/{task_id}",
              headers=headers, timeout=30,
          ).json()
          if task["status"] in ("completed", "failed"):
              return task
          time.sleep(3)
      raise TimeoutError(task_id)
  ```
</CodeGroup>

완료된 작업은 다음과 같습니다.

```json theme={null}
{
  "id": "task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI",
  "model": "gemini-3-pro-image",
  "status": "completed",
  "created_at": 1788104112,
  "started_at": 1788104113,
  "completed_at": 1788104131,
  "expires_at": 1788190531,
  "outputs": [
    "https://r2cdn.agisuitepro.com/o/2026/08/30/task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI_0.jpg"
  ],
  "counts": { "requested": 1, "succeeded": 1, "failed": 0 }
}
```

### 작업 필드

| 필드             | 다음일 때 존재        | 참고                                                 |
| -------------- | --------------- | -------------------------------------------------- |
| `id`           | 항상              | 작업 ID입니다.                                          |
| `model`        | 읽기 시 항상         | 다시 반환되며, 생성 응답에는 없습니다.                             |
| `status`       | 항상              | `queued`, `in_progress`, `completed`, `failed`입니다. |
| `created_at`   | 항상              | Unix 초, UTC입니다.                                    |
| `started_at`   | `in_progress`부터 | 워커가 작업을 가져간 시점입니다.                                 |
| `completed_at` | 종료 상태에서         | `failed`를 포함합니다.                                   |
| `expires_at`   | `completed`     | `created_at`에 24시간을 더한 값입니다.                       |
| `outputs`      | `completed`     | 요청된 이미지마다 하나씩 포함된 이미지 URL 배열입니다.                   |
| `counts`       | 종료 상태에서         | `requested`, `succeeded`, `failed`입니다.             |
| `error`        | `failed`        | `code` 및 `message`가 포함된 객체입니다.                     |

작업이 진행됨에 따라 응답이 확장됩니다. 작업이 아직 실행 중인 동안에는 `outputs` 및 `expires_at`가
단순히 존재하지 않습니다. 고정된 형태를 가정하지 말고 필드를 방어적으로 읽으며,
먼저 `status`를 기준으로 분기하십시오.

### 상태 흐름

```
queued ──► in_progress ──► completed
                       └─► failed
```

두 종료 상태는 모두 최종 상태이며 읽기 엔드포인트는 멱등적입니다. 완료된 작업을
반복해서 읽으면 바이트 단위로 동일한 JSON이 반환됩니다.

### 얼마나 기다려야 하는가

제출에는 1초 미만이 걸리며 부하가 걸려도 이 수준을 유지합니다. 동시 실행 수 12로
30개 작업을 처리했을 때 측정된 p50은 0.88초, p95는 0.92초였습니다. 실제 시간은
생성에 소요됩니다. 모델과 해상도에 따라 대략 12~~50초가 걸리며, 이 중 0~~11초는
대기열에서 소요됩니다. 모델별 수치는 각 모델의 페이지에서 확인할 수 있습니다.

<Warning>
  긴밀한 루프에서 반복하지 말고 2\~3초마다 폴링하십시오. 읽기 자체에 왕복 시간이
  약 0.9초 걸리므로 그보다 빠르게 폴링해도 아무런 이점이 없으며 요청 제한만
  소모합니다.
</Warning>

## 출력 다운로드

`outputs`에는 서명이나 쿼리 문자열이 없는 일반 `https` URL이 포함되어 있으며, API와 호스트 이름이 다른 WideRouter의 전송 CDN에서 제공됩니다. 이에 따라 다음 두 가지 사항이 적용됩니다.

<Steps>
  <Step title="인증되지 않습니다">
    API 키를 전송하지 말고 URL 자체를 비밀로 취급해야 합니다.
    링크를 보유한 사람은 링크가 유효한 동안 이미지를 가져올 수 있습니다.
  </Step>

  <Step title="24시간 후 만료됩니다">
    `expires_at`는 항상 `created_at`에 86400을 더한 값입니다. 보관해야 하는 항목은
    자체 스토리지에 복사하십시오. 출력 URL을 영구 참조로 저장하지 마십시오.
  </Step>
</Steps>

<Warning>
  **다운로드할 때 `User-Agent` 헤더를 전송하십시오.** CDN은 WAF 뒤에 있으며,
  `User-Agent`이 없는 요청과 기본 `Python-urllib/3.x` 요청에 빈 `403`로 응답합니다.
  만료된 링크와 정확히 동일하게 보이지만 실제로는 만료된 것이 아닙니다.
  테스트를 거쳐 정상 작동하는 항목: 브라우저, `curl`, `requests`, `axios`, `okhttp`, Java, Go,
  Postman. 정상 작동하지 않는 항목: 헤더가 없는 원시 `urllib.request.urlopen(url)`입니다.
</Warning>

```python theme={null}
import urllib.request

req = urllib.request.Request(url, headers={"User-Agent": "my-app/1.0"})
with urllib.request.urlopen(req, timeout=120) as r:
    data = r.read()
```

이미지는 C2PA 콘텐츠 자격 증명 매니페스트가 포함된 JPEG로 반환됩니다. 측정된
파일 크기는 `1K`에서 약 0.4–0.7MB, `2K`에서 2.4–3.0MB, `4K`에서 7.5–8.2MB입니다.
파일 확장자를 추정하지 말고 응답에서 `Content-Type`을 읽으십시오.

## 콜백

생성 시 `callback_url`를 설정하면 WideRouter가 완료된 작업을 해당 주소로 게시하므로,
폴링을 완전히 건너뛸 수 있습니다.

```json theme={null}
{
  "model": "gemini-3-pro-image",
  "input": { "prompt": "a paper crane" },
  "callback_url": "https://your-app.example/hooks/widerouter"
}
```

URL은 `https`이어야 합니다. 그 외의 값 — `http`, 단순 호스트 이름, 문자열이 아닌 값 —
은 제출 시점에 `invalid_callback_url`와 함께 거부되므로, 오타가 조용히 전달되지 않는 대신 즉시 실패합니다.

WideRouter는 작업 객체를 `application/json`로 게시하며, 본문을 파싱하기 전에 라우팅에 사용할 수 있는
헤더를 함께 전송합니다.

| 헤더               | 값                  |
| ---------------- | ------------------ |
| `X-Wide-Event`   | `task.completed`   |
| `X-Wide-Task-Id` | 작업 ID              |
| `Content-Type`   | `application/json` |

본문은 해당 시점에 `GET /v1/task/{task_id}`이 반환하는 내용과 바이트 단위로 동일합니다 —
필드와 값이 모두 동일하므로 하나의 핸들러로 두 경로를 모두 처리할 수 있습니다.

테스트에서는 전달이 즉시 이루어졌습니다. 세 작업의 콜백이 모두 작업이
`completed`에 도달한 후 1초 이내에 도착했습니다. 엔드포인트가 `5xx`로 응답하면
WideRouter가 재시도하며, 관찰된 시도 시점은 대략 0초, 10초, 70초였습니다.

<Warning>
  **콜백에는 서명이 없습니다.** HMAC 헤더가 없으며, 요청이 WideRouter에서 왔음을 증명하는 유일한
  요소는 URL입니다. 페이로드를 권한 있는 정보가 아닌 참고 정보로 취급하십시오. `callback_url`에
  길고 추측하기 어려운 경로를 사용하고, 중요한 작업을 수행하기 전에 핸들러가
  `GET /v1/task/{task_id}`을 다시 읽도록 하십시오.
</Warning>

<Info>
  콜백은 전달을 보장하는 기능이 아니라 지연 시간을 줄이기 위한 최적화입니다. 콜백이 도착하지 않는
  작업에 대비해 폴링 대체 경로를 유지하고, 핸들러를 멱등적으로 구현하십시오. 재시도로 인해 동일한
  작업이 두 번 이상 도착할 수 있으므로 작업 `id`를 기준으로 처리해야 합니다.
</Info>

## 작업이 실패하는 경우

`failed` 작업은 읽기 엔드포인트에서 여전히 `200`입니다. 실패는 HTTP 상태가
아니라 본문에 있습니다.

```json theme={null}
{
  "id": "task_jodb26qdNoG5CuwzA23OukOgjeH5ktnh",
  "model": "gemini-3-pro-image",
  "status": "failed",
  "created_at": 1788104830,
  "started_at": 1788104831,
  "completed_at": 1788104831,
  "counts": { "requested": 1, "succeeded": 0, "failed": 1 },
  "error": { "code": "input_fetch_error", "message": "fetch input image failed: ..." }
}
```

`outputs`도 없고 `expires_at`도 없다는 점에 유의하십시오. 입력 가져오기 실패는 모델 작업이
시작되기 전에 발생하므로 빠르게 처리되며, 1초 이내입니다.

## 제출 시 오류

어떤 항목도 대기열에 추가되기 전에 검증이 수행되므로, 여기서 `400`이 발생해도 비용이 들지 않습니다.

| HTTP | `code`                 | 의미                                                           |
| ---- | ---------------------- | ------------------------------------------------------------ |
| 400  | `invalid_params`       | 잘못된 필드입니다. `param`에서 정확한 필드명을 지정합니다. 예: `input.aspect_ratio` |
| 400  | `invalid_callback_url` | `callback_url`은 유효한 `https` URL이 아닙니다.                       |
| 400  | `model_not_supported`  | 실제 모델이지만 비동기 API에서는 사용할 수 없습니다.                              |
| 401  | —                      | `Authorization` 헤더가 없거나 유효하지 않습니다.                           |
| 404  | `task_not_found`       | 해당 작업 ID가 없거나 사용자의 키에 속하지 않습니다.                              |
| 503  | `model_not_found`      | 플랫폼에 해당 모델명이 존재하지 않습니다.                                      |

오류는 한 번에 하나의 필드를 가리키며, `param`은 배열 인덱스(`input.images[0]`)를 포함한 전체 경로를 사용하므로 오류를 요청에 바로 매핑할 수 있습니다.

## 여기에서 작동하는 모델

모델 ID는 **정확히** 일치해야 합니다. 별칭은 없으며, `-preview` 접미사가 붙은
이름은 허용되지 않습니다. 비동기 API에서 제공하지 않는 ID를 전송하면 대기열에
추가되기 전에 제출 시점에 `model_not_supported`가 반환됩니다.

<Card title="Nano Banana 시리즈" icon="layers" href="/ko/models/nano-banana/overview">
  Google의 이미지 모델 — 각 환경에서의 사용 가능 여부, 매개변수 및 측정된 지연 시간입니다.
</Card>

## 동기 또는 비동기 선택

두 방식 모두 제공되며 어느 쪽도 더 이상 사용되지 않는 방식이 아닙니다.

|                    | 비동기 작업 API    | 동기 이미지 API                              |
| ------------------ | ------------- | --------------------------------------- |
| 클라이언트가 연결을 유지하는 시간 | 아니요, 약 0.9초   | 예, 12\~50초                              |
| 결과 전달              | CDN URL, 24시간 | 응답 본문의 base64                           |
| 처리 중 클라이언트 연결 해제   | 작업은 계속 완료됨    | 결과는 손실되며 요청에는 계속 과금됨                    |
| 호출당 여러 이미지         | `n` 최대 4개     | 사용할 수 없음 — `n`이 허용되지만 무시되며 이미지 한 개가 반환됨 |
| 콜백                 | 예             | 아니요                                     |

서버리스 함수, 리버스 프록시 또는 모바일 클라이언트 뒤에서 실행되는 작업에는 비동기 방식이 기본 선택으로 더 적합합니다. 이러한 환경은 50초 동안의 요청을 안정적으로 유지하지 못하기 때문입니다. 단순히 바이트를 반환받으려는 스크립트에는 동기 호출이 여전히 더 간단합니다.

## 다음 단계

<CardGroup cols={2}>
  <Card title="Nano Banana 시리즈" icon="layers" href="/ko/models/nano-banana/overview">
    매개변수 매트릭스, 해상도 등급 및 두 모델의 차이점입니다.
  </Card>

  <Card title="빠른 시작" icon="rocket" href="/ko/quickstart">
    약 5분 안에 전체 과정을 처음부터 끝까지 진행합니다.
  </Card>
</CardGroup>
