Repository navigation
Expand file tree
/
Copy pathopenapi-v2.yaml
More file actions
1748 lines (1506 loc) · 91.7 KB
/
Copy pathopenapi-v2.yaml
File metadata and controls
1748 lines (1506 loc) · 91.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Comfy API v2 — public specification.
#
# GENERATED ONE-WAY — DO NOT HAND-EDIT.
# Projected automatically from the canonical Comfy API v2 contract and
# synced by CI. Change the upstream contract, not this public copy.
openapi: 3.0.3
info:
title: Comfy API v2
version: 2.0.0
description: "The official, versioned HTTP API for running ComfyUI workflows from\nexternal applications: upload inputs, submit a workflow, observe\nexecution, retrieve results.\n\nDesign principles:\n- **Poll-first.** Every capability is reachable via plain GET polling;\n the SSE stream is a live enhancement, never the source of truth.\n- **Everything is resumable.** Submission is idempotent; job state and\n outputs are retrievable by ID until `expires_at`.\n- **UUID identity, content-addressed dedup.** Assets are UUID-identified\n records over blobs keyed by a server-computed blake3 hash. The hash is\n nullable and may be computed lazily.\n- **Follow links, don't build URLs.** Responses embed follow-up URLs.\n\nAdditive changes only within v2; breaking changes require v3.\n"
servers:
- url: http://127.0.0.1:8189
description: Self-hosted (comfy-api-proxy)
- url: https://cloud.comfy.org
description: Comfy Cloud
- url: https://{deployment}.run.comfy.app
description: Serverless deployment
variables:
deployment:
description: DNS-safe deployment id (subdomain label). Staging uses {deployment}.stg.run.comfy.app.
default: dep-1234abcd-56ef-7890-abcd-ef1234567890
security:
- bearerAuth: []
- {}
tags:
- name: assets
description: UUID-identified records over content-addressed blobs.
- name: jobs
description: One execution of a workflow — durable, pollable, cancelable.
paths:
/api/v2/assets:
post:
operationId: postAssets
tags:
- assets
summary: Upload an asset (single-call multipart)
description: 'Single-call `multipart/form-data` upload. The platform streams the
bytes through its trusted byte-path, dedups by the server-computed
hash, and mints the asset record.
The blake3 hash is always computed server-side from the received
bytes — a client-declared `expected_hash` is verified, never trusted
(on Comfy Cloud and serverless, only when sent before `file` — see below).
If location metadata (GPS, XMP, IPTC) is removed from an image before
it is stored, the asset''s `hash` is the hash of the stored bytes and
can differ from `expected_hash`. On Comfy Cloud, an upload whose
bytes open as a JPEG, PNG, WebP or classic TIFF (including TIFF-based
camera RAW such as DNG, NEF, ARW and CR2, but not BigTIFF) and that
is over 50 MiB is refused with 422 `validation_error`, whatever its
`content_type` and whether or not it carries location metadata. A
request that declares a `Content-Length` over Comfy Cloud''s 200 MB
(200,000,000-byte) body limit is refused before that check with a
plain 413 that does not use the `ErrorEnvelope` shape. A body sent
without a declared length is cut off when it passes that limit, and
no 413 is sent: a cut-off while the fields before `file` are read
fails with 422 `invalid_body`, and a cut-off inside the file usually
fails with 500 `upstream_error`. Nothing after the `file` field is
read, so a body that only passes the limit there is uploaded
normally.
Deployment-scoped surfaces neither strip location metadata nor apply
the 50 MiB image cap; they limit the request body to 110 MB
(110,000,000 bytes). A request that declares a `Content-Length` over
that limit is refused with the same plain 413; a body sent without a
declared length is cut off when it passes the limit, and the upload
then fails with an error response that is not guaranteed to be a 413.
The uploaded asset is referenceable immediately; any content scanning
runs in the background and does not block the response.
Send `file` as the last part. Comfy Cloud and serverless deployments
stream it straight into storage and read the other fields only up to
it, so a part sent after `file` is never read: a `content_type` or
`file_path` sent only after it is refused as missing (422); an
optional field (`expected_hash` or `tags`) is silently ignored — the
upload succeeds without the hash check or the caller''s tags; on
serverless deployments, a trailing `expires_in` is likewise ignored,
losing the retention override; and a second `file` part is dropped,
so only the first part''s bytes are stored. Comfy Cloud ignores
`expires_in` wherever it is sent, before or after `file`. The
response does not say any of these were skipped.
An upload is a single request. v1 expects files of up to ~100 MB;
the hard cap is the body limit of the surface it is sent to (above).
Chunked/resumable large uploads are a deliberate open question.
Cloud''s hosted API caps every part sent before `file` at 8 KiB,
including a part it does not recognise (which it otherwise
ignores), so an extra field over 8 KiB, such as file bytes sent
under a misspelled name, is refused with 422 `invalid_body`. Parts
sent after `file` are never read: the 8 KiB cap does not apply to
them, and a metadata field sent there is treated as absent, so send
every metadata field before `file`. A declared `Content-Length`
still counts those bytes toward the size limit above; without one,
a body that only exceeds the limit there is uploaded normally,
since nothing after `file` is read.
On Comfy Cloud an upload fails with 422 `upload_timed_out` when no
request bytes arrive for 30 seconds, or when the whole request takes
longer than 10 minutes, however steadily bytes keep arriving.
On Comfy Cloud an upload can also be refused with 503 `upload_busy`
and `Retry-After` when the server is busy with other uploads: nothing
was stored, so the same upload can be sent again after `Retry-After`.
On a Developer Platform deployment the same 422 `upload_timed_out`
applies when no request bytes arrive for 30 seconds, or when the whole
request takes longer than 20 minutes.
'
x-streaming-upload: true
x-idempotency-key: recommended
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required:
- file
- content_type
- file_path
properties:
content_type:
type: string
example: image/png
description: The asset's media type. On Comfy Cloud it must be valid UTF-8, not blank, at most 255 bytes, must not start or end with whitespace, and must not contain control characters (C0, DEL, C1, U+2028, U+2029) or any invisible format or default-ignorable character (every Unicode general category Cf character, including the zero-width joiners and direction marks, and the other default-ignorable code points); otherwise the request is refused with 422 `invalid_body` before anything is stored. An accepted value is stored as sent. A deployment refuses a `content_type` that is blank, is not valid UTF-8 or contains a control character (C0 other than tab, DEL, C1) or a bidi embedding, override or isolate (U+202A-U+202E, U+2066-U+2069), has a tab outside the whitespace that opens a parameter after a `;`, or whose part before its first `;` is not `type/subtype` (two RFC 9110 tokens, optionally followed by spaces), with 422 `invalid_request` before anything is stored.
file_path:
type: string
description: 'Placement path / filename (global-namespace-root form, e.g. `photo.png` or `models/checkpoints/x.safetensors`). On Comfy Cloud the last segment becomes the asset name, so it must be valid UTF-8, at most 255 bytes, must not start with `.`, and must not contain control characters (C0, DEL, C1, U+2028, U+2029) or invisible format and default-ignorable characters (Unicode general category Cf, which includes the bidi embedding, override and isolate controls, soft hyphen, zero-width space, word joiner, BOM and tag characters, plus the other default-ignorable code points such as U+034F, U+2065 and the Hangul fillers); otherwise the request is refused with 422 `invalid_body` before anything is stored. ZWNJ/ZWJ (U+200C, U+200D) are allowed only between two visible non-ASCII characters (so not first, last, next to ASCII or next to another of these invisible characters, though one may follow an allowed variation selector, one right after an Indic virama on a letter of the same script may end a word, and a ZWJ after an Arabic-script letter that joins on both sides, or after that letter''s marks, may precede ASCII such as a ''.'' or end the name), for Persian, Indic and emoji names. The direction marks U+200E, U+200F and U+061C are allowed, for right-to-left names, and so are the visible Arabic number signs (prepended concatenation marks such as U+0600, but not U+070F); the direction marks can still change how a name is displayed. The emoji presentation selectors U+FE0E and U+FE0F are allowed only after an ASCII digit, `#` or `*` that U+20E3 (the keycap) follows, after U+2139, the punctuation emoji U+203C, U+2049, U+3030 and U+303D or the math symbols U+2194, U+25FB-U+25FE, U+2934 and U+2935, and after any other symbol of category So except a regional indicator, a skin-tone modifier, a hair component or U+2800. Other variation selectors are allowed only after a base that can have variants: the Mongolian ones after a Mongolian letter, the rest after a unified Han ideograph, plus U+FE00 after the Myanmar and Phags-pa characters it is registered for. The Mongolian and Han rules follow scripts, not the registered sequence list. The tag characters are allowed only as the tag run of the England, Scotland and Wales flag emoji. These allowed invisible characters, white space, U+2800 and combining marks must not make up the whole segment or come before a leading `.`. A deployment keeps the whole `file_path` as the asset name, so it must be valid UTF-8 and must not contain a control character (C0 U+0000-U+001F, including tab, CR and LF; DEL U+007F; C1 U+0080-U+009F) or a bidi embedding, override or isolate (U+202A-U+202E, U+2066-U+2069); otherwise the request is refused with 422 `invalid_request` before anything is stored. Directory segments and other characters stay allowed there.'
example: photo.png
expected_hash:
type: string
description: 'Optional client-computed blake3 (`blake3:<hex>`) of the bytes sent. Omit the part to skip verification: on Comfy Cloud and on a deployment a part sent empty is refused with 422 as malformed. Send it before `file`: nothing after the `file` field is read, so a trailing `expected_hash` is neither verified nor refused, and the response does not say the check was skipped. Verified against the server-computed blake3 of the received bytes, not of the stored bytes; mismatch is rejected with 409 `hash_mismatch`. On serverless deployments no asset is minted and no existing asset''s retention is changed; elsewhere the upload is stored before the check, so the 409 returns no asset and a retry with the corrected expectation (and the same file extension) dedups onto it. On Comfy Cloud nothing removes that asset, so when the bytes themselves were wrong they stay stored: as an asset named after the last segment of `file_path` (`x.safetensors` for `models/checkpoints/x.safetensors`), or, when the bytes deduped onto an asset you already had, as that asset under its earlier name. The rest of `file_path` is not recorded. On Comfy Cloud and on a deployment, when `expected_hash` is sent more than once before `file` only the last value is checked, but an empty one among them is still refused with 422.'
example: blake3:9f8a1c0d...
tags:
type: array
items:
type: string
description: 'Category tags (e.g. `input`). A create carries at most 64 tags, each a non-empty UTF-8 string of at most 256 bytes (surrounding whitespace included) with no NUL byte. A deployment adds `input` to every asset and counts it, so it takes at most 63 of the caller''s own; the `model_type:` tags derived for a model asset are not counted. A tag set breaking a limit is refused with 422. The rest of this paragraph describes Cloud''s hosted API and a deployment, not the self-hosted proxy. Both cap the bytes of all `tags` parts sent before `file` combined: 8 KiB on Cloud''s hosted API (422 `invalid_body`) and 64 KiB on a deployment (422 `invalid_request`). The cap counts duplicates before they are removed and JSON escapes before they are decoded, and is checked before the count and 256-byte limits above, so a tag set within them can still be refused. A `quoted-printable` part is counted and read after it is decoded; a part in any other `Content-Transfer-Encoding` is read as sent, so a `base64` part is stored as one tag holding its encoded text, while a `7bit`, `8bit` or `binary` part is read like a part with no encoding. A part is read as a JSON array when it starts with `[` (after leading whitespace) and is valid JSON or continues, after optional whitespace, with `"`: `["a","b"]` is two tags, `[]` adds no tags (nor does an empty part), and a malformed array such as `[1]` or `["a` is refused with 422. Any other non-empty part is one bare tag, kept as literal text, so `foo\ud800` sent as a bare part is stored as written. Invalid UTF-8 in a `tags` part, or a lone surrogate escape (e.g. `\ud800`) in a JSON-array `tags` part, is refused with 422 (`invalid_body` on Cloud, `invalid_request` on a deployment).'
expires_in:
type: integer
minimum: 60
maximum: 604800
description: 'Optional retention override in seconds (60s–7d): the asset''s `expires_at` becomes now + `expires_in`, replacing the platform''s default retention. Implementations without configurable retention ignore it. The bounds apply to this override only — the platform default is operator-configured and may lie outside them.'
example: 86400
file:
type: string
format: binary
description: 'The raw bytes. The order of properties in this schema does not set the order of parts on the wire: `file` must be the last part sent (see the field-order rule above).'
responses:
'201':
description: New blob stored; asset minted.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'200':
description: Bytes deduped to an existing blob; asset minted over it.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'409':
description: '`hash_mismatch`.'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'413':
description: The upload is over the body limit. On Comfy Cloud (200 MB, 200,000,000 bytes) and on deployment-scoped surfaces (110 MB, 110,000,000 bytes) the request declared a `Content-Length` over it; the server's body limit sends this before the upload is handled, so the body is not an `ErrorEnvelope` and its shape is not part of this contract. The self-hosted proxy instead refuses a file over its configured cap (100 MiB by default) with an `ErrorEnvelope` whose code is `payload_too_large`.
'422':
description: '`idempotency_key_reuse`, `input_blocked` (the bytes are already stored and flagged by content moderation, so `file_path` is not registered for them), `upload_timed_out` (the client stopped sending or sent too slowly; the server cannot recover the upload, so retry it from a faster connection), or validation failure — including a `tags` set over the limits above or a disallowed tag (`invalid_request` on a deployment or the self-hosted proxy; a deployment answers a body that is not `multipart/form-data` with a boundary with a plain 500 instead). On a deployment that includes a truncated `file` part: the body ends inside the part, before the delimiter that ends it. A body that stops right at that delimiter, without the final `--`, is not rejected: the part reads as complete and is stored. Comfy Cloud''s asset upload answers validation failure instead with `invalid_content_type` (the request is not `multipart/form-data` with a boundary), `invalid_body` (a malformed multipart body or upload field) or `validation_error` (the platform refused the upload, for example an image over the size cap or a disallowed tag). Cloud refuses a malformed multipart body with `invalid_body` when the fault is found before the `file` part. Cloud stops parsing at `file`, so parts after it are never read, and a body that ends inside the streamed file currently gets 500 `upstream_error` instead. The exception is an upload with `expected_hash`: if Cloud accepts the upload before the whole file is read, it reads the rest. A body that ends inside the `file` part, before its delimiter, then gets `invalid_body`, and a complete body gets 500 `upstream_error` instead of 201 or 409; in both cases the asset is already stored, holding only the part of the file read before the upload was accepted. `invalid_body` also covers `tags` parts over the 8 KiB combined cap described under `tags`, and a body sent without a declared length that is cut off at the body limit, as described for this operation. Cloud uses the same code for a missing `file`; a missing `content_type`, or one that breaks the rules under `content_type`; a missing `file_path`, or one that is absolute, starts with `.`, contains `..`, is not already in clean form (no `//`, no `.` segment such as `/./`, and no trailing `/`) or breaks the rules under `file_path`; a `content_type`, `file_path` or `expected_hash` part over 8 KiB; and a non-empty `expected_hash` that is not `blake3:` followed by 64 lowercase hex characters. A `content_type` or `file_path` sent only after `file` counts as missing (see the field-order rule above). Any Content-Type other than `multipart/form-data` with a `boundary` (including a missing or unparseable one) gets `invalid_content_type` on Cloud. On Comfy Cloud and on a deployment, an `expected_hash` part that is present but empty is a malformed field (`invalid_body` on Comfy Cloud, `invalid_request` on a deployment), not an omitted hash.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/assets/from-hash:
post:
operationId: assetFromHash
tags:
- assets
summary: Create or reuse an asset over existing bytes
description: 'Zero-byte fast-path: creates or reuses an asset over a blob the
platform already has, identified by its blake3 hash.
On Comfy Cloud, the caller must already own the content in the
current workspace. Content owned only by another member of that
workspace does not satisfy the ownership check and returns `404`
`blob_not_found`, just like an absent hash. A matching caller-owned
Cloud asset is reused with `200`, `created_new: false` and its UUID,
name and tags, even when the request supplies a different valid
`file_path` or `tags`.
On a deployment, reuse matches both the hash and the exact path in
the build''s scope. A match returns `200` with `created_new: false`;
a different path over content the caller may use creates a new
reference with `201` and `created_new: true`. The self-hosted proxy
returns its existing hash-indexed record with `200` and
`created_new: false`, without applying the requested path or tags.
Reuse keeps the existing name and tags. Cloud and deployment
validation still applies to submitted metadata before reuse;
metadata is not exempt from validation because it will not replace
stored values. Retention may refresh, and content URLs are generated
for the response, so reuse does not promise an unchanged response.
Trust boundary: resolves only against blobs the platform itself
ingested and hashed, and only those the calling account is authorized
to use — the client hash is a lookup key, never an authority to
register new content. A miss and "exists but not yours" are
deliberately indistinguishable (`404` `blob_not_found`).
The lookup key is the `hash` an upload returned, which is the hash of
the stored bytes. When location metadata was removed from an image
before it was stored, that differs from the client''s digest of the
file it sent, and the client''s digest finds nothing.
'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- hash
properties:
hash:
type: string
example: blake3:9f8a1c0d...
file_path:
type: string
description: 'Placement path / filename for a new reference. Comfy Cloud uses its last segment as the name; a deployment uses the full path. Both default to the bare hash digest when omitted or empty. Cloud reuse keeps the existing name even if a different valid path is supplied; a deployment also matches the path when deciding reuse. The self-hosted proxy keeps its indexed record''s path. On Comfy Cloud a `file_path` that fails path validation, or whose last segment breaks the `file_path` rules of `POST /api/v2/assets`, is answered with 404 `blob_not_found`, the same as an unknown hash. The UTF-8 rule cannot fire here: invalid UTF-8 bytes and lone surrogate escapes (`\ud800`) are replaced with U+FFFD when the JSON body is decoded, so such a name is stored with U+FFFD in their place. A deployment answers a `file_path` that fails path validation or holds a control character (C0, DEL, C1) or a bidi embedding, override or isolate (U+202A-U+202E, U+2066-U+2069) with 422 `invalid_request`; the same U+FFFD replacement applies there.'
example: photo.png
tags:
type: array
items:
type: string
description: 'Tags for a new reference; reuse keeps the existing tags. Comfy Cloud defaults an omitted or empty set to `input`. Cloud and deployment validation still applies on reuse; the self-hosted proxy does not apply submitted tags. A create carries at most 64 tags, each a non-empty UTF-8 string of at most 256 bytes (surrounding whitespace included) with no NUL byte. A deployment adds `input` to every asset and counts it, so it takes at most 63 of the caller''s own; the `model_type:` tags derived for a model asset are not counted. A tag set breaking a limit is refused with 422. Invalid UTF-8 or a lone surrogate escape (e.g. `\ud800`) inside a JSON-encoded tag is not refused: JSON decoding replaces it with U+FFFD (3 bytes each), and the 256-byte limit applies to the replaced value.'
expires_in:
type: integer
minimum: 60
maximum: 604800
description: 'Optional retention override in seconds (60s–7d): the asset''s `expires_at` becomes now + `expires_in`, replacing the platform''s default retention. Implementations without configurable retention ignore it. The bounds apply to this override only — the platform default is operator-configured and may lie outside them.'
example: 86400
responses:
'201':
description: A new asset reference with a new UUID was created over existing bytes; `created_new` is true. Supplied metadata and defaults apply as described above.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'200':
description: An existing asset was reused under the serving surface's rules above; `created_new` is false. Its UUID, name and tags are kept; retention may refresh and content URLs are generated for this response.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: '`blob_not_found` — no blob the caller may mint from.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: 'The `tags` or `expires_in` is malformed, `tags` breaks the limits above or holds a disallowed tag, or (on a deployment) the `file_path` is malformed: `invalid_request` on a deployment. On Cloud a tag with a malformed shape is `invalid_body`, and a refusal by the platform (for example a disallowed tag) is `validation_error`. Cloud answers a malformed `file_path` with 404 `blob_not_found` instead (see `file_path`). A deployment checks the tags even when an identical reference exists.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/assets/by-hash/{hash}:
head:
operationId: headAssetByHash
tags:
- assets
summary: Existence check for a blob by blake3 hash
description: '`200` if the calling account can mint from this blob, `404` otherwise. Same account-scoping as `from-hash`; lets a client decide between the dedup fast-path and a full upload before sending bytes. Use the `hash` an upload returned: for an image whose location metadata was removed before storage, the client''s digest of the file it sent names no stored blob. A refusal answers its status with no body, so a `403`''s code is not readable here.'
parameters:
- $ref: '#/components/parameters/BlakeHash'
responses:
'200':
description: Blob present and mintable by the caller.
'404':
description: No blob the caller may mint from.
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/assets/{id}:
get:
operationId: getAsset
tags:
- assets
summary: Asset metadata
description: Returns the asset object with a fresh short-lived `url` for the content. Re-fetching always yields fresh URLs.
parameters:
- $ref: '#/components/parameters/AssetId'
responses:
'200':
description: The asset.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
delete:
operationId: deleteAsset
tags:
- assets
summary: Delete an asset record
description: 'Deletes the asset RECORD. The underlying content-addressed blob is
untouched while any other asset still references it (hash dedup means
blobs are shared) — deleting an asset never destroys another asset''s
bytes.
A second delete of the same id returns `404`, indistinguishable from
an id that never existed or belongs to another account.
'
parameters:
- $ref: '#/components/parameters/AssetId'
responses:
'204':
description: Record deleted.
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: '`asset_in_use` — the record cannot be deleted while the platform still depends on it. Each surface defines its own holds (for example: a job''s outputs reference the record, or a content-moderation workflow requires it to be preserved); the response body deliberately never says which hold applies.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/assets/{id}/content:
get:
operationId: getAssetContent
tags:
- assets
summary: Asset bytes
description: 'Serves the bytes directly on surfaces where the platform stores blobs
itself (self-hosted); on Cloud and serverless issues a `302` to a
fresh signed URL. Range requests are supported for resumable
downloads of large outputs.
'
parameters:
- $ref: '#/components/parameters/AssetId'
- name: Range
in: header
required: false
schema:
type: string
description: Standard HTTP range, e.g. `bytes=0-1048575`.
responses:
'200':
description: The full content.
content:
application/octet-stream:
schema:
type: string
format: binary
'206':
description: Partial content for a ranged request.
content:
application/octet-stream:
schema:
type: string
format: binary
'302':
description: Redirect to a fresh signed URL (Cloud / serverless).
headers:
Location:
schema:
type: string
format: uri
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'416':
description: Range not satisfiable.
'429':
$ref: '#/components/responses/RateLimited'
'451':
description: '`content_blocked` on a deployment: content moderation flagged these bytes, so they are not served.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs:
post:
operationId: postJobs
tags:
- jobs
summary: Submit a workflow for execution
description: 'Accepts the API-format workflow graph verbatim. Validation is
synchronous: graph structure, unknown node classes, and asset
references (`core/ASSET` objects — every referenced `id` must exist
and be owned by the caller). A `201` means the job is durably
recorded and queued.
UI-format workflow JSON (the export with `nodes`/`links`) is
rejected with `workflow_format_ui`.
`Idempotency-Key` is single-use (reject-on-duplicate, NOT
record-and-replay): the first request to present a given key is
processed normally; ANY later request presenting the same key — a
retry, a concurrent duplicate, or a same-key request with a different
body — is rejected `422` `idempotency_key_reuse` and is never
re-executed. The key is claimed only for a request that actually
reaches submission and is released if that submission definitively
fails without creating a job (a validation error, or an upstream
reject such as out-of-credits or queue-full), so a legitimate retry
with the same key can proceed. If a submission''s outcome is unknown
(an upstream timeout or 5xx where the job may or may not have been
created), the key stays claimed and the retry is rejected: poll or
list your jobs to find the possibly-created job rather than
resubmitting. Keys expire after 24h. There is no response replay and
no `Idempotency-Replayed` header.
Reserved for post-MVP and rejected if present today: `webhook_url`,
`inputs`.
'
x-idempotency-key: recommended
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- workflow
properties:
workflow:
type: object
description: API-format workflow graph, verbatim.
additionalProperties: true
extra_data:
type: object
description: 'Per-prompt ComfyUI `extra_data`, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt and excluded from idempotency comparison. On a deployment it is dispatch-only and never stored; on Comfy Cloud it is persisted with the prompt, because the worker needs it, and redacted on every path that returns a workflow to a caller.
Send the one credential you hold: an API key as `api_key_comfy_org`, or the session token an interactively signed-in client has instead as `auth_token_comfy_org`. Sending both is accepted and both are forwarded, but it is not a supported combination and which one a node uses is not defined here. Note a session token is short-lived and is not re-minted for you, so one submitted long before it executes may expire in the queue.'
additionalProperties: false
properties:
api_key_comfy_org:
type: string
description: API key for partner (API) nodes.
auth_token_comfy_org:
type: string
description: Session bearer token for partner (API) nodes — the equivalent of `api_key_comfy_org` for a caller authenticated by session rather than by key.
preview_method:
type: string
description: ComfyUI's preview method for this prompt, passed to ComfyUI as sent, which validates it. Used only for jobs sent by Comfy Cloud's editor; ignored for other callers.
extra_pnginfo:
type: object
description: ComfyUI's `extra_pnginfo` for this prompt, which carries the editor's workflow into saved outputs. Used only for jobs sent by Comfy Cloud's editor; ignored for other callers.
additionalProperties: true
metadata:
type: object
description: 'Your own labels for the job, such as which of your customers it ran for. At most 16 pairs. A key is 1 to 40 characters from `A-Z a-z 0-9 _ - .`; a value is a string of at most 256 bytes in UTF-8 (bytes, not characters: 200 `é` is 400 bytes) with no control character (U+0000 to U+001F, tab and newline included, and U+007F to U+009F) and no bidirectional embedding, override or isolate (U+202A to U+202E, U+2066 to U+2069). Anything else is refused `422` `metadata_invalid` and no job is created: a bad key or value is named in the message and in `details.key`, while more than 16 pairs is refused with the count and no `details`. A `metadata` that is not an object is refused `400` `invalid_request`; `null` is the same as none. Stored with the job, returned on it, and filterable on `GET /api/v2/jobs`. Set once, here: nothing changes it later. Only jobs sent to a deployment keep it today: Comfy Cloud refuses a non-empty `metadata` with `422` `metadata_not_supported`, and runs a job with empty or absent `metadata` as before.'
maxProperties: 16
additionalProperties:
type: string
description: 'At most 256 bytes in UTF-8, which is what the server counts. `maxLength` counts characters, so it is only an upper bound: a shorter value with multi-byte characters can still be refused.'
maxLength: 256
x-go-type: interface{}
example:
client: acme
batch: '2026-10-05'
responses:
'201':
description: Job created and queued.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'400':
description: '`invalid_request`: `metadata` is not an object. A body that is not JSON at all, or whose `workflow` or `extra_data` is not an object, is a `400` too, answered by the request decoder before any of these checks, with a bare `message` body rather than this envelope.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
description: '`insufficient_credits` (Cloud / serverless only).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: '`not_found`: on a deployment''s address, no deployment there that the caller''s workspace owns.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: '`invalid_workflow` (with per-node details), `workflow_format_ui`, `missing_asset`, `idempotency_key_reuse`, `metadata_invalid` (for a bad key or value, the message and `details.key` name the key; for more than 16 pairs, the message gives the count and there are no `details`), or `metadata_not_supported` (a surface that keeps no job labels yet).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: '`queue_full` (bounded queue depth reached) or, on deployment-scoped surfaces, `deployment_not_ready` (deployment still provisioning/starting) or `deployment_unavailable` (the deployment is ready but its GPU provider is not taking work on it yet), or `rate_limited` (the caller is past a request rate limit). Disambiguate by `error.code`; all four mean back off and retry after `Retry-After`.'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
get:
operationId: listJobs
tags:
- jobs
summary: List jobs, newest first
description: 'Lists jobs newest first. At a deployment''s address, the jobs sent to
that deployment; at the workspace address, every job in the
caller''s workspace. A caller never sees another workspace''s jobs.
Filter by your own labels with `metadata[<key>]=<value>`, up to 3
times; a job is listed only when every pair matches exactly. A 4th
filter, a repeated key, a key outside the key rule, or a value with a
character no label may hold (a control character or a bidirectional
embedding, override or isolate, as on submit) is refused `400`
`invalid_metadata_filter`. A filter value must be URL-encoded; a raw
`;` or a bad `%` escape answers `400` `invalid_metadata_filter`.
Pages: pass the response''s `next_cursor` back as `cursor`, with the
same filters, to read the next page. `next_cursor` is absent on the
last page. Each job appears once across the pages.
List items are a job''s stored record, not the `Job` object: they
carry no `outputs` and no `urls`. Read a job with
`GET /api/v2/jobs/{id}` for its outputs.
Only the serverless platform lists jobs today. Comfy Cloud answers
`501` `not_implemented`.
'
parameters:
- name: limit
in: query
required: false
description: 'Most jobs per page. Above 500 is read as 500; absent means 500. Not a positive integer: `422` `invalid_request`.'
schema:
type: integer
minimum: 1
- name: cursor
in: query
required: false
description: 'The previous page''s `next_cursor`, unchanged. Opaque: do not build or edit one. A cursor that is not well-formed is refused `400` `invalid_cursor`. Cursors are not signed, so a well-formed one is read as a position whether or not this list issued it.'
schema:
type: string
- name: metadata
in: query
required: false
style: deepObject
explode: true
description: Exact-match label filters, written `metadata[client]=acme`. Up to 3; all must match.
schema:
type: object
maxProperties: 3
additionalProperties:
type: string
example:
client: acme
responses:
'200':
description: One page of jobs, newest first.
content:
application/json:
schema:
$ref: '#/components/schemas/JobList'
'400':
description: '`invalid_metadata_filter` or `invalid_cursor`.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: '`not_found`: on a deployment''s address, no deployment there that the caller''s workspace owns.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: '`invalid_request`: `limit` is not a positive integer.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
'501':
description: '`not_implemented`: this surface does not list jobs yet.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/api/v2/jobs/{id}:
get:
operationId: getJob
tags:
- jobs
summary: Job status (the polling workhorse)
description: 'Returns the full job object: current status, the latest progress
snapshot, and every output committed so far (`outputs` populates
incrementally while the job runs). This is the authoritative,
resumable view of a job; everything on the SSE stream is derived
from it.
'
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: The job.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs/{id}/workflow:
get:
operationId: getJobWorkflow
tags:
- jobs
summary: The workflow behind a job — authoring version if pinned, executed graph otherwise
description: "Returns the workflow behind a job. The response's `format` field says\nwhich of two different shapes `workflow` is in:\n\n- `format: save` — the original authoring workflow exactly as saved\n in the Comfy Cloud editor at the version the job ran, including\n canvas layout and frontend-only nodes (e.g. Note nodes; Get/Set\n nodes not yet expanded). Returned only when the job is pinned to a\n specific workflow version — see the \"when you get which\" note\n below.\n- `format: api` — the executed API-format prompt graph the job\n actually ran: frontend-only constructs are gone and Get/Set nodes\n are expanded. This is the same shape `POST /api/v2/jobs`'s\n `workflow` request field takes, and never includes the\n submission's `extra_data`, which can carry a live credential.\n\nAlways branch on `format`, never assume one or the other — which\nshape comes back depends on how the job was submitted, not on\nanything the caller controls per-request.\n\nA deliberate sub-resource, not a field on `GET /api/v2/jobs/{id}` —\nso the polling workhorse stays cheap and a caller pays for this only\nwhen it actually wants the workflow (for example, to recover what\nproduced a given output).\n\nTied to the job's own retention: this 404s under the same conditions\n`GET /api/v2/jobs/{id}` does (unknown, not-yours, or past its\nretention deadline) — there is no separate lifetime for the\nworkflow.\n\n**When you get which:** a job only carries a pinned workflow version\nwhen it was submitted with that association. Today that means jobs\nsubmitted from the Comfy Cloud frontend/editor. Jobs submitted\ndirectly through this v2 API (`POST /api/v2/jobs`) do not carry that\nassociation — v2 job submission has no version-linking fields yet —\nso they always get `format: api`. This is expected, not a bug: it\nwill change once v2 submission grows the same version pinning.\n\nA job pinned to a version also falls back to `format: api` if that\nversion, or the workflow it belongs to, is no longer readable by the\ncaller — for example the caller deleted the workflow since the job\nran. This is the same fallback as an unpinned job, and for the same\nreason: it is preferable to the alternative of erroring the whole\nrequest over data that is genuinely gone.\n"
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: The workflow graph.
content:
application/json:
schema:
$ref: '#/components/schemas/JobWorkflowResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs/{id}/logs:
get:
operationId: getJobLogs
tags:
- jobs
summary: What the run printed
description: "Returns the job's captured execution log. Fetched on demand: a log\nis a debugging artifact a caller wants occasionally, while\n`GET /api/v2/jobs/{id}` is polled to terminal on every run, so the\nlog is a resource of its own rather than a field that would ride\nevery one of those polls to be read at most once.\n\nCaptured whenever the worker reports its own outcome, success and\nfailure alike, since a job that succeeds while producing the wrong\nthing is exactly what a failure-only log cannot explain. A run the\nplatform or the provider killed — out of memory, a crashed worker, a\ntimeout, a job past its maximum runtime — never gets that far, so it\nreaches a terminal status carrying no log at all. That is a real gap\nand worth stating: the failures a caller most wants a log for are\nthe ones least likely to have produced one.\n\n**`204` is the normal answer for a job with no log.** The cases behind\nit: this surface does not capture logs at all, the job has not\nfinished, the job predates log capture, the run was killed before the\nworker could report one, the worker reported none, or capture was\nattempted and failed.\n\nA surface that captures logs may say which side of the job's end the\nanswer falls on, in the optional `No-Log-Reason` response header:\n\n- `not_finished`: the job has not reached a terminal status. It may\n have a log once it does, so a caller that wants one reads again\n after a terminal status.\n- `finished`: the job reached a terminal status with no log captured.\n This does not say why. In particular it does not mean the run\n printed nothing: a killed run, an empty report and a failed capture\n all answer the same.\n\nThe header is absent on a surface that captures no logs, and on a\nbuild that does not send it yet, so its absence says nothing about the\njob; read the job's status instead. A `204` on a job already in a\nterminal state means stop asking, with or without the header, and so\ndoes a missing `urls.logs`.\n\n**Only jobs run on the serverless platform** (a\n`{deployment}.run.comfy.app` host) have one today. An implementation\nthat captures no logs must still serve this operation, answering\n`204` for every job it can read, so that the two answers stay\ndistinct — Comfy Cloud does. A self-hosted deployment on a build\npredating this operation has not implemented it yet and will answer\na routing `404` instead, which is the case `job.urls.logs` exists to\nkeep a client out of: its absence says the surface has no logs at\nall, without a request.\n\nTied to the job's own retention: this `404`s under the same\nconditions `GET /api/v2/jobs/{id}` does (unknown, not-yours, or past\nits retention deadline). Nothing ages a log out ahead of the job's\nown `expires_at`. Account deletion is an exception: it clears the\nworkflow, caller labels and log in a personal workspace the account\noccupies alone. A scrubbed job answers `200` with empty `text`,\n`truncated: false`, `complete: true` and `captured_at` set to the\nscrub time, even if it never captured a worker log.\n\nLive tailing is not offered here yet. When it is, it arrives on this\nsame path under `Accept: text/event-stream`, leaving this\nJSON snapshot the default; its resume semantics will be defined\nthen, against a capture that is incremental. Until then the SSE\n`log` event on `GET /api/v2/jobs/{id}/events` is the reserved live\nrail, and this is the authoritative snapshot it reconciles against.\n"
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: The captured log.
content:
application/json:
schema:
$ref: '#/components/schemas/JobLogs'
'204':
description: This job has no log. A normal answer, not an error — see the description for the cases it covers.
headers:
No-Log-Reason:
$ref: '#/components/headers/NoLogReason'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs/{id}/events:
get:
operationId: getJobEvents
tags:
- jobs
summary: Live event stream (SSE)
description: 'Server-Sent Events stream of the job''s live state. On connect the
client receives the current snapshot (a `status` event, the latest
`progress`, and the most recent `preview` if any), then future
updates. The stream ends after either the terminal `status` event
or a terminal `error` event (see the `error` event below).
Live push only — NOT a replayable log: events carry no `id`, there
is no `Last-Event-ID` resume, and frames emitted while disconnected
are gone. Each `progress` event is a complete snapshot, so a single
one fully re-syncs a reconnecting client; the authoritative state is
always `GET /api/v2/jobs/{id}`.
'
x-sse-events:
status:
description: Every lifecycle transition, including the initial state on connect and queue_position updates while queued.
schema: '#/components/schemas/StatusEvent'
progress:
description: Node- and step-level progress, throttled server-side (~2/s). Complete snapshot per event.
schema: '#/components/schemas/Progress'
preview:
description: In-progress preview image (JPEG, base64), throttled; server keeps only the most recent, pushed on connect.
schema: '#/components/schemas/PreviewEvent'
output:
description: 'Emitted the moment each output asset is committed, carrying the same `Output` object that appears on `job.outputs[]`. A latency optimization only: it lets a client render each result as it lands instead of waiting for the terminal `status` event. It is delivered best-effort over the live broadcast path — an output whose durable asset record is not yet resolvable when its node finishes may be delivered on a slightly later event or, failing that, only in the terminal `status` snapshot — so the authoritative, complete set of outputs is always `job.outputs[]` on `GET /api/v2/jobs/{id}` and on the terminal `status` event. A client must therefore treat these as additive hints and must not assume it receives one per output.'
schema: '#/components/schemas/Output'
log:
description: 'Selected execution log lines, carried while the run is still going. Best-effort diagnostics, and lossy by the same rule as the rest of this stream: lines emitted while a client was disconnected are gone and no `Last-Event-ID` replays them. The authoritative, complete log is the snapshot at `GET /api/v2/jobs/{id}/logs`, which a client re-reads after a terminal status to reconcile whatever it missed — on a surface that captures logs at all. Comfy Cloud does not, and answers `204` there for every job, so this event has nothing to be the live view of; see that operation for what a self-hosted deployment answers. NOT YET EMITTED by the server in the first iteration — reserved in the catalog so the wire contract is stable. Clients must not depend on receiving this event yet: to get a log today, stream to a terminal status and read the snapshot.'
x-sse-not-yet-emitted: true
schema: '#/components/schemas/LogEvent'
error:
description: 'Terminal event sent when the stream ends for a reason OTHER than the job reaching a terminal state — the credential the stream was opened with stopped being accepted (`credential_expired`, the common case for a short-lived Cloud JWT or OAuth access token that expires while a long job is still running), or the job stopped being accessible (`job_not_found`, `forbidden`). Transient upstream failures (5xx, network errors) do NOT end the stream — they are retried on the next poll. No `status` event follows it. Its whole purpose is to make an aborted stream distinguishable from a completed one: without it both simply close, and a client cannot tell "your job finished" from "you were cut off". On `credential_expired` the job itself is unaffected — refresh the credential and reopen the stream, or fall back to `GET /api/v2/jobs/{id}`. A serverless deployment sends only `job_not_found`.
Browser note: a browser `EventSource` also fires a BUILT-IN `error` event on any transport failure, and a server-sent `event: error` frame is delivered to the same listener. The two are told apart by the payload — this event always carries a JSON `ErrorEnvelope` in `data`, the built-in one carries none — so a handler should check for `data` before treating an `error` as an explained, terminal end-of-stream rather than a retryable connection drop.'
schema: '#/components/schemas/ErrorEnvelope'
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: 'SSE stream; see `x-sse-events` for the event catalog. Sent with `Cache-Control: no-cache, no-store, no-transform` and `X-Accel-Buffering: no` so intermediaries neither cache, compress nor buffer the stream.'
content:
text/event-stream:
schema:
type: string
description: Stream of `event:`/`data:` frames. Data payloads are the JSON schemas listed in x-sse-events.
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
description: '`too_many_streams` — the caller already has the maximum number of concurrent GET .../events streams open. Close an existing stream (or wait for one to reach a terminal status) before opening another; GET /api/v2/jobs/{id} remains available as a plain poll regardless of this limit. Or `rate_limited`, the caller past a request rate limit; retry after `Retry-After`.'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'501':
description: '`not_implemented` — this deployment does not yet serve live event streaming. GET /api/v2/jobs/{id} remains available as a plain poll in the meantime.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
$ref: '#/components/responses/DeploymentLookupUnavailable'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs/{id}/cancel:
post:
operationId: cancelJob
tags: