---
title: Job과 결과
description: Sume Job을 조회하고, 상태를 폴링하고, 결과를 가져오고, 이벤트를 읽고, 취소를 요청하는 방법을 살펴보세요.
---

Sume 생성 엔드포인트는 내구성 있는 Job을 만듭니다. 제출 응답의 `job_id`는 연동 복구를 위해 반드시 저장해야 합니다. 프로세스가 재시작돼도 Job을 다시 조회할 수 있습니다.

유료 생성에서 `queued`는 정상 접수 상태입니다. 워크스페이스 동시성 한도는 워커가 Job을 `processing`으로 옮길 때 적용되며, API가 유효한 Job을 받을 때는 적용되지 않습니다. 큐 용량, 티어 한도, 큐 가득 참 오류는 [생성 접수](/workflows/generation-admission)에서 살펴보세요.

## 상태

| Status | Meaning | Terminal |
|---|---|---|
| `queued` | 요청이 접수되어 Job이 실행을 기다리는 상태입니다. | No |
| `processing` | Job이 실행 중이거나 마무리되는 상태입니다. | No |
| `completed` | 결과를 사용할 수 있는 상태입니다. | Yes |
| `failed` | 공개 오류와 함께 종료 실패에 도달한 상태입니다. | Yes |
| `canceled` | 취소가 요청되어 종료된 상태입니다. | Yes |

## 상태 폴링

```bash
curl https://api.sume.com/v1/jobs/job_123/status \
  -H "Authorization: Bearer $SUME_API_KEY"
```

지수 백오프를 사용하고 `completed`, `failed`, `canceled`에서 폴링을 멈추세요. 로컬 프로세스가 타임아웃됐다는 이유만으로 원래 유료 요청을 다시 제출하지 마세요.

## MCP `jobs_wait` (단건 또는 배치)

원격 MCP `jobs_wait`는 다음을 받습니다.

- `job_id` — 단건 대기. 응답 형태는 그대로입니다(`object: "job_wait"`).
- `job_ids` — 1–400개 id와 선택적 `wait_for: "all" | "any"`(기본 `all`). 응답은 `object: "job_wait_batch"`이며 요청한 모든 id의 상태 스냅샷을 담습니다.

병렬 fan-out 뒤에는 N번의 단건 wait 대신 짧은 슬라이스마다 배치 wait 한 번을 우선하세요. 타임아웃 시 같은 id로 `jobs_wait`를 다시 호출하고, 유료 create를 다시 제출하지 마세요. `wait_for: "any"`여도 모든 id를 보고하며, 남은 Job은 계속 진행되고 계속 청구됩니다. 알 수 없거나 다른 워크스페이스의 id는 호출 전체를 실패시킵니다.

### 슬라이스에는 상한이 있고, 서버가 이를 강제합니다

`timeout_seconds`의 기본값은 **100**이고 상한은 **120**입니다. 스틸 이미지만 **50**으로 줄일 가치가 있는데, 속도 때문이 아니라(wait는 Job이 종료 상태가 되는 즉시 반환됩니다) 50초를 넘긴 이미지는 느린 것이 아니라 대개 멈춘 것이기 때문입니다.

wait 한 번은 슬라이스 내내 아무것도 전송하지 않고 열려 있는 HTTP 요청 하나이며, 어떤 엣지든 그런 요청을 결국 끊습니다. 그러면 호출자는 도구 결과를 전혀 받지 못하지만 Job은 계속 실행되고 계속 청구됩니다. 더 큰 `timeout_seconds`는 거부가 아니라 클램프되며, 응답의 `wait_slice_clamped`가 그 사실을 알려줍니다.

10분짜리 렌더는 더 긴 슬라이스를 요청하는 대신 wait를 반복해서 기다리세요. `jobs_wait`에서의 `524`(또는 `522` / `523` / `525`)는 전송 실패이지 Job의 결과가 아닙니다. 같은 id로 `jobs_wait`를 다시 호출하거나 `jobs_status`를 한 번 읽으세요. 유료 create를 다시 제출하지 말고, Job이 막혔다고 보고하지 마세요.

## 결과 가져오기

MCP에서는 `jobs_result`가 `job_ids`도 받습니다. 상한은 `jobs_wait`와 같은 1–400개이므로, 한 번의 wait로 기다린 wave를 N번이 아니라 한 번의 호출로 다시 읽을 수 있습니다. 응답은 `job_result_batch`이며, `results[]`는 요청 순서대로 id당 한 항목을 담고 각 항목은 `ok`와 함께 `value` 또는 형식화된 `error`를 가집니다. 부분 성공은 의도된 정상 동작입니다. 아직 실행 중인 id는 `job_not_completed`로 돌아오지만 완료된 id는 그대로 결과를 반환하며, `partial_failure.failed_job_ids`가 다시 읽어야 할 id를 정확히 알려줍니다. 항목마다 `ok`를 확인하세요. 한 id의 실패는 다른 id에 대해 아무것도 말해주지 않습니다.

