-
Notifications
You must be signed in to change notification settings - Fork 207
Expand file tree
/
Copy pathopenapi-v2.yaml
More file actions
1368 lines (1209 loc) · 56.3 KB
/
Copy pathopenapi-v2.yaml
File metadata and controls
1368 lines (1209 loc) · 56.3 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.
The uploaded asset is referenceable immediately; any content scanning
runs in the background and does not block the response.
~100 MB single-request expectation for v1; chunked/resumable large
uploads are a deliberate open question.
'
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:
file:
type: string
format: binary
description: The raw bytes.
content_type:
type: string
example: image/png
file_path:
type: string
description: Placement path / filename (global-namespace-root form, e.g. `photo.png` or `models/checkpoints/x.safetensors`).
example: photo.png
expected_hash:
type: string
description: Optional client-computed blake3 (`blake3:<hex>`). Verified against the server-computed hash; mismatch is rejected with 409 `hash_mismatch` and no asset is minted.
example: blake3:9f8a1c0d...
tags:
type: array
items:
type: string
description: Category tags (e.g. `input`).
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: 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'
'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), or validation failure.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/assets/from-hash:
post:
operationId: assetFromHash
tags:
- assets
summary: Mint an asset over existing bytes (dedup fast-path)
description: 'Zero-byte fast-path: mints a new asset UUID over a blob the platform
already has, identified by its blake3 hash.
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`).
'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- hash
properties:
hash:
type: string
example: blake3:9f8a1c0d...
file_path:
type: string
example: photo.png
tags:
type: array
items:
type: string
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: Asset minted over the existing blob.
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'200':
description: An identical reference already existed; returned as-is.
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: '`invalid_request` on a deployment: the `file_path`, `tags` or `expires_in` is malformed.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
'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.'
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'
'429':
$ref: '#/components/responses/RateLimited'
'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'
'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'
'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'
'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.
responses:
'201':
description: Job created and queued.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'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'
'422':
description: '`invalid_workflow` (with per-node details), `workflow_format_ui`, `missing_asset`, or `idempotency_key_reuse`.'
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'
'500':
$ref: '#/components/responses/UpstreamError'
/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'
'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'
'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
is a debugging artifact a caller wants occasionally, while
`GET /api/v2/jobs/{id}` is polled to terminal on every run, so the
log is a resource of its own rather than a field that would ride
every one of those polls to be read at most once.
Captured whenever the worker reports its own outcome, success and
failure alike, since a job that succeeds while producing the wrong
thing is exactly what a failure-only log cannot explain. A run the
platform or the provider killed — out of memory, a crashed worker, a
timeout, a job past its maximum runtime — never gets that far, so it
reaches a terminal status carrying no log at all. That is a real gap
and worth stating: the failures a caller most wants a log for are
the ones least likely to have produced one.
**`204` is the normal answer for a job with no log**, and the cases
behind it are deliberately not distinguished: this surface does not
capture logs at all, the job has not finished, the job predates log
capture, the run was killed before the worker could report one,
capture was attempted and failed, or the job ran on the public demo
deployment, which captures and stores the log like every other
serverless deployment but withholds it on read, because that surface
takes callers with no credential and a job id would otherwise be the
only thing between one anonymous caller and another''s run.
Because a `204` never says which of those it is, do not branch on the
reason — but do note that one of them resolves itself. A job that has
not finished may have a log once it does, so a caller that wants one
reads again after a terminal status. A `204` on a job already in a
terminal state is final, and so is a missing `urls.logs`; both mean
stop asking.
**Only jobs run on the serverless platform** (a
`{deployment}.run.comfy.app` host) have one today. An implementation
that captures no logs must still serve this operation, answering
`204` for every job it can read, so that the two answers stay
distinct — Comfy Cloud does. A self-hosted deployment on a build
predating this operation has not implemented it yet and will answer
a routing `404` instead, which is the case `job.urls.logs` exists to
keep a client out of: its absence says the surface has no logs at
all, without a request.
Tied to the job''s own retention: this `404`s under the same
conditions `GET /api/v2/jobs/{id}` does (unknown, not-yours, or past
its retention deadline). Nothing ages a log out ahead of the job''s
own `expires_at`, so a job never outlives its log.
Live tailing is not offered here yet. When it is, it arrives on this
same path under `Accept: text/event-stream`, leaving this
JSON snapshot the default; its resume semantics will be defined
then, against a capture that is incremental. Until then the SSE
`log` event on `GET /api/v2/jobs/{id}/events` is the reserved live
rail, and this is the authoritative snapshot it reconciles against.
'
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.
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'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.
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'
'500':
$ref: '#/components/responses/UpstreamError'
/api/v2/jobs/{id}/cancel:
post:
operationId: cancelJob
tags:
- jobs
summary: Request cancellation
description: 'Requests cancellation and returns the current job object —
`canceling` (interruption takes effect at node/step boundaries),
`canceled` where the provider''s record already shows the interrupt, or
already-terminal. Idempotent: canceling a finished job is a no-op
returning the terminal state. On serverless, GPU seconds consumed
before the interrupt takes effect are still billed.
'
x-retryable: true
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: Current job state.
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'
'500':
$ref: '#/components/responses/UpstreamError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: '`Authorization: Bearer <credential>`. The credential is one of: an account-scoped API key (`comfyui-…`), accepted on Cloud and serverless; a Comfy Cloud session JWT; or an OAuth access token issued for the Comfy Cloud resource. Which kinds a given deployment accepts is deployment configuration — an API key always works on Cloud and serverless, and a deployment that does not accept JWT bearers answers `401` with a message saying so. Self-hosted accepts unauthenticated requests by default and can be configured with a static bearer token.'
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema:
type: string
description: 'Client-generated UUID (recommended). Single-use: the first request to present a key is processed; any later request with the same key is rejected `422` `idempotency_key_reuse` (reject-on-duplicate, no response replay). Keys expire after 24h.'
JobId:
name: id
in: path
required: true
schema:
type: string
example: 7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b
AssetId:
name: id
in: path
required: true
schema:
type: string
example: 9f8a1c0d-2b3e-4f56-8a7b-1c2d3e4f5a6b
BlakeHash:
name: hash
in: path
required: true
schema:
type: string
description: Content hash, written `blake3:<hex>`.
example: blake3:9f8a1c0d...
headers:
RetryAfter:
schema:
type: integer
description: Seconds to wait before retrying.
responses:
Unauthorized:
description: '`unauthorized` — missing or invalid credentials.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
Forbidden:
description: '`forbidden` — authenticated but not allowed.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
NotFound:
description: '`not_found`.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
UpstreamError:
description: '`upstream_error` — an unexpected failure reaching or processing the request in this implementation''s backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation''s own error-mapping notes. Every operation in this contract can fail this way.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
RateLimited:
description: '`rate_limited` — the caller has exceeded a request rate limit for this account. Account/rate-scoped, not resource-specific — this can be returned even for a job, asset or deployment the caller doesn''t own or that doesn''t exist.'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
schemas:
Asset:
type: object
description: 'A user-owned record identified by a server-assigned UUID, backing an immutable blob whose content carries a server-computed blake3 hash. `hash` may be computed lazily: an asset record (and its retrievable bytes) can exist before its hash is filled in.'
required:
- id
- hash
- size_bytes
- content_type
- created_at
- url
- url_expires_at
properties:
id:
type: string
example: 9f8a1c0d-2b3e-4f56-8a7b-1c2d3e4f5a6b
hash:
type: string
nullable: true
description: '`blake3:<hex>`; null while lazily computed.'
example: blake3:9f8a1c0d...
size_bytes:
type: integer
format: int64
example: 4816293
content_type:
type: string
example: image/png
file_path:
type: string
nullable: true
example: photo.png
created_new:
type: boolean
description: 'On create responses: distinguishes a brand-new blob (true) from a dedup hit against bytes the platform already had (false).'
created_at:
type: string
format: date-time
url:
type: string
format: uri
description: Short-lived content URL (signed, or proxy-served).
url_expires_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
nullable: true
description: 'Retention deadline for the asset itself (distinct from `url_expires_at`, the signed URL''s validity). Null or absent means the asset is non-expiring. On a dedup-hit create response the deadline may be later than now + the requested/default retention: re-referencing content extends its retention, never shortens it.'
job_id:
type: string
nullable: true
description: ID of the job that produced this asset. Absent for uploaded assets, which have no producing job.
Job:
type: object
description: One execution of a workflow. Durable from creation until `expires_at`; `outputs` populates incrementally during execution.
required:
- id
- status
- created_at
- started_at
- completed_at
- expires_at
- queue_position
- progress
- outputs
- error
- urls
properties:
id:
type: string
example: 7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b
status:
$ref: '#/components/schemas/JobStatus'
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
nullable: true
completed_at:
type: string
format: date-time
nullable: true
expires_at:
type: string
format: date-time
description: Retention deadline — a platform property, not an API constant.
queue_position:
type: integer
nullable: true
progress:
allOf:
- $ref: '#/components/schemas/Progress'
nullable: true
description: The latest progress snapshot; same data the SSE stream pushes.
outputs:
type: array
items:
$ref: '#/components/schemas/Output'
error:
allOf:
- $ref: '#/components/schemas/JobError'
nullable: true
metrics:
type: object
description: 'Values are nullable (a metric not yet available — e.g. `execution_ms` before a job starts running — is `null`, not omitted); the example below is deliberately all-non-null purely to work around a Spectral/nimma lint-tooling crash on a literal `null` inside a schema `example` combined with `additionalProperties.nullable: true` — the schema itself is unchanged and still allows null values at runtime.'
additionalProperties:
type: integer
nullable: true
example:
queue_ms: 9000
execution_ms: 42000
urls:
$ref: '#/components/schemas/JobUrls'
deployment_id:
type: string
description: 'The deployment the job was sent to: the id in the address it was posted at, which stays the same when the deployment moves to another release. Absent on a surface that runs jobs on no deployment.'
example: dep-0f19a2b3c4d5
release_id:
type: string
description: The release of the deployment's build that ran the job, which can differ from the release the deployment runs now. Absent where the serving surface does not report it.
example: 7c1e9a40-3b2d-4f6a-9e81-0c5d2a7b4f13
JobLogs: