@giveitsmaller/contracts 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2533 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: GISL Compression API
4
+ description: |
5
+ REST API for the GISL (Give It Smaller) file compression and processing service.
6
+
7
+ **Architecture:**
8
+ - Upload files to get a `file_id`
9
+ - Create workflows referencing uploaded files with operations (compress, thumbnail, watermark, merge, archive, convert)
10
+ - Poll status, stream SSE events, or receive webhook callbacks
11
+ - Download results per operation output
12
+
13
+ **Response envelope:**
14
+ All mutation and query endpoints return `{ success: true, data: {...} }` on success
15
+ and `{ success: false, error: "...", details: [...] }` on failure.
16
+ Exceptions: `GET /api/operations/schema` returns raw JSON (CDN-cacheable),
17
+ health probes return flat objects, and `POST /api/contact` returns 204 with no body.
18
+ version: 2.0.0
19
+ contact:
20
+ name: API Support
21
+
22
+ servers:
23
+ - url: http://localhost:8080
24
+ description: Local development
25
+ - url: https://api.staging.giveitsmaller.com
26
+ description: Staging environment
27
+
28
+ tags:
29
+ - name: Upload
30
+ description: File upload operations (single and multipart)
31
+ - name: Workflow
32
+ description: Workflow creation, status, download, and real-time events
33
+ - name: Operations
34
+ description: Operation schema introspection and retry
35
+ - name: Health
36
+ description: Health check endpoints
37
+ - name: Contact
38
+ description: Contact form submissions
39
+
40
+ paths:
41
+ # ============================================
42
+ # UPLOAD ENDPOINTS
43
+ # ============================================
44
+
45
+ /api/uploads:
46
+ post:
47
+ summary: Upload a file
48
+ description: |
49
+ Upload a single file for later use in workflows. Returns a `file_id` that can be
50
+ referenced when creating workflows.
51
+
52
+ No job or workflow is created — the file is stored in S3 and persisted in the
53
+ `uploads` table. Create a workflow via `POST /api/workflows` to process it.
54
+
55
+ For large files, use the multipart upload flow instead
56
+ (`POST /api/uploads/multipart/initiate`).
57
+ operationId: uploadFile
58
+ tags:
59
+ - Upload
60
+ requestBody:
61
+ required: true
62
+ content:
63
+ multipart/form-data:
64
+ schema:
65
+ $ref: '#/components/schemas/SingleUploadRequest'
66
+ responses:
67
+ '200':
68
+ description: File uploaded successfully
69
+ content:
70
+ application/json:
71
+ schema:
72
+ $ref: '#/components/schemas/UploadSuccessEnvelope'
73
+ example:
74
+ success: true
75
+ data:
76
+ file_id: "019539ab-1111-7000-8000-000000000001"
77
+ original_name: "photo.jpg"
78
+ mime_type: "image/jpeg"
79
+ size_bytes: 2457600
80
+ '400':
81
+ description: Validation failed (missing file, invalid filename)
82
+ content:
83
+ application/json:
84
+ schema:
85
+ $ref: '#/components/schemas/ErrorEnvelope'
86
+ example:
87
+ success: false
88
+ error: "File is required"
89
+ '413':
90
+ description: File exceeds maximum upload size
91
+ content:
92
+ application/json:
93
+ schema:
94
+ $ref: '#/components/schemas/ErrorEnvelope'
95
+ example:
96
+ success: false
97
+ error: "File size exceeds maximum allowed (500MB)"
98
+ '415':
99
+ description: Unsupported file type
100
+ content:
101
+ application/json:
102
+ schema:
103
+ $ref: '#/components/schemas/ErrorEnvelope'
104
+ example:
105
+ success: false
106
+ error: "Unsupported MIME type: application/x-msdownload"
107
+ '429':
108
+ description: Rate limit exceeded
109
+ content:
110
+ application/json:
111
+ schema:
112
+ $ref: '#/components/schemas/ErrorEnvelope'
113
+ '500':
114
+ description: Internal server error
115
+ content:
116
+ application/json:
117
+ schema:
118
+ $ref: '#/components/schemas/ErrorEnvelope'
119
+
120
+ /api/uploads/multipart/initiate:
121
+ post:
122
+ summary: Initiate direct S3 multipart upload
123
+ description: |
124
+ Start a direct-to-S3 chunked upload. The client sends the first chunk (recommended
125
+ 8MB) and the total file size. The API uses the first chunk for:
126
+ - MIME type detection (to validate supported formats)
127
+ - Upload throughput measurement (to calculate optimal chunk size)
128
+
129
+ The API stores the first chunk as S3 multipart upload part 1, calculates the
130
+ optimal chunk size and total number of parts, then returns pre-signed PUT URLs
131
+ for the remaining parts. The client uploads parts 2-N directly to S3.
132
+
133
+ **Chunk sizing strategy:**
134
+ - First chunk: fixed 8MB (sent to API)
135
+ - Remaining chunks: API calculates `recommended_chunk_size` from throughput * 5s,
136
+ clamped to 5MB-100MB
137
+ - The last part may be smaller than 5MB (S3 allows this for the final part)
138
+ - Maximum 500 parts per upload
139
+
140
+ **Pre-signed URL TTL:**
141
+ Dynamic based on estimated upload duration * 2, clamped between 900s and 3600s.
142
+
143
+ After all parts are uploaded to S3, call the complete endpoint to finalise.
144
+ operationId: multipartInitiate
145
+ tags:
146
+ - Upload
147
+ requestBody:
148
+ required: true
149
+ content:
150
+ multipart/form-data:
151
+ schema:
152
+ $ref: '#/components/schemas/MultipartInitiateRequest'
153
+ responses:
154
+ '200':
155
+ description: Multipart upload initiated, pre-signed URLs returned
156
+ content:
157
+ application/json:
158
+ schema:
159
+ $ref: '#/components/schemas/MultipartInitiateSuccessEnvelope'
160
+ example:
161
+ success: true
162
+ data:
163
+ upload_id: "019539ab-1111-7000-8000-000000000001"
164
+ mime_type: "image/jpeg"
165
+ first_chunk_etag: '"d8e8fca2dc0f896fd7cb4cb0031ba249"'
166
+ first_chunk_size_bytes: 8388608
167
+ total_parts: 5
168
+ recommended_chunk_size: 10485760
169
+ presigned_urls:
170
+ - part_number: 2
171
+ url: "https://gisl-stg-euw1-input.s3.eu-west-1.amazonaws.com/uploads/...?X-Amz-Expires=1800&..."
172
+ expires_at: "2026-03-06T12:30:00.000Z"
173
+ - part_number: 3
174
+ url: "https://gisl-stg-euw1-input.s3.eu-west-1.amazonaws.com/uploads/...?X-Amz-Expires=1800&..."
175
+ expires_at: "2026-03-06T12:30:00.000Z"
176
+ '400':
177
+ description: Validation failed (missing fields, invalid file)
178
+ content:
179
+ application/json:
180
+ schema:
181
+ $ref: '#/components/schemas/ErrorEnvelope'
182
+ example:
183
+ success: false
184
+ error: "File is required"
185
+ '413':
186
+ description: File exceeds maximum upload size
187
+ content:
188
+ application/json:
189
+ schema:
190
+ $ref: '#/components/schemas/ErrorEnvelope'
191
+ '415':
192
+ description: Unsupported file type
193
+ content:
194
+ application/json:
195
+ schema:
196
+ $ref: '#/components/schemas/ErrorEnvelope'
197
+ '429':
198
+ description: Rate limit exceeded
199
+ content:
200
+ application/json:
201
+ schema:
202
+ $ref: '#/components/schemas/ErrorEnvelope'
203
+ '500':
204
+ description: Internal server error
205
+ content:
206
+ application/json:
207
+ schema:
208
+ $ref: '#/components/schemas/ErrorEnvelope'
209
+
210
+ /api/uploads/multipart/complete:
211
+ post:
212
+ summary: Complete direct S3 multipart upload
213
+ description: |
214
+ Finalise a direct-to-S3 multipart upload after all parts have been uploaded.
215
+
216
+ The client sends the `upload_id` (from the initiate response) and an array of
217
+ part ETags collected from the S3 PUT responses for parts 2-N. The API already
218
+ has the part 1 ETag from the initiate step.
219
+
220
+ The API calls S3 CompleteMultipartUpload and persists the upload record.
221
+ No job or workflow is created — use `POST /api/workflows` to process the file.
222
+ operationId: multipartComplete
223
+ tags:
224
+ - Upload
225
+ requestBody:
226
+ required: true
227
+ content:
228
+ application/json:
229
+ schema:
230
+ $ref: '#/components/schemas/MultipartCompleteRequest'
231
+ responses:
232
+ '201':
233
+ description: |
234
+ Upload completed successfully. The returned `upload_id` is the identifier
235
+ of the finalised upload and can be passed as `file_id` when creating
236
+ workflows via `POST /api/workflows`. File metadata (original name, MIME
237
+ type, size) is available from `GET /api/uploads/{id}/metadata` if the
238
+ client did not retain the values captured during initiate.
239
+ content:
240
+ application/json:
241
+ schema:
242
+ $ref: '#/components/schemas/MultipartCompleteSuccessEnvelope'
243
+ example:
244
+ success: true
245
+ data:
246
+ upload_id: "019539ab-1111-7000-8000-000000000001"
247
+ status: "completed"
248
+ '400':
249
+ description: Invalid upload_id, missing parts, or ETag mismatch
250
+ content:
251
+ application/json:
252
+ schema:
253
+ $ref: '#/components/schemas/ErrorEnvelope'
254
+ example:
255
+ success: false
256
+ error: "Missing ETags for parts: 3, 5"
257
+ '404':
258
+ description: Upload session not found or expired
259
+ content:
260
+ application/json:
261
+ schema:
262
+ $ref: '#/components/schemas/ErrorEnvelope'
263
+ example:
264
+ success: false
265
+ error: "Upload session not found or expired"
266
+ '500':
267
+ description: Internal server error
268
+ content:
269
+ application/json:
270
+ schema:
271
+ $ref: '#/components/schemas/ErrorEnvelope'
272
+
273
+ /api/uploads/{id}/metadata:
274
+ get:
275
+ summary: Get file metadata
276
+ description: |
277
+ Returns metadata extracted from an uploaded file. Useful for inspecting files
278
+ before building workflows (e.g. check dimensions to decide if resize is needed,
279
+ check codec to decide on compression options).
280
+
281
+ Fields vary by MIME type:
282
+ - **Images:** dimensions, color_space, dpi, has_alpha, exif, dominant_colors
283
+ - **Video:** dimensions, duration, codec, fps, audio_codec, bitrate
284
+ - **Audio:** duration, bitrate, channels, sample_rate, codec
285
+ - **Documents:** page_count, dimensions (for PDF)
286
+ operationId: getUploadMetadata
287
+ tags:
288
+ - Upload
289
+ parameters:
290
+ - name: id
291
+ in: path
292
+ required: true
293
+ description: Upload file ID (UUID v7)
294
+ schema:
295
+ $ref: '#/components/schemas/UuidV7'
296
+ responses:
297
+ '200':
298
+ description: File metadata retrieved
299
+ content:
300
+ application/json:
301
+ schema:
302
+ $ref: '#/components/schemas/MetadataSuccessEnvelope'
303
+ example:
304
+ success: true
305
+ data:
306
+ file_id: "019539ab-1111-7000-8000-000000000001"
307
+ original_name: "photo.jpg"
308
+ mime_type: "image/jpeg"
309
+ size_bytes: 4521984
310
+ dimensions:
311
+ width: 4032
312
+ height: 3024
313
+ color_space: "sRGB"
314
+ dpi: 72
315
+ has_alpha: false
316
+ exif:
317
+ camera: "iPhone 15 Pro"
318
+ datetime: "2026-03-01T14:30:00Z"
319
+ gps:
320
+ lat: 51.5074
321
+ lon: -0.1278
322
+ copyright: null
323
+ dominant_colors:
324
+ - "#2a5c3f"
325
+ - "#8b6f4e"
326
+ - "#d4c5a9"
327
+ '404':
328
+ description: Upload not found
329
+ content:
330
+ application/json:
331
+ schema:
332
+ $ref: '#/components/schemas/ErrorEnvelope'
333
+ '500':
334
+ description: Internal server error
335
+ content:
336
+ application/json:
337
+ schema:
338
+ $ref: '#/components/schemas/ErrorEnvelope'
339
+
340
+ # ============================================
341
+ # WORKFLOW ENDPOINTS
342
+ # ============================================
343
+
344
+ /api/workflows:
345
+ post:
346
+ summary: Create a workflow
347
+ description: |
348
+ Create a workflow containing one or more jobs, each with one or more operations.
349
+ Jobs reference uploaded files by `file_id`, or depend on other jobs via `source`
350
+ (for single-input downstream jobs) or `inputs` (for multi-input jobs like merge/archive).
351
+
352
+ **Job source types (exactly one required per job):**
353
+ - `file_id`: Reference an uploaded file directly
354
+ - `source`: Reference another job's output (for DAG pipelines)
355
+ - `inputs`: Reference multiple job outputs (for merge/archive)
356
+
357
+ **Workflow edges** define job dependencies as a DAG. Jobs with no dependencies
358
+ start immediately. Jobs with dependencies wait for upstream jobs to complete.
359
+
360
+ **Default operation:** If `operations` is omitted on a single-input job with
361
+ `file_id`, the default is `[{ "type": "compress" }]` with default options for
362
+ the detected MIME type. Multi-input jobs must always specify operations explicitly.
363
+
364
+ **Multi-input constraint:** Multi-input jobs must have exactly one operation,
365
+ which must be a multi-input type (`merge` or `archive`). Chaining after a
366
+ multi-input operation is not valid — create a downstream edge-sourced job instead.
367
+ operationId: createWorkflow
368
+ tags:
369
+ - Workflow
370
+ requestBody:
371
+ required: true
372
+ content:
373
+ application/json:
374
+ schema:
375
+ $ref: '#/components/schemas/WorkflowCreateRequest'
376
+ responses:
377
+ '201':
378
+ description: Workflow created
379
+ content:
380
+ application/json:
381
+ schema:
382
+ $ref: '#/components/schemas/WorkflowCreateSuccessEnvelope'
383
+ examples:
384
+ simple:
385
+ summary: Single file, single operation
386
+ value:
387
+ success: true
388
+ data:
389
+ workflow_id: "019539ac-2222-7000-8000-000000000001"
390
+ status: "pending"
391
+ jobs:
392
+ - ref: "main"
393
+ job_id: "019539ad-3333-7000-8000-000000000001"
394
+ status: "pending"
395
+ depends_on: []
396
+ operations:
397
+ - id: "019539ae-4444-7000-8000-000000000001"
398
+ type: "compress"
399
+ status: "pending"
400
+ batch:
401
+ summary: Batch compress (3 independent files)
402
+ value:
403
+ success: true
404
+ data:
405
+ workflow_id: "019539ac-2222-7000-8000-000000000002"
406
+ status: "pending"
407
+ jobs:
408
+ - ref: "file1"
409
+ job_id: "019539ad-3333-7000-8000-000000000002"
410
+ status: "pending"
411
+ depends_on: []
412
+ operations:
413
+ - id: "019539ae-4444-7000-8000-000000000002"
414
+ type: "compress"
415
+ status: "pending"
416
+ - ref: "file2"
417
+ job_id: "019539ad-3333-7000-8000-000000000003"
418
+ status: "pending"
419
+ depends_on: []
420
+ operations:
421
+ - id: "019539ae-4444-7000-8000-000000000003"
422
+ type: "compress"
423
+ status: "pending"
424
+ - ref: "file3"
425
+ job_id: "019539ad-3333-7000-8000-000000000004"
426
+ status: "pending"
427
+ depends_on: []
428
+ operations:
429
+ - id: "019539ae-4444-7000-8000-000000000004"
430
+ type: "compress"
431
+ status: "pending"
432
+ '400':
433
+ description: Validation failed (malformed request body)
434
+ content:
435
+ application/json:
436
+ schema:
437
+ $ref: '#/components/schemas/ErrorEnvelope'
438
+ '404':
439
+ description: Referenced file_id not found
440
+ content:
441
+ application/json:
442
+ schema:
443
+ $ref: '#/components/schemas/ErrorEnvelope'
444
+ example:
445
+ success: false
446
+ error: "Upload not found: 019539ab-1111-7000-8000-999999999999"
447
+ '422':
448
+ description: |
449
+ Semantically invalid request. Valid JSON but contains errors such as:
450
+ unknown operation type, invalid option combinations, cyclic dependency
451
+ in workflow_edges, multi-input job with multiple operations, or
452
+ source ref pointing to a non-existent job.
453
+ content:
454
+ application/json:
455
+ schema:
456
+ $ref: '#/components/schemas/ValidationErrorEnvelope'
457
+ example:
458
+ success: false
459
+ error: "INVALID_OPTIONS"
460
+ details:
461
+ - operation: "compress"
462
+ option: "quality"
463
+ message: "Must be between 1 and 100"
464
+ - operation: "thumbnail"
465
+ option: "width"
466
+ message: "Required field"
467
+ '429':
468
+ description: Rate limit exceeded
469
+ content:
470
+ application/json:
471
+ schema:
472
+ $ref: '#/components/schemas/ErrorEnvelope'
473
+ '500':
474
+ description: Internal server error
475
+ content:
476
+ application/json:
477
+ schema:
478
+ $ref: '#/components/schemas/ErrorEnvelope'
479
+
480
+ /api/workflows/{id}/status:
481
+ get:
482
+ summary: Get workflow status
483
+ description: |
484
+ Retrieve the current status of a workflow with nested job and operation breakdown.
485
+ Each operation includes its current progress and, when completed, its result
486
+ (download URL, size, metrics).
487
+ operationId: getWorkflowStatus
488
+ tags:
489
+ - Workflow
490
+ parameters:
491
+ - name: id
492
+ in: path
493
+ required: true
494
+ description: Workflow ID (UUID v7)
495
+ schema:
496
+ $ref: '#/components/schemas/UuidV7'
497
+ responses:
498
+ '200':
499
+ description: Workflow status retrieved
500
+ content:
501
+ application/json:
502
+ schema:
503
+ $ref: '#/components/schemas/WorkflowStatusSuccessEnvelope'
504
+ examples:
505
+ in_progress:
506
+ summary: Workflow in progress
507
+ value:
508
+ success: true
509
+ data:
510
+ workflow_id: "019539ac-2222-7000-8000-000000000001"
511
+ status: "in_progress"
512
+ jobs:
513
+ - ref: "main"
514
+ job_id: "019539ad-3333-7000-8000-000000000001"
515
+ status: "in_progress"
516
+ depends_on: []
517
+ operations:
518
+ - id: "019539ae-4444-7000-8000-000000000001"
519
+ type: "compress"
520
+ status: "in_progress"
521
+ progress: 65
522
+ completed:
523
+ summary: Workflow completed
524
+ value:
525
+ success: true
526
+ data:
527
+ workflow_id: "019539ac-2222-7000-8000-000000000001"
528
+ status: "completed"
529
+ jobs:
530
+ - ref: "main"
531
+ job_id: "019539ad-3333-7000-8000-000000000001"
532
+ status: "completed"
533
+ depends_on: []
534
+ operations:
535
+ - id: "019539ae-4444-7000-8000-000000000001"
536
+ type: "compress"
537
+ status: "completed"
538
+ result:
539
+ download_url: "https://cdn.giveitsmaller.com/jobs/019539ad-.../019539ae-.../photo.jpg?token=..."
540
+ size_bytes: 1105920
541
+ metrics:
542
+ compression_ratio: 0.45
543
+ duration_ms: 2340
544
+ '404':
545
+ description: Workflow not found
546
+ content:
547
+ application/json:
548
+ schema:
549
+ $ref: '#/components/schemas/ErrorEnvelope'
550
+ '500':
551
+ description: Internal server error
552
+ content:
553
+ application/json:
554
+ schema:
555
+ $ref: '#/components/schemas/ErrorEnvelope'
556
+
557
+ /api/workflows/{id}/downloads:
558
+ get:
559
+ summary: Get download URLs
560
+ description: |
561
+ Get download URLs for all completed operation outputs in a workflow.
562
+ Each job's operations are listed with their output files and pre-signed
563
+ download URLs.
564
+ operationId: getWorkflowDownloads
565
+ tags:
566
+ - Workflow
567
+ parameters:
568
+ - name: id
569
+ in: path
570
+ required: true
571
+ description: Workflow ID (UUID v7)
572
+ schema:
573
+ $ref: '#/components/schemas/UuidV7'
574
+ responses:
575
+ '200':
576
+ description: Download URLs retrieved
577
+ content:
578
+ application/json:
579
+ schema:
580
+ $ref: '#/components/schemas/WorkflowDownloadSuccessEnvelope'
581
+ example:
582
+ success: true
583
+ data:
584
+ downloads:
585
+ - ref: "main"
586
+ job_id: "019539ad-3333-7000-8000-000000000001"
587
+ files:
588
+ - operation: "compress"
589
+ operation_id: "019539ae-4444-7000-8000-000000000001"
590
+ filename: "photo.jpg"
591
+ size_bytes: 1105920
592
+ download_url: "https://cdn.giveitsmaller.com/jobs/019539ad-.../019539ae-.../photo.jpg?token=..."
593
+ '404':
594
+ description: Workflow not found
595
+ content:
596
+ application/json:
597
+ schema:
598
+ $ref: '#/components/schemas/ErrorEnvelope'
599
+ '500':
600
+ description: Internal server error
601
+ content:
602
+ application/json:
603
+ schema:
604
+ $ref: '#/components/schemas/ErrorEnvelope'
605
+
606
+ /api/workflows/{id}/events:
607
+ get:
608
+ summary: Stream workflow events (SSE)
609
+ description: |
610
+ Server-Sent Events endpoint for real-time workflow progress. The server pushes
611
+ events as workflow, job, and operation statuses change.
612
+
613
+ The connection stays open until the workflow reaches a terminal state
614
+ (`completed`, `failed`, `partially_failed`) or the client disconnects.
615
+
616
+ **Event types:**
617
+ - `operation.progress` — operation progress update (progress percentage)
618
+ - `operation.completed` — individual operation finished successfully
619
+ - `operation.failed` — individual operation failed
620
+ - `job.completed` — all operations in a job finished
621
+ - `job.failed` — job failed
622
+ - `workflow.completed` — all jobs completed successfully
623
+ - `workflow.failed` — all jobs finished, at least one failed
624
+ - `workflow.partially_failed` — some jobs succeeded, some failed
625
+
626
+ **Event data format:**
627
+ ```
628
+ event: operation.progress
629
+ data: {"job_ref":"main","operation_id":"op-uuid","type":"compress","progress":65}
630
+
631
+ event: operation.completed
632
+ data: {"job_ref":"main","operation_id":"op-uuid","type":"compress","status":"completed","progress":100}
633
+
634
+ event: workflow.completed
635
+ data: {"workflow_id":"wf-uuid","status":"completed"}
636
+ ```
637
+ operationId: streamWorkflowEvents
638
+ tags:
639
+ - Workflow
640
+ parameters:
641
+ - name: id
642
+ in: path
643
+ required: true
644
+ description: Workflow ID (UUID v7)
645
+ schema:
646
+ $ref: '#/components/schemas/UuidV7'
647
+ responses:
648
+ '200':
649
+ description: SSE event stream
650
+ content:
651
+ text/event-stream:
652
+ schema:
653
+ type: string
654
+ description: |
655
+ Server-Sent Events stream. Each event has an `event` field (type)
656
+ and a `data` field (JSON payload). See endpoint description for
657
+ event types and payload shapes.
658
+ '404':
659
+ description: Workflow not found
660
+ content:
661
+ application/json:
662
+ schema:
663
+ $ref: '#/components/schemas/ErrorEnvelope'
664
+ '500':
665
+ description: Internal server error
666
+ content:
667
+ application/json:
668
+ schema:
669
+ $ref: '#/components/schemas/ErrorEnvelope'
670
+
671
+ # ============================================
672
+ # OPERATIONS ENDPOINTS
673
+ # ============================================
674
+
675
+ /api/operations/schema:
676
+ get:
677
+ summary: Get operation schema
678
+ description: |
679
+ Returns the operations schema describing all available operation types,
680
+ their options, constraints, and defaults — grouped by MIME type.
681
+
682
+ **This endpoint does NOT use the standard ResponseEnvelope.** It returns
683
+ the raw schema JSON directly for CDN cacheability (static resource).
684
+
685
+ Supports optional query parameters to filter the schema:
686
+ - `mime_type`: Filter to operations available for a specific MIME type
687
+ - `operation`: Filter to a specific operation type
688
+
689
+ The schema includes conditional validation rules using `depends_on`
690
+ (e.g. `quality` is only valid when `mode: lossy`). Clients should use
691
+ these to build dynamic forms.
692
+ operationId: getOperationsSchema
693
+ tags:
694
+ - Operations
695
+ parameters:
696
+ - name: mime_type
697
+ in: query
698
+ required: false
699
+ description: Filter by MIME type (e.g. `image/jpeg`, `video/mp4`)
700
+ schema:
701
+ type: string
702
+ example: "image/jpeg"
703
+ - name: operation
704
+ in: query
705
+ required: false
706
+ description: Filter by operation type
707
+ schema:
708
+ $ref: '#/components/schemas/OperationType'
709
+ responses:
710
+ '200':
711
+ description: Operation schema (raw JSON, no ResponseEnvelope)
712
+ headers:
713
+ Cache-Control:
714
+ schema:
715
+ type: string
716
+ description: Cache headers for CDN
717
+ example: "public, max-age=3600"
718
+ content:
719
+ application/json:
720
+ schema:
721
+ $ref: '#/components/schemas/OperationsSchemaResponse'
722
+ examples:
723
+ full_schema:
724
+ summary: Full schema (truncated for brevity)
725
+ value:
726
+ schema_version: "1.0.0"
727
+ operations:
728
+ compress:
729
+ description: "Reduce file size while maintaining acceptable quality"
730
+ default: true
731
+ input_model: "single"
732
+ options:
733
+ mode:
734
+ type: "enum"
735
+ values: ["lossy", "lossless", "auto"]
736
+ quality:
737
+ type: "integer"
738
+ min: 1
739
+ max: 100
740
+ depends_on:
741
+ mode: "lossy"
742
+ width:
743
+ type: "integer"
744
+ min: 1
745
+ max: 16384
746
+ height:
747
+ type: "integer"
748
+ min: 1
749
+ max: 16384
750
+ thumbnail:
751
+ description: "Generate a preview image from the source file"
752
+ default: false
753
+ input_model: "single"
754
+ options:
755
+ width:
756
+ type: "integer"
757
+ min: 1
758
+ max: 4096
759
+ required: true
760
+ height:
761
+ type: "integer"
762
+ min: 1
763
+ max: 4096
764
+ required: true
765
+ fit:
766
+ type: "enum"
767
+ values: ["max", "crop", "scale"]
768
+ '500':
769
+ description: Internal server error
770
+ content:
771
+ application/json:
772
+ schema:
773
+ $ref: '#/components/schemas/ErrorEnvelope'
774
+
775
+ /api/operations/{id}/retry:
776
+ post:
777
+ summary: Retry a failed operation
778
+ description: |
779
+ Retry a failed operation without recreating the entire workflow. Creates a new
780
+ operation that replaces the failed one, using the same source file and options.
781
+
782
+ The original failed operation is marked as `retried` and a new operation with
783
+ a new ID is created in `pending` state.
784
+ operationId: retryOperation
785
+ tags:
786
+ - Operations
787
+ parameters:
788
+ - name: id
789
+ in: path
790
+ required: true
791
+ description: Failed operation ID (UUID v7)
792
+ schema:
793
+ $ref: '#/components/schemas/UuidV7'
794
+ responses:
795
+ '202':
796
+ description: Retry accepted, new operation queued
797
+ content:
798
+ application/json:
799
+ schema:
800
+ $ref: '#/components/schemas/RetrySuccessEnvelope'
801
+ example:
802
+ success: true
803
+ data:
804
+ operation_id: "019539af-5555-7000-8000-000000000001"
805
+ original_operation_id: "019539ae-4444-7000-8000-000000000001"
806
+ status: "pending"
807
+ '404':
808
+ description: Operation not found
809
+ content:
810
+ application/json:
811
+ schema:
812
+ $ref: '#/components/schemas/ErrorEnvelope'
813
+ '409':
814
+ description: Operation is not in a failed state, or has already been retried
815
+ content:
816
+ application/json:
817
+ schema:
818
+ $ref: '#/components/schemas/ErrorEnvelope'
819
+ example:
820
+ success: false
821
+ error: "Operation is not in a failed state"
822
+ '500':
823
+ description: Internal server error
824
+ content:
825
+ application/json:
826
+ schema:
827
+ $ref: '#/components/schemas/ErrorEnvelope'
828
+
829
+ # ============================================
830
+ # HEALTH ENDPOINTS
831
+ # ============================================
832
+
833
+ /healthz:
834
+ get:
835
+ summary: Liveness probe
836
+ description: Kubernetes liveness probe endpoint
837
+ operationId: liveness
838
+ tags:
839
+ - Health
840
+ responses:
841
+ '200':
842
+ description: Application is alive
843
+ content:
844
+ application/json:
845
+ schema:
846
+ $ref: '#/components/schemas/LivenessResponse'
847
+ example:
848
+ app: true
849
+
850
+ /readyz:
851
+ get:
852
+ summary: Readiness probe
853
+ description: Kubernetes readiness probe endpoint - checks database and cache connectivity
854
+ operationId: readiness
855
+ tags:
856
+ - Health
857
+ responses:
858
+ '200':
859
+ description: Application is ready to receive traffic
860
+ content:
861
+ application/json:
862
+ schema:
863
+ $ref: '#/components/schemas/ReadinessResponse'
864
+ '503':
865
+ description: Application is not ready
866
+ content:
867
+ application/json:
868
+ schema:
869
+ $ref: '#/components/schemas/ReadinessResponse'
870
+
871
+ # ============================================
872
+ # CONTACT ENDPOINT
873
+ # ============================================
874
+
875
+ /api/contact:
876
+ post:
877
+ summary: Submit contact form
878
+ description: |
879
+ Submit a contact form message. All fields except `name` and `website` are required.
880
+
881
+ The `website` field is a honeypot for bot detection. It is hidden from real users
882
+ via CSS. Legitimate submissions must omit this field or send an empty string.
883
+ The API rejects any request where `website` is non-empty.
884
+
885
+ On success, returns 204 with no body. The API sends the message via its
886
+ configured delivery channel (e.g. email, queue).
887
+ operationId: submitContact
888
+ tags:
889
+ - Contact
890
+ requestBody:
891
+ required: true
892
+ content:
893
+ application/json:
894
+ schema:
895
+ $ref: '#/components/schemas/ContactRequest'
896
+ example:
897
+ name: "Jane Doe"
898
+ email: "jane@example.com"
899
+ subject: "general_enquiry"
900
+ message: "I have a question about supported file formats."
901
+ responses:
902
+ '204':
903
+ description: Contact form submitted successfully. No response body.
904
+ '400':
905
+ description: Validation failed - one or more fields are invalid
906
+ content:
907
+ application/json:
908
+ schema:
909
+ $ref: '#/components/schemas/ContactValidationErrorResponse'
910
+ example:
911
+ errors:
912
+ email:
913
+ - "This value is not a valid email address."
914
+ message:
915
+ - "This field is required."
916
+ '429':
917
+ description: Rate limit exceeded
918
+ content:
919
+ application/json:
920
+ schema:
921
+ $ref: '#/components/schemas/ErrorEnvelope'
922
+ '500':
923
+ description: Internal server error
924
+ content:
925
+ application/json:
926
+ schema:
927
+ $ref: '#/components/schemas/ErrorEnvelope'
928
+
929
+ # ============================================
930
+ # WEBHOOKS (outbound callbacks to consumer URLs)
931
+ # ============================================
932
+
933
+ webhooks:
934
+ workflowCallback:
935
+ post:
936
+ summary: Workflow event callback
937
+ operationId: webhookWorkflowCallback
938
+ description: |
939
+ POSTed to the `callback_url` provided in `WorkflowCreateRequest` when a
940
+ subscribed event occurs. The consumer endpoint must return a 2xx status
941
+ to acknowledge receipt.
942
+ parameters:
943
+ - name: X-GIS-Signature
944
+ in: header
945
+ required: true
946
+ schema:
947
+ type: string
948
+ pattern: '^sha256=[0-9a-f]{64}$'
949
+ description: |
950
+ HMAC-SHA256 signature of the raw request body using the per-workflow
951
+ `webhook_secret` as the key. Format: `sha256=<hex-digest>`.
952
+
953
+ Verification pseudocode:
954
+ ```
955
+ expected = "sha256=" + hex(hmac_sha256(webhook_secret, raw_body))
956
+ valid = constant_time_equal(header_value, expected)
957
+ ```
958
+ example: "sha256=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
959
+ requestBody:
960
+ required: true
961
+ content:
962
+ application/json:
963
+ schema:
964
+ $ref: '#/components/schemas/WebhookPayload'
965
+ responses:
966
+ '200':
967
+ description: Callback acknowledged
968
+ '202':
969
+ description: Callback accepted for processing
970
+ '204':
971
+ description: Callback acknowledged (no body)
972
+
973
+ components:
974
+ schemas:
975
+ # ============================================
976
+ # SHARED PRIMITIVES
977
+ # ============================================
978
+
979
+ UuidV7:
980
+ type: string
981
+ format: uuid
982
+ pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
983
+ description: UUID v7 format identifier (time-ordered)
984
+ example: "019539ab-1111-7000-8000-000000000001"
985
+
986
+ # ============================================
987
+ # RESPONSE ENVELOPES
988
+ # ============================================
989
+
990
+ ResponseEnvelope:
991
+ type: object
992
+ description: |
993
+ Standard response wrapper. All API responses (except GET /api/operations/schema,
994
+ health probes, and POST /api/contact) use this envelope.
995
+ Success: `{ success: true, data: {...} }`.
996
+ Error: `{ success: false, error: "...", details: [...] }`.
997
+ required:
998
+ - success
999
+ properties:
1000
+ success:
1001
+ type: boolean
1002
+
1003
+ ErrorEnvelope:
1004
+ type: object
1005
+ description: Error response envelope
1006
+ required:
1007
+ - success
1008
+ - error
1009
+ properties:
1010
+ success:
1011
+ type: boolean
1012
+ const: false
1013
+ error:
1014
+ type: string
1015
+ description: Human-readable error message
1016
+
1017
+ ValidationErrorEnvelope:
1018
+ type: object
1019
+ description: |
1020
+ Validation error response with structured details.
1021
+ Used for 422 responses where multiple validation issues are reported.
1022
+ required:
1023
+ - success
1024
+ - error
1025
+ - details
1026
+ properties:
1027
+ success:
1028
+ type: boolean
1029
+ const: false
1030
+ error:
1031
+ type: string
1032
+ description: Error code (e.g. "INVALID_OPTIONS")
1033
+ details:
1034
+ type: array
1035
+ description: List of individual validation errors
1036
+ items:
1037
+ type: object
1038
+ required:
1039
+ - message
1040
+ properties:
1041
+ operation:
1042
+ type: string
1043
+ description: Operation type where the error occurred
1044
+ option:
1045
+ type: string
1046
+ description: Option name that failed validation
1047
+ field:
1048
+ type: string
1049
+ description: Field path for non-operation validation errors
1050
+ message:
1051
+ type: string
1052
+ description: Human-readable error message
1053
+
1054
+ # ============================================
1055
+ # STATUS ENUMS
1056
+ # ============================================
1057
+
1058
+ WorkflowStatus:
1059
+ type: string
1060
+ description: |
1061
+ Workflow lifecycle status:
1062
+ - pending: Created but no jobs have started
1063
+ - in_progress: At least one job is running
1064
+ - completed: All jobs completed successfully
1065
+ - failed: All jobs finished, at least one failed, none succeeded
1066
+ - partially_failed: Some jobs succeeded, some failed
1067
+ enum:
1068
+ - pending
1069
+ - in_progress
1070
+ - completed
1071
+ - failed
1072
+ - partially_failed
1073
+
1074
+ JobStatus:
1075
+ type: string
1076
+ description: |
1077
+ Job lifecycle status:
1078
+ - pending: Created, waiting to be scheduled
1079
+ - waiting: Blocked by upstream job dependencies (workflow_edges)
1080
+ - in_progress: At least one operation is running
1081
+ - completed: All operations completed successfully
1082
+ - failed: Job failed (at least one operation failed)
1083
+ enum:
1084
+ - pending
1085
+ - waiting
1086
+ - in_progress
1087
+ - completed
1088
+ - failed
1089
+
1090
+ OperationStatus:
1091
+ type: string
1092
+ description: |
1093
+ Operation lifecycle status:
1094
+ - pending: Created, waiting to start
1095
+ - in_progress: Currently processing
1096
+ - completed: Finished successfully
1097
+ - failed: Encountered an error
1098
+ - retried: Failed and replaced by a new retry operation
1099
+ enum:
1100
+ - pending
1101
+ - in_progress
1102
+ - completed
1103
+ - failed
1104
+ - retried
1105
+
1106
+ # ============================================
1107
+ # OPERATION & JOB TYPES
1108
+ # ============================================
1109
+
1110
+ OperationType:
1111
+ type: string
1112
+ description: |
1113
+ Available operation types:
1114
+ - compress: Reduce file size (images, audio, video, documents)
1115
+ - thumbnail: Legacy thumbnail value. Generates a preview image
1116
+ for any media type via a single Lambda. Currently the only
1117
+ thumbnail value the compression_api publisher emits; retirement
1118
+ is planned after the publisher adopts the four sub-type values
1119
+ below in a follow-up API PR.
1120
+ - thumbnail_image: Image thumbnail sub-type. Backed by a dedicated
1121
+ Rust image Lambda. Not yet emitted by the publisher.
1122
+ - thumbnail_video: Video thumbnail sub-type. Backed by a dedicated
1123
+ FFmpeg Lambda. Not yet emitted.
1124
+ - thumbnail_document: PDF/EPUB thumbnail sub-type. Backed by a
1125
+ dedicated Ghostscript Lambda. Not yet emitted.
1126
+ - thumbnail_office: Office document (DOCX/XLSX/PPTX/ODT/ODS/ODP)
1127
+ thumbnail sub-type. Backed by a dedicated LibreOffice Lambda.
1128
+ Not yet emitted.
1129
+ - watermark: Apply branding/protection (images only)
1130
+ - merge: Concatenate/combine multiple files into one (images, video, audio, documents/PDF). Multi-input.
1131
+ - archive: Bundle files into ZIP/tar.gz (all types). Multi-input.
1132
+ - convert: Change file format (all types)
1133
+
1134
+ Both the legacy `thumbnail` value and the four sub-type values
1135
+ are valid routing targets today during the thumbnail migration
1136
+ window. See `asyncapi/events.yaml` for the full routing
1137
+ vocabulary and the publisher branching rule.
1138
+ enum:
1139
+ - compress
1140
+ - thumbnail
1141
+ - thumbnail_image
1142
+ - thumbnail_video
1143
+ - thumbnail_document
1144
+ - thumbnail_office
1145
+ - watermark
1146
+ - merge
1147
+ - archive
1148
+ - convert
1149
+
1150
+ OperationInputModel:
1151
+ type: string
1152
+ description: |
1153
+ Whether the operation accepts a single file or multiple files:
1154
+ - single: One input file (compress, thumbnail, thumbnail_image,
1155
+ thumbnail_video, thumbnail_document, thumbnail_office,
1156
+ watermark, convert)
1157
+ - multi: Multiple input files (merge, archive)
1158
+ enum:
1159
+ - single
1160
+ - multi
1161
+
1162
+ JobType:
1163
+ type: string
1164
+ description: |
1165
+ Media type category derived from MIME type. Used as the
1166
+ `job_type` SNS message attribute on the `job-requests` topic —
1167
+ the single filter attribute that routes compression traffic to
1168
+ per-media-type compression queues. Not used by any other SNS
1169
+ topic; non-compression operations are routed by `operation_type`
1170
+ on the separate `operations` topic.
1171
+
1172
+ Derivation:
1173
+ - image/* -> image
1174
+ - video/* -> video
1175
+ - audio/* -> audio
1176
+ - document types -> document (PDF, DOCX, XLSX, PPTX, ODT, ODS, ODP, EPUB)
1177
+ enum:
1178
+ - image
1179
+ - video
1180
+ - audio
1181
+ - document
1182
+
1183
+ # ============================================
1184
+ # UPLOAD SCHEMAS
1185
+ # ============================================
1186
+
1187
+ SingleUploadRequest:
1188
+ type: object
1189
+ required:
1190
+ - file
1191
+ properties:
1192
+ file:
1193
+ type: string
1194
+ format: binary
1195
+ description: The file to upload
1196
+ filename:
1197
+ type: string
1198
+ maxLength: 255
1199
+ pattern: '^[^/\\]+$'
1200
+ description: |
1201
+ Original filename with extension (e.g. "photo.jpg").
1202
+ Optional — browsers include filename automatically in multipart uploads.
1203
+ Must not contain directory separators (/ or \).
1204
+ example: "photo.jpg"
1205
+
1206
+ UploadResponse:
1207
+ type: object
1208
+ description: Upload result data (same shape for single and multipart complete)
1209
+ required:
1210
+ - file_id
1211
+ - original_name
1212
+ - mime_type
1213
+ - size_bytes
1214
+ properties:
1215
+ file_id:
1216
+ $ref: '#/components/schemas/UuidV7'
1217
+ description: Unique file identifier for use in workflow creation
1218
+ original_name:
1219
+ type: string
1220
+ description: Original filename
1221
+ example: "photo.jpg"
1222
+ mime_type:
1223
+ type: string
1224
+ description: Detected MIME type
1225
+ example: "image/jpeg"
1226
+ size_bytes:
1227
+ type: integer
1228
+ format: int64
1229
+ minimum: 1
1230
+ description: File size in bytes
1231
+ example: 2457600
1232
+
1233
+ UploadSuccessEnvelope:
1234
+ type: object
1235
+ required:
1236
+ - success
1237
+ - data
1238
+ properties:
1239
+ success:
1240
+ type: boolean
1241
+ const: true
1242
+ data:
1243
+ $ref: '#/components/schemas/UploadResponse'
1244
+
1245
+ MultipartInitiateRequest:
1246
+ type: object
1247
+ required:
1248
+ - file
1249
+ - filename
1250
+ - total_size
1251
+ properties:
1252
+ file:
1253
+ type: string
1254
+ format: binary
1255
+ description: |
1256
+ First chunk of the file (recommended 8MB).
1257
+ Used for MIME type detection and throughput measurement.
1258
+ Stored as S3 multipart upload part 1.
1259
+ filename:
1260
+ type: string
1261
+ maxLength: 255
1262
+ pattern: '^[^/\\]+$'
1263
+ description: Original filename with extension. Must not contain directory separators.
1264
+ example: "photo.jpg"
1265
+ total_size:
1266
+ type: integer
1267
+ format: int64
1268
+ minimum: 1
1269
+ description: Total file size in bytes (all chunks combined)
1270
+
1271
+ MultipartInitiateResponse:
1272
+ type: object
1273
+ required:
1274
+ - upload_id
1275
+ - mime_type
1276
+ - first_chunk_etag
1277
+ - first_chunk_size_bytes
1278
+ - total_parts
1279
+ - recommended_chunk_size
1280
+ - presigned_urls
1281
+ properties:
1282
+ upload_id:
1283
+ $ref: '#/components/schemas/UuidV7'
1284
+ description: |
1285
+ Multipart upload session identifier. Pass this as `upload_id` in the
1286
+ complete request; after completion, pass the same value as `file_id`
1287
+ when creating workflows via `POST /api/workflows`.
1288
+ mime_type:
1289
+ type: string
1290
+ description: MIME type detected from the first chunk
1291
+ example: "image/jpeg"
1292
+ first_chunk_etag:
1293
+ type: string
1294
+ description: ETag of the first chunk stored as S3 part 1
1295
+ example: '"d8e8fca2dc0f896fd7cb4cb0031ba249"'
1296
+ first_chunk_size_bytes:
1297
+ type: integer
1298
+ description: Size of the first chunk received (for client validation)
1299
+ example: 8388608
1300
+ total_parts:
1301
+ type: integer
1302
+ minimum: 2
1303
+ maximum: 500
1304
+ description: |
1305
+ Total number of parts. The client slices the remaining file into
1306
+ exactly (total_parts - 1) chunks using recommended_chunk_size.
1307
+ The last chunk may be smaller.
1308
+ example: 5
1309
+ recommended_chunk_size:
1310
+ type: integer
1311
+ minimum: 5242880
1312
+ maximum: 104857600
1313
+ description: |
1314
+ Chunk size in bytes for remaining parts. Calculated from first chunk
1315
+ throughput * 5s target, clamped to 5MB-100MB. The last chunk may be
1316
+ smaller than 5MB.
1317
+ example: 10485760
1318
+ presigned_urls:
1319
+ type: array
1320
+ description: |
1321
+ Pre-signed S3 PUT URLs for parts 2 through total_parts.
1322
+ Each URL accepts a PUT request with raw chunk bytes as body.
1323
+ Collect the ETag from each S3 response for the complete request.
1324
+ items:
1325
+ $ref: '#/components/schemas/PresignedUrlPart'
1326
+
1327
+ MultipartInitiateSuccessEnvelope:
1328
+ type: object
1329
+ required:
1330
+ - success
1331
+ - data
1332
+ properties:
1333
+ success:
1334
+ type: boolean
1335
+ const: true
1336
+ data:
1337
+ $ref: '#/components/schemas/MultipartInitiateResponse'
1338
+
1339
+ PresignedUrlPart:
1340
+ type: object
1341
+ required:
1342
+ - part_number
1343
+ - url
1344
+ - expires_at
1345
+ properties:
1346
+ part_number:
1347
+ type: integer
1348
+ minimum: 2
1349
+ description: S3 multipart part number (starts at 2, API handled part 1)
1350
+ url:
1351
+ type: string
1352
+ format: uri
1353
+ description: Pre-signed S3 PUT URL. Send PUT with raw binary chunk as body.
1354
+ expires_at:
1355
+ type: string
1356
+ format: date-time
1357
+ description: |
1358
+ ISO 8601 expiry timestamp. TTL is dynamic: estimated_upload_duration * 2,
1359
+ clamped between 900s and 3600s.
1360
+
1361
+ MultipartCompleteRequest:
1362
+ type: object
1363
+ required:
1364
+ - upload_id
1365
+ - parts
1366
+ properties:
1367
+ upload_id:
1368
+ $ref: '#/components/schemas/UuidV7'
1369
+ description: Multipart upload session identifier from the initiate response
1370
+ parts:
1371
+ type: array
1372
+ description: |
1373
+ ETags for parts 2 through total_parts, collected from S3 PUT responses.
1374
+ Part 1 ETag is already known from the initiate step.
1375
+ minItems: 1
1376
+ items:
1377
+ type: object
1378
+ required:
1379
+ - part_number
1380
+ - etag
1381
+ properties:
1382
+ part_number:
1383
+ type: integer
1384
+ minimum: 2
1385
+ description: S3 part number (must match presigned_urls part_number)
1386
+ etag:
1387
+ type: string
1388
+ description: ETag from S3 PUT response header
1389
+ example: '"d8e8fca2dc0f896fd7cb4cb0031ba249"'
1390
+
1391
+ MultipartCompleteResponse:
1392
+ type: object
1393
+ description: |
1394
+ Result of finalising a multipart upload. Intentionally narrower than
1395
+ `UploadResponse` (single-upload shape) — the server returns only the
1396
+ finalised `upload_id` and a completion status. Clients who need file
1397
+ metadata (original name, MIME type, size) can use the values captured
1398
+ during initiate, or call `GET /api/uploads/{id}/metadata`.
1399
+ required:
1400
+ - upload_id
1401
+ - status
1402
+ properties:
1403
+ upload_id:
1404
+ $ref: '#/components/schemas/UuidV7'
1405
+ description: |
1406
+ Identifier of the finalised upload. Pass this as `file_id` when
1407
+ creating workflows via `POST /api/workflows` — the `file_id`
1408
+ parameter on the workflows endpoint accepts any upload identifier.
1409
+ status:
1410
+ type: string
1411
+ enum:
1412
+ - completed
1413
+ description: Terminal status of the multipart upload session.
1414
+
1415
+ MultipartCompleteSuccessEnvelope:
1416
+ type: object
1417
+ required:
1418
+ - success
1419
+ - data
1420
+ properties:
1421
+ success:
1422
+ type: boolean
1423
+ const: true
1424
+ data:
1425
+ $ref: '#/components/schemas/MultipartCompleteResponse'
1426
+
1427
+ # ============================================
1428
+ # METADATA SCHEMAS
1429
+ # ============================================
1430
+
1431
+ MetadataResponse:
1432
+ type: object
1433
+ description: |
1434
+ File metadata. Fields vary by MIME type. Common fields are always present;
1435
+ type-specific fields (dimensions, exif, duration, etc.) are included when
1436
+ available for the file type.
1437
+ required:
1438
+ - file_id
1439
+ - original_name
1440
+ - mime_type
1441
+ - size_bytes
1442
+ properties:
1443
+ file_id:
1444
+ $ref: '#/components/schemas/UuidV7'
1445
+ original_name:
1446
+ type: string
1447
+ example: "photo.jpg"
1448
+ mime_type:
1449
+ type: string
1450
+ example: "image/jpeg"
1451
+ size_bytes:
1452
+ type: integer
1453
+ format: int64
1454
+ example: 4521984
1455
+ # Image fields
1456
+ dimensions:
1457
+ type: object
1458
+ description: Image/video dimensions (images, video, PDF)
1459
+ properties:
1460
+ width:
1461
+ type: integer
1462
+ example: 4032
1463
+ height:
1464
+ type: integer
1465
+ example: 3024
1466
+ color_space:
1467
+ type: string
1468
+ description: Color space (images)
1469
+ example: "sRGB"
1470
+ dpi:
1471
+ type: integer
1472
+ description: Dots per inch (images, PDF)
1473
+ example: 72
1474
+ has_alpha:
1475
+ type: boolean
1476
+ description: Whether the image has an alpha channel (images)
1477
+ exif:
1478
+ type: object
1479
+ description: EXIF metadata (images)
1480
+ properties:
1481
+ camera:
1482
+ type: string
1483
+ example: "iPhone 15 Pro"
1484
+ datetime:
1485
+ type: string
1486
+ format: date-time
1487
+ gps:
1488
+ type: object
1489
+ properties:
1490
+ lat:
1491
+ type: number
1492
+ format: double
1493
+ lon:
1494
+ type: number
1495
+ format: double
1496
+ copyright:
1497
+ type:
1498
+ - string
1499
+ - "null"
1500
+ dominant_colors:
1501
+ type: array
1502
+ description: Dominant colors as hex strings (images)
1503
+ items:
1504
+ type: string
1505
+ pattern: '^#[0-9a-fA-F]{6}$'
1506
+ example: ["#2a5c3f", "#8b6f4e", "#d4c5a9"]
1507
+ # Video/Audio fields
1508
+ duration:
1509
+ type: number
1510
+ format: double
1511
+ description: Duration in seconds (video, audio)
1512
+ codec:
1513
+ type: string
1514
+ description: Video/audio codec
1515
+ fps:
1516
+ type: number
1517
+ format: double
1518
+ description: Frames per second (video)
1519
+ audio_codec:
1520
+ type: string
1521
+ description: Audio codec (video)
1522
+ bitrate:
1523
+ type: integer
1524
+ description: Bitrate in bps (video, audio)
1525
+ channels:
1526
+ type: integer
1527
+ description: Audio channels (audio)
1528
+ sample_rate:
1529
+ type: integer
1530
+ description: Sample rate in Hz (audio)
1531
+ # Document fields
1532
+ page_count:
1533
+ type: integer
1534
+ description: Number of pages (documents)
1535
+
1536
+ MetadataSuccessEnvelope:
1537
+ type: object
1538
+ required:
1539
+ - success
1540
+ - data
1541
+ properties:
1542
+ success:
1543
+ type: boolean
1544
+ const: true
1545
+ data:
1546
+ $ref: '#/components/schemas/MetadataResponse'
1547
+
1548
+ # ============================================
1549
+ # WORKFLOW REQUEST SCHEMAS
1550
+ # ============================================
1551
+
1552
+ WorkflowCreateRequest:
1553
+ type: object
1554
+ required:
1555
+ - jobs
1556
+ properties:
1557
+ jobs:
1558
+ type: array
1559
+ description: List of jobs in this workflow
1560
+ minItems: 1
1561
+ items:
1562
+ $ref: '#/components/schemas/JobDefinition'
1563
+ workflow_edges:
1564
+ type: array
1565
+ description: |
1566
+ DAG dependency edges between jobs. Each edge defines that a downstream
1567
+ job depends on an upstream job's output. Jobs with no incoming edges
1568
+ start immediately. Jobs with dependencies wait for all upstream jobs.
1569
+ items:
1570
+ $ref: '#/components/schemas/WorkflowEdge'
1571
+ callback_url:
1572
+ type:
1573
+ - string
1574
+ - "null"
1575
+ format: uri
1576
+ pattern: '^https://'
1577
+ description: |
1578
+ Webhook URL (HTTPS only). The API POSTs a `WebhookPayload` JSON body to
1579
+ this URL when matching events occur. The payload includes event type,
1580
+ delivery ID, timestamp, and full workflow state with job results and
1581
+ download URLs. Must use HTTPS to prevent credential leakage and SSRF
1582
+ against internal endpoints.
1583
+
1584
+ **Signature verification:**
1585
+ Each request includes an `X-GIS-Signature` header containing an
1586
+ HMAC-SHA256 hex digest of the raw request body, using the per-workflow
1587
+ `webhook_secret` (returned in the workflow creation response) as the key.
1588
+ Header format: `sha256=<hex(hmac-sha256(webhook_secret, raw_body))>`.
1589
+ Consumers MUST verify the signature before processing the payload.
1590
+ callback_events:
1591
+ type: array
1592
+ description: |
1593
+ Which events trigger the webhook callback. Defaults to terminal events only.
1594
+ items:
1595
+ $ref: '#/components/schemas/CallbackEventType'
1596
+ default:
1597
+ - "workflow.completed"
1598
+ - "workflow.failed"
1599
+ - "workflow.partially_failed"
1600
+ export:
1601
+ $ref: '#/components/schemas/ExportConfig'
1602
+
1603
+ JobDefinition:
1604
+ type: object
1605
+ description: |
1606
+ A job within a workflow. Each job must have exactly one source:
1607
+ - `file_id`: Direct reference to an uploaded file
1608
+ - `source`: Reference another job's output (single-input downstream)
1609
+ - `inputs`: Reference multiple job outputs (multi-input: merge/archive)
1610
+
1611
+ Future versions will add `url` and `import` source types for external
1612
+ file references (V2).
1613
+
1614
+ For single-input jobs with `file_id`, `operations` defaults to
1615
+ `[{ "type": "compress" }]` if omitted. Multi-input jobs must specify
1616
+ operations explicitly.
1617
+ required:
1618
+ - ref
1619
+ properties:
1620
+ ref:
1621
+ type: string
1622
+ description: |
1623
+ Unique reference label within this workflow. Used in workflow_edges,
1624
+ source references, and response payloads to identify jobs.
1625
+ example: "main"
1626
+ file_id:
1627
+ $ref: '#/components/schemas/UuidV7'
1628
+ description: Reference to an uploaded file
1629
+ source:
1630
+ $ref: '#/components/schemas/JobSource'
1631
+ inputs:
1632
+ type: array
1633
+ description: |
1634
+ Multiple input references for multi-input operations (merge, archive).
1635
+ Each input references a specific job's operation output.
1636
+ minItems: 2
1637
+ items:
1638
+ $ref: '#/components/schemas/JobInput'
1639
+ operations:
1640
+ type: array
1641
+ description: |
1642
+ Ordered list of operations to perform. Executed sequentially — each
1643
+ operation consumes the previous operation's output. All intermediate
1644
+ and final outputs are kept.
1645
+
1646
+ Multi-input jobs must have exactly one operation (merge or archive).
1647
+ items:
1648
+ $ref: '#/components/schemas/OperationDefinition'
1649
+ oneOf:
1650
+ - required: [file_id]
1651
+ description: Single-input job referencing an uploaded file
1652
+ - required: [source]
1653
+ description: Single-input downstream job referencing another job's output
1654
+ - required: [inputs]
1655
+ description: Multi-input job (merge/archive)
1656
+ allOf:
1657
+ - if:
1658
+ required: [inputs]
1659
+ then:
1660
+ properties:
1661
+ operations:
1662
+ minItems: 1
1663
+ maxItems: 1
1664
+ items:
1665
+ properties:
1666
+ type:
1667
+ enum: [merge, archive]
1668
+ required: [operations]
1669
+ description: |
1670
+ Multi-input jobs must have exactly one operation, and it must be
1671
+ a multi-input type (merge or archive).
1672
+
1673
+ JobSource:
1674
+ type: object
1675
+ description: |
1676
+ Reference to another job's output for single-input downstream jobs.
1677
+ The input is the referenced job's output, resolved via the incoming
1678
+ workflow edge.
1679
+ required:
1680
+ - ref
1681
+ properties:
1682
+ ref:
1683
+ type: string
1684
+ description: Reference label of the upstream job
1685
+ example: "compress-step"
1686
+ operation:
1687
+ type: string
1688
+ description: |
1689
+ Specific operation output to use from the upstream job.
1690
+ If omitted, uses the last operation's output.
1691
+
1692
+ JobInput:
1693
+ type: object
1694
+ description: |
1695
+ Single input reference for multi-input operations. Each references a
1696
+ specific job and optionally a specific operation output from that job.
1697
+ required:
1698
+ - ref
1699
+ properties:
1700
+ ref:
1701
+ type: string
1702
+ description: Reference label of the upstream job
1703
+ example: "intro"
1704
+ operation:
1705
+ type: string
1706
+ description: |
1707
+ Specific operation output to use. If omitted, uses the last
1708
+ operation's output.
1709
+ per_input_options:
1710
+ type: object
1711
+ description: |
1712
+ Per-input option overrides. For merge operations, individual inputs
1713
+ can override global transition settings for the join point preceding
1714
+ this input. Keys are option names, values are the override values.
1715
+ additionalProperties: true
1716
+
1717
+ OperationDefinition:
1718
+ type: object
1719
+ description: Definition of a single operation within a job
1720
+ required:
1721
+ - type
1722
+ properties:
1723
+ type:
1724
+ $ref: '#/components/schemas/OperationType'
1725
+ options:
1726
+ type: object
1727
+ description: |
1728
+ Operation-specific options. The available options and their validation
1729
+ rules depend on the operation type and the input file's MIME type.
1730
+ See `GET /api/operations/schema` for the full schema.
1731
+
1732
+ Options are validated against the schema using JSON Schema if/then/else
1733
+ rules. For example, `quality` is only valid when `mode: lossy` for
1734
+ compress operations.
1735
+ additionalProperties: true
1736
+
1737
+ WorkflowEdge:
1738
+ type: object
1739
+ description: |
1740
+ Directed edge in the workflow DAG. Defines that the downstream job
1741
+ depends on the upstream job completing successfully.
1742
+ required:
1743
+ - from
1744
+ - to
1745
+ properties:
1746
+ from:
1747
+ type: string
1748
+ description: Reference label of the upstream job
1749
+ example: "compress-intro"
1750
+ to:
1751
+ type: string
1752
+ description: Reference label of the downstream job
1753
+ example: "merge-all"
1754
+
1755
+ # ============================================
1756
+ # WORKFLOW RESPONSE SCHEMAS
1757
+ # ============================================
1758
+
1759
+ WorkflowCreateResponse:
1760
+ type: object
1761
+ required:
1762
+ - workflow_id
1763
+ - status
1764
+ - jobs
1765
+ properties:
1766
+ workflow_id:
1767
+ $ref: '#/components/schemas/UuidV7'
1768
+ status:
1769
+ $ref: '#/components/schemas/WorkflowStatus'
1770
+ jobs:
1771
+ type: array
1772
+ items:
1773
+ $ref: '#/components/schemas/JobResponse'
1774
+ webhook_secret:
1775
+ type:
1776
+ - string
1777
+ - "null"
1778
+ readOnly: true
1779
+ minLength: 64
1780
+ maxLength: 64
1781
+ pattern: '^[0-9a-f]{64}$'
1782
+ description: |
1783
+ HMAC-SHA256 signing key for webhook verification. Present only when
1784
+ `callback_url` was provided in the request. This is the only time the
1785
+ secret is exposed — it does not appear in status queries.
1786
+
1787
+ Use this key to verify the `X-GIS-Signature` header on incoming webhook
1788
+ requests: `sha256=<hex(hmac-sha256(webhook_secret, raw_body))>`.
1789
+ example: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
1790
+
1791
+ WorkflowCreateSuccessEnvelope:
1792
+ type: object
1793
+ required:
1794
+ - success
1795
+ - data
1796
+ properties:
1797
+ success:
1798
+ type: boolean
1799
+ const: true
1800
+ data:
1801
+ $ref: '#/components/schemas/WorkflowCreateResponse'
1802
+
1803
+ WorkflowStatusResponse:
1804
+ type: object
1805
+ required:
1806
+ - workflow_id
1807
+ - status
1808
+ - jobs
1809
+ properties:
1810
+ workflow_id:
1811
+ $ref: '#/components/schemas/UuidV7'
1812
+ status:
1813
+ $ref: '#/components/schemas/WorkflowStatus'
1814
+ jobs:
1815
+ type: array
1816
+ items:
1817
+ $ref: '#/components/schemas/JobResponse'
1818
+
1819
+ WorkflowStatusSuccessEnvelope:
1820
+ type: object
1821
+ required:
1822
+ - success
1823
+ - data
1824
+ properties:
1825
+ success:
1826
+ type: boolean
1827
+ const: true
1828
+ data:
1829
+ $ref: '#/components/schemas/WorkflowStatusResponse'
1830
+
1831
+ JobResponse:
1832
+ type: object
1833
+ description: Job status within a workflow response
1834
+ required:
1835
+ - ref
1836
+ - job_id
1837
+ - status
1838
+ - depends_on
1839
+ - operations
1840
+ properties:
1841
+ ref:
1842
+ type: string
1843
+ description: Job reference label
1844
+ example: "main"
1845
+ job_id:
1846
+ $ref: '#/components/schemas/UuidV7'
1847
+ status:
1848
+ $ref: '#/components/schemas/JobStatus'
1849
+ depends_on:
1850
+ type: array
1851
+ description: List of upstream job refs this job depends on
1852
+ items:
1853
+ type: string
1854
+ operations:
1855
+ type: array
1856
+ items:
1857
+ $ref: '#/components/schemas/OperationResponse'
1858
+
1859
+ OperationResponse:
1860
+ type: object
1861
+ description: Operation status within a job response
1862
+ required:
1863
+ - id
1864
+ - type
1865
+ - status
1866
+ properties:
1867
+ id:
1868
+ $ref: '#/components/schemas/UuidV7'
1869
+ type:
1870
+ $ref: '#/components/schemas/OperationType'
1871
+ status:
1872
+ $ref: '#/components/schemas/OperationStatus'
1873
+ progress:
1874
+ type: integer
1875
+ minimum: 0
1876
+ maximum: 100
1877
+ description: Progress percentage (0-100). Present when in_progress or completed.
1878
+ result:
1879
+ $ref: '#/components/schemas/OperationResult'
1880
+
1881
+ OperationResult:
1882
+ type: object
1883
+ description: |
1884
+ Result of a completed operation. Present only when operation status is `completed`.
1885
+ required:
1886
+ - download_url
1887
+ - size_bytes
1888
+ properties:
1889
+ download_url:
1890
+ type: string
1891
+ format: uri
1892
+ description: Pre-signed download URL for the operation output
1893
+ size_bytes:
1894
+ type: integer
1895
+ format: int64
1896
+ description: Output file size in bytes
1897
+ export_key:
1898
+ type: string
1899
+ description: Key in the customer's export destination (if export configured)
1900
+ metrics:
1901
+ type: object
1902
+ description: Operation-specific performance metrics
1903
+ properties:
1904
+ compression_ratio:
1905
+ type: number
1906
+ format: double
1907
+ description: Ratio of output size to input size (e.g. 0.45 = 55% reduction)
1908
+ duration_ms:
1909
+ type: integer
1910
+ description: Processing time in milliseconds
1911
+
1912
+ # ============================================
1913
+ # WORKFLOW DOWNLOAD SCHEMAS
1914
+ # ============================================
1915
+
1916
+ WorkflowDownloadResponse:
1917
+ type: object
1918
+ required:
1919
+ - downloads
1920
+ properties:
1921
+ downloads:
1922
+ type: array
1923
+ items:
1924
+ $ref: '#/components/schemas/JobDownload'
1925
+
1926
+ WorkflowDownloadSuccessEnvelope:
1927
+ type: object
1928
+ required:
1929
+ - success
1930
+ - data
1931
+ properties:
1932
+ success:
1933
+ type: boolean
1934
+ const: true
1935
+ data:
1936
+ $ref: '#/components/schemas/WorkflowDownloadResponse'
1937
+
1938
+ JobDownload:
1939
+ type: object
1940
+ required:
1941
+ - ref
1942
+ - job_id
1943
+ - files
1944
+ properties:
1945
+ ref:
1946
+ type: string
1947
+ description: Job reference label
1948
+ job_id:
1949
+ $ref: '#/components/schemas/UuidV7'
1950
+ files:
1951
+ type: array
1952
+ items:
1953
+ $ref: '#/components/schemas/OperationDownload'
1954
+
1955
+ OperationDownload:
1956
+ type: object
1957
+ required:
1958
+ - operation
1959
+ - operation_id
1960
+ - filename
1961
+ - size_bytes
1962
+ - download_url
1963
+ properties:
1964
+ operation:
1965
+ type: string
1966
+ description: Operation type that produced this file
1967
+ operation_id:
1968
+ $ref: '#/components/schemas/UuidV7'
1969
+ filename:
1970
+ type: string
1971
+ description: Output filename
1972
+ example: "photo.jpg"
1973
+ size_bytes:
1974
+ type: integer
1975
+ format: int64
1976
+ description: Output file size in bytes
1977
+ download_url:
1978
+ type: string
1979
+ format: uri
1980
+ description: Pre-signed download URL
1981
+
1982
+ # ============================================
1983
+ # SSE EVENT SCHEMAS
1984
+ # ============================================
1985
+
1986
+ SseEventType:
1987
+ type: string
1988
+ description: |
1989
+ Server-Sent Event types pushed on the /events endpoint:
1990
+ - operation.progress: Progress update for an operation
1991
+ - operation.completed: Operation finished successfully
1992
+ - operation.failed: Operation encountered an error
1993
+ - job.completed: All operations in a job completed
1994
+ - job.failed: Job failed
1995
+ - workflow.completed: All jobs completed successfully
1996
+ - workflow.failed: All jobs finished, at least one failed
1997
+ - workflow.partially_failed: Some succeeded, some failed
1998
+ enum:
1999
+ - operation.progress
2000
+ - operation.completed
2001
+ - operation.failed
2002
+ - job.completed
2003
+ - job.failed
2004
+ - workflow.completed
2005
+ - workflow.failed
2006
+ - workflow.partially_failed
2007
+
2008
+ SseOperationProgressData:
2009
+ type: object
2010
+ description: Payload for `operation.progress` events
2011
+ required:
2012
+ - job_ref
2013
+ - operation_id
2014
+ - type
2015
+ - progress
2016
+ properties:
2017
+ job_ref:
2018
+ type: string
2019
+ operation_id:
2020
+ $ref: '#/components/schemas/UuidV7'
2021
+ type:
2022
+ $ref: '#/components/schemas/OperationType'
2023
+ progress:
2024
+ type: integer
2025
+ minimum: 0
2026
+ maximum: 100
2027
+
2028
+ SseOperationCompletedData:
2029
+ type: object
2030
+ description: Payload for `operation.completed` events
2031
+ required:
2032
+ - job_ref
2033
+ - operation_id
2034
+ - type
2035
+ - status
2036
+ - progress
2037
+ properties:
2038
+ job_ref:
2039
+ type: string
2040
+ operation_id:
2041
+ $ref: '#/components/schemas/UuidV7'
2042
+ type:
2043
+ $ref: '#/components/schemas/OperationType'
2044
+ status:
2045
+ type: string
2046
+ const: "completed"
2047
+ progress:
2048
+ type: integer
2049
+ const: 100
2050
+ result:
2051
+ $ref: '#/components/schemas/OperationResult'
2052
+
2053
+ SseOperationFailedData:
2054
+ type: object
2055
+ description: Payload for `operation.failed` events
2056
+ required:
2057
+ - job_ref
2058
+ - operation_id
2059
+ - type
2060
+ - status
2061
+ - error_code
2062
+ - error_message
2063
+ properties:
2064
+ job_ref:
2065
+ type: string
2066
+ operation_id:
2067
+ $ref: '#/components/schemas/UuidV7'
2068
+ type:
2069
+ $ref: '#/components/schemas/OperationType'
2070
+ status:
2071
+ type: string
2072
+ const: "failed"
2073
+ error_code:
2074
+ type: string
2075
+ error_message:
2076
+ type: string
2077
+
2078
+ SseJobCompletedData:
2079
+ type: object
2080
+ description: Payload for `job.completed` events
2081
+ required:
2082
+ - job_ref
2083
+ - job_id
2084
+ - status
2085
+ properties:
2086
+ job_ref:
2087
+ type: string
2088
+ job_id:
2089
+ $ref: '#/components/schemas/UuidV7'
2090
+ status:
2091
+ type: string
2092
+ const: "completed"
2093
+
2094
+ SseJobFailedData:
2095
+ type: object
2096
+ description: Payload for `job.failed` events
2097
+ required:
2098
+ - job_ref
2099
+ - job_id
2100
+ - status
2101
+ properties:
2102
+ job_ref:
2103
+ type: string
2104
+ job_id:
2105
+ $ref: '#/components/schemas/UuidV7'
2106
+ status:
2107
+ type: string
2108
+ const: "failed"
2109
+
2110
+ SseWorkflowTerminalData:
2111
+ type: object
2112
+ description: |
2113
+ Payload for workflow terminal events
2114
+ (workflow.completed, workflow.failed, workflow.partially_failed)
2115
+ required:
2116
+ - workflow_id
2117
+ - status
2118
+ properties:
2119
+ workflow_id:
2120
+ $ref: '#/components/schemas/UuidV7'
2121
+ status:
2122
+ type: string
2123
+ enum:
2124
+ - completed
2125
+ - failed
2126
+ - partially_failed
2127
+
2128
+ # ============================================
2129
+ # OPERATIONS SCHEMA ENDPOINT RESPONSE
2130
+ # ============================================
2131
+
2132
+ OperationsSchemaResponse:
2133
+ type: object
2134
+ description: |
2135
+ Operations meta-schema. Describes all available operation types, their options,
2136
+ constraints, defaults, and MIME type applicability. Returned raw (no envelope)
2137
+ for CDN cacheability.
2138
+
2139
+ Each operation defines options with types, constraints, and conditional
2140
+ dependencies (via `depends_on`). Clients use this to build dynamic forms
2141
+ and validate options before submission.
2142
+ required:
2143
+ - schema_version
2144
+ - operations
2145
+ properties:
2146
+ schema_version:
2147
+ type: string
2148
+ description: Schema version for cache-busting
2149
+ example: "1.0.0"
2150
+ operations:
2151
+ type: object
2152
+ description: Map of operation type to its schema definition
2153
+ additionalProperties:
2154
+ $ref: '#/components/schemas/OperationSchemaDefinition'
2155
+
2156
+ OperationSchemaDefinition:
2157
+ type: object
2158
+ description: Schema for a single operation type
2159
+ required:
2160
+ - description
2161
+ - input_model
2162
+ - options
2163
+ properties:
2164
+ description:
2165
+ type: string
2166
+ description: Human-readable description of what the operation does
2167
+ default:
2168
+ type: boolean
2169
+ description: Whether this is the default operation when none specified
2170
+ input_model:
2171
+ $ref: '#/components/schemas/OperationInputModel'
2172
+ min_inputs:
2173
+ type: integer
2174
+ description: Minimum number of inputs (multi-input operations only)
2175
+ minimum: 2
2176
+ max_inputs:
2177
+ type: integer
2178
+ description: Maximum number of inputs (multi-input operations only)
2179
+ accepts_mixed_types:
2180
+ type: boolean
2181
+ description: Whether mixed MIME types are allowed (archive only)
2182
+ mime_groups:
2183
+ type: object
2184
+ description: |
2185
+ MIME-type-specific option schemas. When present, options are grouped
2186
+ by MIME category (image, video, audio, document). Each group lists
2187
+ the supported MIME types and group-specific options.
2188
+ additionalProperties:
2189
+ $ref: '#/components/schemas/MimeGroupSchema'
2190
+ options:
2191
+ type: object
2192
+ description: |
2193
+ Global options applicable regardless of MIME type, keyed by option name.
2194
+ For operations with mime_groups, these are the common options.
2195
+ additionalProperties:
2196
+ $ref: '#/components/schemas/OptionSchema'
2197
+ per_input_options:
2198
+ type: object
2199
+ description: |
2200
+ Options that can be overridden per-input for multi-input operations,
2201
+ keyed by option name. For merge: per-join-point transition overrides.
2202
+ additionalProperties:
2203
+ $ref: '#/components/schemas/OptionSchema'
2204
+
2205
+ MimeGroupSchema:
2206
+ type: object
2207
+ description: MIME-group-specific option schema
2208
+ required:
2209
+ - mimes
2210
+ - options
2211
+ properties:
2212
+ mimes:
2213
+ type: array
2214
+ description: List of MIME types in this group
2215
+ items:
2216
+ type: string
2217
+ example: ["image/jpeg", "image/png", "image/webp"]
2218
+ options:
2219
+ type: object
2220
+ description: Options specific to this MIME group, keyed by option name
2221
+ additionalProperties:
2222
+ $ref: '#/components/schemas/OptionSchema'
2223
+ per_input_options:
2224
+ type: object
2225
+ description: Per-input overrides for this MIME group, keyed by option name (multi-input only)
2226
+ additionalProperties:
2227
+ $ref: '#/components/schemas/OptionSchema'
2228
+
2229
+ OptionSchema:
2230
+ type: object
2231
+ description: Schema for a single operation option
2232
+ required:
2233
+ - type
2234
+ properties:
2235
+ type:
2236
+ type: string
2237
+ description: Option value type
2238
+ enum:
2239
+ - integer
2240
+ - float
2241
+ - boolean
2242
+ - enum
2243
+ - string
2244
+ description:
2245
+ type: string
2246
+ description: Human-readable description
2247
+ required:
2248
+ type: boolean
2249
+ description: Whether the option is required
2250
+ default:
2251
+ description: Default value if not specified
2252
+ values:
2253
+ type: array
2254
+ description: Allowed values (for enum type)
2255
+ items: {}
2256
+ value_type:
2257
+ type: string
2258
+ description: |
2259
+ Actual type of enum values when not strings (e.g. "integer" for numeric bitrate enums).
2260
+ Consumers should parse/display values as this type rather than as strings.
2261
+ enum:
2262
+ - integer
2263
+ - float
2264
+ min:
2265
+ type: number
2266
+ description: Minimum value (for integer/float types)
2267
+ max:
2268
+ type: number
2269
+ description: Maximum value (for integer/float types)
2270
+ depends_on:
2271
+ type: object
2272
+ description: |
2273
+ Conditional dependency. This option is only applicable when the condition is met.
2274
+ Simple: `{ "mode": "lossy" }` — option applies when mode equals lossy.
2275
+ Multi-value: `{ "output_format": ["jpeg", "webp"] }` — option applies when output_format is any listed value.
2276
+ Set condition: `{ "width": "set", "height": "set", "logic": "or" }` — option applies when width or height is provided.
2277
+ The "set" sentinel means the option has any value. "logic" can be "and" (default) or "or".
2278
+ additionalProperties: true
2279
+
2280
+ # ============================================
2281
+ # RETRY SCHEMAS
2282
+ # ============================================
2283
+
2284
+ RetryResponse:
2285
+ type: object
2286
+ required:
2287
+ - operation_id
2288
+ - original_operation_id
2289
+ - status
2290
+ properties:
2291
+ operation_id:
2292
+ $ref: '#/components/schemas/UuidV7'
2293
+ description: New operation ID for the retry
2294
+ original_operation_id:
2295
+ $ref: '#/components/schemas/UuidV7'
2296
+ description: ID of the original failed operation
2297
+ status:
2298
+ type: string
2299
+ enum:
2300
+ - pending
2301
+ description: Always "pending" for a new retry
2302
+
2303
+ RetrySuccessEnvelope:
2304
+ type: object
2305
+ required:
2306
+ - success
2307
+ - data
2308
+ properties:
2309
+ success:
2310
+ type: boolean
2311
+ const: true
2312
+ data:
2313
+ $ref: '#/components/schemas/RetryResponse'
2314
+
2315
+ # ============================================
2316
+ # CALLBACK & EXPORT CONFIG
2317
+ # ============================================
2318
+
2319
+ CallbackEventType:
2320
+ type: string
2321
+ description: |
2322
+ Events that can trigger a webhook callback:
2323
+ - workflow.completed: All jobs done successfully
2324
+ - workflow.failed: At least one job failed, none in progress
2325
+ - workflow.partially_failed: Some succeeded, some failed
2326
+ - operation.completed: Individual operation done (opt-in for granular progress)
2327
+ enum:
2328
+ - workflow.completed
2329
+ - workflow.failed
2330
+ - workflow.partially_failed
2331
+ - operation.completed
2332
+
2333
+ WebhookPayload:
2334
+ type: object
2335
+ description: |
2336
+ Payload POSTed to the `callback_url` when a subscribed event occurs.
2337
+ The `workflow` field contains the full current state including all jobs
2338
+ and their operation results, matching the `WorkflowStatusResponse` shape.
2339
+
2340
+ For `operation.completed` events, the `operation` field identifies which
2341
+ specific operation triggered the callback, so consumers do not need to
2342
+ scan the entire workflow to find the change.
2343
+ required:
2344
+ - event_type
2345
+ - delivery_id
2346
+ - timestamp
2347
+ - workflow
2348
+ properties:
2349
+ event_type:
2350
+ $ref: '#/components/schemas/CallbackEventType'
2351
+ delivery_id:
2352
+ $ref: '#/components/schemas/UuidV7'
2353
+ description: |
2354
+ Unique identifier for this event. Stable across retry attempts —
2355
+ the same delivery_id is sent if the API retries a failed delivery.
2356
+ Consumers should use this for idempotency to avoid processing
2357
+ the same event twice.
2358
+ timestamp:
2359
+ type: string
2360
+ format: date-time
2361
+ description: ISO 8601 timestamp of when the event occurred
2362
+ example: "2026-03-13T14:30:00Z"
2363
+ workflow:
2364
+ $ref: '#/components/schemas/WorkflowStatusResponse'
2365
+ operation:
2366
+ $ref: '#/components/schemas/WebhookOperationContext'
2367
+ allOf:
2368
+ - if:
2369
+ properties:
2370
+ event_type:
2371
+ const: operation.completed
2372
+ then:
2373
+ required: [operation]
2374
+ properties:
2375
+ operation:
2376
+ type: object
2377
+ description: operation.completed events must include operation context
2378
+ else:
2379
+ properties:
2380
+ operation:
2381
+ type: "null"
2382
+ description: Workflow-level events have null operation context
2383
+
2384
+ WebhookOperationContext:
2385
+ type:
2386
+ - object
2387
+ - "null"
2388
+ description: |
2389
+ Identifies which operation triggered the callback. Present only for
2390
+ `operation.completed` events; null for workflow-level events.
2391
+ required:
2392
+ - job_ref
2393
+ - operation_id
2394
+ properties:
2395
+ job_ref:
2396
+ type: string
2397
+ description: Reference label of the job containing the operation
2398
+ example: "main"
2399
+ operation_id:
2400
+ $ref: '#/components/schemas/UuidV7'
2401
+ description: ID of the operation that completed
2402
+
2403
+ ExportConfig:
2404
+ type:
2405
+ - object
2406
+ - "null"
2407
+ description: |
2408
+ Export configuration. When set, all operation outputs are copied to the
2409
+ customer's destination in addition to GISL's own S3 storage.
2410
+ Currently supports AWS S3 via cross-account AssumeRole.
2411
+ required:
2412
+ - service
2413
+ - bucket
2414
+ - role_arn
2415
+ properties:
2416
+ service:
2417
+ type: string
2418
+ description: Destination service
2419
+ enum:
2420
+ - s3
2421
+ example: "s3"
2422
+ bucket:
2423
+ type: string
2424
+ description: Destination bucket name
2425
+ example: "customer-output-bucket"
2426
+ key_prefix:
2427
+ type: string
2428
+ description: Key prefix for exported files
2429
+ example: "compressed/"
2430
+ role_arn:
2431
+ type: string
2432
+ description: |
2433
+ IAM role ARN in the customer's AWS account. GISL's Lambda assumes this
2434
+ role to write to the customer's bucket. The customer must configure a
2435
+ trust policy allowing GISL's execution role to assume it.
2436
+ pattern: '^arn:aws:iam::\d{12}:role/.+$'
2437
+ example: "arn:aws:iam::123456789012:role/giveitsmaller-write"
2438
+
2439
+ # ============================================
2440
+ # HEALTH SCHEMAS
2441
+ # ============================================
2442
+
2443
+ LivenessResponse:
2444
+ type: object
2445
+ required:
2446
+ - app
2447
+ properties:
2448
+ app:
2449
+ type: boolean
2450
+ description: Application is running
2451
+
2452
+ ReadinessResponse:
2453
+ type: object
2454
+ properties:
2455
+ database:
2456
+ type: boolean
2457
+ description: Database connection is healthy
2458
+ cache:
2459
+ type: boolean
2460
+ description: Cache connection is healthy
2461
+
2462
+ # ============================================
2463
+ # CONTACT SCHEMAS
2464
+ # ============================================
2465
+
2466
+ ContactSubject:
2467
+ type: string
2468
+ enum:
2469
+ - general_enquiry
2470
+ - bug_report
2471
+ - suggestion
2472
+ - complaint
2473
+ - business_enquiry
2474
+ description: |
2475
+ Subject category:
2476
+ - general_enquiry: General questions
2477
+ - bug_report: Report a bug or issue
2478
+ - suggestion: Feature suggestion or improvement idea
2479
+ - complaint: Complaint about the service
2480
+ - business_enquiry: Business or partnership enquiry
2481
+
2482
+ ContactRequest:
2483
+ type: object
2484
+ required:
2485
+ - email
2486
+ - subject
2487
+ - message
2488
+ properties:
2489
+ name:
2490
+ type: string
2491
+ maxLength: 100
2492
+ description: Sender's name (optional)
2493
+ example: "Jane Doe"
2494
+ email:
2495
+ type: string
2496
+ format: email
2497
+ maxLength: 254
2498
+ description: Sender's email address
2499
+ example: "jane@example.com"
2500
+ subject:
2501
+ $ref: '#/components/schemas/ContactSubject'
2502
+ message:
2503
+ type: string
2504
+ minLength: 1
2505
+ maxLength: 1000
2506
+ description: Message body
2507
+ example: "I have a question about supported file formats."
2508
+ website:
2509
+ type: string
2510
+ maxLength: 255
2511
+ description: |
2512
+ Honeypot field for bot detection. Hidden from real users via CSS.
2513
+ Legitimate submissions must omit this field or send an empty string.
2514
+ The API rejects any request where this field is non-empty.
2515
+
2516
+ ContactValidationErrorResponse:
2517
+ type: object
2518
+ required:
2519
+ - errors
2520
+ properties:
2521
+ errors:
2522
+ type: object
2523
+ description: |
2524
+ Map of field names to arrays of validation error messages.
2525
+ additionalProperties:
2526
+ type: array
2527
+ items:
2528
+ type: string
2529
+ example:
2530
+ email:
2531
+ - "This value is not a valid email address."
2532
+ subject:
2533
+ - "This value is not valid."