```bash
curl https://api.sume.com/v1/jobs/job_123/result \
  -H "Authorization: Bearer $SUME_API_KEY"
```

결과는 완료 후에만 사용할 수 있습니다. Job이 아직 완료되지 않았다면 API는 결과가 비어 있는 척하지 않고 conflict 응답을 돌려줍니다.

## 이벤트 읽기

```bash
curl https://api.sume.com/v1/jobs/job_123/events \
  -H "Authorization: Bearer $SUME_API_KEY"
```

이벤트는 디버깅과 복구를 위한 공개 타임라인을 제공합니다.

- `job.created`
- `job.started`
- `provider.submitted`
- `job.completed`
- `job.failed`
- `job.canceled`
- `webhook.delivery`

공개 이벤트는 원본 provider task id나 원본 provider URL을 노출하지 않습니다.

## 취소 요청

```bash
curl -X POST https://api.sume.com/v1/jobs/job_123/cancel \
  -H "Authorization: Bearer $SUME_API_KEY"
```

취소는 queued 또는 processing Job에 사용할 수 있습니다. provider 실행이 이미 취소로 멈출 수 없는 지점을 지났다면 Job이 그대로 끝날 수 있습니다.

## 통신 모드

모든 제출 엔드포인트는 `mode`를 받습니다. 모드는 **결과를 어떻게 전달받을지**만 정합니다. Job 생성 여부, 비용, 실제 실행 시간은 모드에 따라 달라지지 않습니다.

| Mode | HTTP 응답 | 첫 응답에 job id | 서버가 블로킹하나 | 클라이언트가 할 일 |
|---|---|---|---|---|
| `async` (기본) | `202`와 Job envelope, 폴링 URL | 예 | 아니오 | `terminal`이 true가 될 때까지 `status_url`을 폴링하고, `result_ready`가 true면 `GET result_url`. |
| `sync` | 같은 envelope. 종료 상태 전환을 최대 `wait_timeout_seconds`(최대 **30**)까지 기다린 뒤 응답 | 예 | 예, 최대 30초 — waiter 용량이 없으면 그보다 짧게 | 종료 상태면 응답에서 바로 읽으세요. 아니면 **폴링하세요. 재제출하지 마세요.** |
| `subscribe` | `sync`와 동일 — 같은 제한 대기 | 예 | `sync`와 동일 | `sync`와 동일. fal 스타일의 긴 대기가 필요하면 아래 클라이언트 subscribe 레시피를 `async`와 함께 쓰세요. |
| `webhook` | `202`와 Job envelope, 폴링 URL. 콜백을 저장합니다 | 예 | 아니오 | 종료 콜백을 기다리고 서명을 검증하세요. 폴링은 백업으로 유지하세요. |

`mode`를 생략하면 `async`입니다. `mode` 없이 `webhook_url`(또는 별칭 `callback_url`)만 보내면 `webhook`입니다.

제출은 Sume가 내구성 있는 job id를 확보하는 순간 접수되므로, **모든 모드가 첫 응답에 job id를 돌려줍니다.** `2xx`는 Job이 존재하고 유료 작업이 진행 중이라는 뜻이지, Job이 끝났다는 뜻이 아닙니다. 둘을 구분하려면 envelope의 `terminal`과 `result_ready`를 읽으세요.

### `sync`와 `subscribe`는 별칭입니다

두 모드는 같은 제한 waiter를 돌리고 같은 envelope를 돌려줍니다. 다른 큐 API에서 넘어온 클라이언트가 `subscribe`를 먼저 찾기 때문에 남겨둔 이름이며, 오래 유지되는 구독도, 이벤트 스트림도, 더 긴 대기도 아닙니다. 현재 Developer API에는 SSE나 WebSocket 전송이 없습니다. `GET /v1/jobs/:id/events`는 스트림이 아니라 pull 스냅샷입니다.

### "Subscribe"는 세 가지 다른 것을 뜻합니다

이 단어는 서로 관계없는 세 표면에서 서로 다른 대기 시간을 가리킵니다. 어느 것도 푸시 스트림이 아닙니다. 타임아웃을 잡기 전에 아래 표를 먼저 확인하세요.

| 등장 위치 | 정체 | 대기 시간 |
|---|---|---|
| Job `mode: "subscribe"` (이 페이지) | `sync`의 별칭입니다. 제출 호출에서 한 번의 제한된 HTTP 대기를 합니다. | 최대 `wait_timeout_seconds`, 상한 **30초**입니다. |
| SDK `subscribeFormatRun()` ([TypeScript SDK](/sdk)) | run을 만든 뒤 클라이언트 쪽에서 종료 receipt까지 폴링합니다. | 분 단위입니다. HTTP를 붙잡는 것이 아니라 SDK 자체 타임아웃입니다. |
| Format·Action·Agent의 `communication.mode` | Job이 아니라 **run**의 전달 방식 선택입니다. 값은 `async`와 `webhook`이며 `subscribe`는 없습니다. | 아무것도 블로킹하지 않습니다. |

짚어 둘 결과가 두 가지 있습니다.

- `mode: "subscribe"`를 보낸다고 진행 이벤트를 받는 것이 **아닙니다.** `sync`와 똑같은 30초 대기를 받을 뿐입니다. 진행 상황이 필요하면 `async`로 제출하고 `GET /v1/jobs/:id/events`를 읽거나 [웹훅](/workflows/webhooks)을 쓰세요.
- `communication.mode`에는 `subscribe` 값이 아예 없고, 두 값의 동작도 같습니다. 실제로 전달을 켜는 것은 `webhook_url`을 넣는 행위입니다.

새 연동은 **`async`**(폴링하거나 이벤트를 읽는 방식) 또는 **`webhook`**(통보받는 방식)을 쓰세요. `sync`와 `subscribe`는 계속 지원되며 없어지지 않습니다. 다만 30초를 넘길 수 있는 작업 — 대부분의 영상 작업이 여기에 해당합니다 — 에는 맞지 않는 도구일 뿐입니다.

### 30초는 대기 예산이지 Job 실행 시간이 아닙니다

`wait_timeout_seconds`는 `0..30`으로 클램프됩니다. 이 값은 **HTTP 요청**이 블로킹되는 시간을 제한할 뿐, **Job**이 걸릴 수 있는 시간을 제한하지 않습니다. 이미지 Job은 대개 이 안에 끝나지만, 비디오·아바타 비디오·페이스 스왑 Job은 보통 그렇지 않습니다.

대기 예산이 소진되거나, API 프로세스에 waiter 용량이 없어 블로킹 대기를 건너뛴 경우:

1. 응답은 여전히 `2xx`이고 job id를 담고 있습니다. 대기 소진은 접수 실패가 아닙니다.
2. envelope에는 `status_url`, `result_url`, `events_url`, `cancel_url`과 `sync` 객체가 들어 있습니다. 종료 상태 전에 대기가 끝났다면 `sync.timed_out`이 true이고, 프로세스별 waiter 예산이 가득 차 대기를 건너뛰었다면 `sync.capacity_exhausted`가 true입니다.
3. `GET status_url`로 **반드시** 이어가세요. `next_poll_after_seconds`가 있으면 그 값을 존중하고, 없으면 백오프하세요.
4. 같은 의도로 **새 유료 Job을 만들지 마세요.** 제출 자체를 재시도하는 것은 괜찮습니다. **같은** `Idempotency-Key`를 재사용하면 두 번 청구되는 대신 원래 Job이 돌아옵니다.

`async`와 `webhook` 응답에서 `sync`는 null입니다.

### 클라이언트 subscribe: 종료 상태까지 Job 폴링하기

클라이언트 측 `subscribe()`에 해당하는 공식 방식이며, 30초를 넘길 수 있는 모든 작업의 정답입니다. `async`로 제출하고, 폴링하고, 결과를 읽으세요. 대기가 **여러분의** 클라이언트 안에 있으므로 HTTP 요청을 열어둔 채 두지 않고도 타임아웃을 몇 분으로 잡을 수 있습니다.

```text
job = POST /v1/{product}/generate { mode: "async", ... } with Idempotency-Key
loop:
  s = GET /v1/jobs/{job.id}/status
  onStatus(s)                     # 선택적 진행 콜백
  if s.terminal: break
  sleep(s.next_poll_after_seconds or exponential backoff)
if s.sume_status == "completed":
  return GET /v1/jobs/{job.id}/result
else:                             # failed 또는 canceled
  raise from (GET /v1/jobs/{job.id}).job.error
```

불리언(`terminal`, `result_ready`)이나 `sume_status`로 폴링을 종료하세요. status 엔드포인트는 다른 큐 API에서 넘어온 클라이언트를 위해 `sume_status`와 일대일로 대응하는 큐 형태의 `status` 필드(`IN_QUEUE` / `IN_PROGRESS` / `COMPLETED` / `FAILED` / `CANCELED`)도 돌려줍니다. 두 값은 서로 어긋나지 않지만 섞어 쓰지는 마세요.

`GET /v1/jobs/:id/result`는 완료된 Job 전용입니다. 그 외에는 `409 job_not_completed`를 돌려주므로, 실패 사유는 Job 레코드에서 읽으세요.

1단계 — 제출:

```bash
curl -X POST https://api.sume.com/v1/image-1.0/generate \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hero-shot-2026-08-03-001" \
  -d '{"prompt":"Product hero shot of a matte black bottle on marble","mode":"async"}'
```

응답에는 `request_id`(job id), `status_url`, `result_url`, `next_poll_after_seconds`가 담깁니다.

2단계 — `"terminal": true`가 될 때까지 폴링:

```bash
curl https://api.sume.com/v1/jobs/job_123/status \
  -H "Authorization: Bearer $SUME_API_KEY"
```

3단계 — `"result_ready": true`가 되면 결과 가져오기:

```bash
curl https://api.sume.com/v1/jobs/job_123/result \
  -H "Authorization: Bearer $SUME_API_KEY"
```

어떤 HTTP 클라이언트로도 이 루프를 돌릴 수 있습니다. OkHttp나 Ktor를 쓰는 Kotlin이라면 형태는 이렇습니다.

```kotlin
// 예시 스케치 — Sume는 Kotlin 패키지를 배포하지 않습니다.
// 1. mode = "async"와 Idempotency-Key로 generate 라우트에 POST.
// 2. /v1/jobs/{id}/status를 루프로 GET. next_poll_after_seconds가 있으면
//    그 값을, 없으면 지수 백오프를 사용.
// 3. terminal = true면 중단. result_ready = true면 /v1/jobs/{id}/result를 GET.
// 전체 마감 시간은 클라이언트 쪽에서 정합니다 — 비디오라면 20분 정도가 적당합니다.
// 30초를 넘지 않는 wait_timeout_seconds와는 다릅니다.
```

클라이언트 타임아웃은 Job을 취소하지 않습니다. Job은 계속 실행되고 계속 청구되며, 여러분이 지켜보기를 멈춘 것뿐입니다. job id를 저장했다가 `status_url`에서 다시 이어가거나, 명시적으로 취소하세요.

TypeScript라면 `@sume-com/sdk`의 [`waitForJob`](/sdk/runs)이 바로 이 루프입니다.

### Job 웹훅은 종료 상태 전용입니다

`mode: "webhook"`은 공개 HTTPS `webhook_url`로 정확히 세 가지 이벤트(`job.completed`, `job.failed`, `job.canceled`)만 전달합니다. 진행 상황이나 부분 웹훅은 없습니다. Sume는 `{timestamp}.{raw_body}`에 대한 HMAC SHA-256으로 원본 본문에 서명하고 `x-sume-webhook-timestamp`와 `x-sume-webhook-signature: sume-v1=…`를 보냅니다. payload와 검증 코드는 [웹훅](/workflows/webhooks)에서 살펴보세요.

웹훅은 전달 최적화이지 유일한 복구 경로가 아닙니다. 전달이 누락되거나 재시도될 때를 위해 `status_url` 폴링을 쓸 수 있게 유지하세요.

Action, Format, Agent Completion **run**은 자체 `*.run.terminal` 이벤트를 쓰는 별도 표면입니다. [Run 웹훅](/agents/run-webhooks)을 참고하세요.

## Idempotency

클라이언트 타임아웃이나 네트워크 실패 뒤 재시도할 때는 제출 요청에 `Idempotency-Key`를 보내세요.

```bash
curl -X POST https://api.sume.com/v1/avatar-1.0/generate \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: avatar-request-2026-06-29-001" \
  -d '{"avatar_handle":"studio_presenter","input":{"type":"prompt","prompt":"Friendly studio presenter"}}'
```

같은 키는 같은 연산과 payload에만 재사용하세요.

## 결과 형태

완료된 Job은 artifact를 포함할 수 있습니다.

```json
{
  "id": "job_...",
  "status": "completed",
  "result": {
    "artifacts": [
      {
        "id": "artifact_...",
        "url": "https://media.sume.com/artifacts/...",
        "media_type": "image",
        "content_type": "image/png"
      }
    ]
  }
}
```

결과의 Sume 미디어 URL을 사용하세요. 원본 provider URL은 공개 API 출력이 아닙니다.
