openapi: 3.2.0 $self: https://remora.invalid/spec/api/openapi.yaml jsonSchemaDialect: https://spec.openapis.org/oas/3.2/dialect/2025-09-17 info: title: Remora API version: 1.0.0 description: Public job management and service-authenticated worker operations. servers: - url: /api security: - oidc: [] paths: /v1/jobs: post: operationId: submitJob parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/JobSubmission' responses: '202': description: Accepted, or the original accepted response for the same key and fingerprint. headers: Idempotency-Expires-At: $ref: '#/components/headers/IdempotencyExpiresAt' content: application/json: schema: $ref: '#/components/schemas/Job' '409': $ref: '#/components/responses/IdempotencyConflict' default: $ref: '#/components/responses/Error' get: operationId: listJobs parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - name: include_deleted in: query schema: type: boolean default: false responses: '200': description: Authorized keyset page bound to one snapshot and filter. content: application/json: schema: $ref: '#/components/schemas/JobPage' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}: parameters: - $ref: '#/components/parameters/JobId' get: operationId: getJob responses: '200': description: Current authorized job projection. content: application/json: schema: $ref: '#/components/schemas/Job' '404': $ref: '#/components/responses/NotFound' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}/result: parameters: - $ref: '#/components/parameters/JobId' get: operationId: getJobResult responses: '200': description: Visible committed manifest-rooted result. content: application/json: schema: $ref: '#/components/schemas/ResultDescriptor' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/PreconditionFailed' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}/cancel: parameters: - $ref: '#/components/parameters/JobId' post: operationId: cancelJob parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '202': description: Cancellation intent accepted or prior response replayed. content: application/json: schema: $ref: '#/components/schemas/Job' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}/soft-delete: parameters: - $ref: '#/components/parameters/JobId' post: operationId: softDeleteJob parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: Job hidden, or prior response replayed. content: application/json: schema: $ref: '#/components/schemas/Job' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}/restore: parameters: - $ref: '#/components/parameters/JobId' post: operationId: restoreJob parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: Soft-deleted job restored, or prior response replayed. content: application/json: schema: $ref: '#/components/schemas/Job' default: $ref: '#/components/responses/Error' /v1/jobs/{job_id}/hard-delete: parameters: - $ref: '#/components/parameters/JobId' post: operationId: hardDeleteJob parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '202': description: Durable deletion operation created or replayed. content: application/json: schema: $ref: '#/components/schemas/DeletionOperation' default: $ref: '#/components/responses/Error' /v1/deletions/{operation_id}: parameters: - name: operation_id in: path required: true schema: $ref: '#/components/schemas/OpaqueId' get: operationId: getDeletionOperation responses: '200': description: Current deletion operation. content: application/json: schema: $ref: '#/components/schemas/DeletionOperation' '404': $ref: '#/components/responses/NotFound' default: $ref: '#/components/responses/Error' /v1/events: get: operationId: connectEvents parameters: - name: cursor in: query description: Opaque authorization-bound replay cursor from an HTTP snapshot or prior frame. schema: type: string minLength: 16 maxLength: 2048 responses: '101': description: WebSocket frames conform to events.schema.json. An absent, invalid, expired, or unauthorized cursor yields resync_required. default: $ref: '#/components/responses/Error' /internal/v1/jobs/{job_id}/claim: parameters: - $ref: '#/components/parameters/JobId' post: operationId: claimJob security: - serviceAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ClaimRequest' responses: '200': description: Allocated authority or replay of the same claim operation. content: application/json: schema: $ref: '#/components/schemas/AttemptAuthority' default: $ref: '#/components/responses/Error' /internal/v1/attempts/{attempt_id}/renew: parameters: - $ref: '#/components/parameters/AttemptId' post: operationId: renewAttempt security: - serviceAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttemptMutation' responses: '204': description: Renewed or duplicate operation accepted. '409': $ref: '#/components/responses/PreconditionFailed' default: $ref: '#/components/responses/Error' /internal/v1/attempts/{attempt_id}/progress: parameters: - $ref: '#/components/parameters/AttemptId' post: operationId: reportAttemptProgress security: - serviceAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttemptProgressRequest' responses: '204': description: Current progress accepted or duplicate ignored. '409': $ref: '#/components/responses/PreconditionFailed' default: $ref: '#/components/responses/Error' /internal/v1/attempts/{attempt_id}/outcome: parameters: - $ref: '#/components/parameters/AttemptId' post: operationId: reportAttemptOutcome security: - serviceAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttemptOutcomeRequest' responses: '200': description: Durable outcome projection or replay of the same operation. content: application/json: schema: $ref: '#/components/schemas/Job' '409': $ref: '#/components/responses/PreconditionFailed' default: $ref: '#/components/responses/Error' components: securitySchemes: oidc: type: openIdConnect openIdConnectUrl: https://identity.invalid/.well-known/openid-configuration serviceAuth: type: mutualTLS parameters: IdempotencyKey: name: Idempotency-Key in: header required: true description: Subject- and operation-scoped key retained for at least 24 hours. schema: type: string pattern: '^[A-Za-z0-9._~-]{16,128}$' Cursor: name: cursor in: query schema: type: string minLength: 16 maxLength: 2048 Limit: name: limit in: query schema: type: integer minimum: 1 maximum: 200 default: 50 JobId: name: job_id in: path required: true schema: $ref: '#/components/schemas/OpaqueId' AttemptId: name: attempt_id in: path required: true schema: $ref: '#/components/schemas/OpaqueId' headers: IdempotencyExpiresAt: description: Earliest time at which the server may forget this key. schema: type: string format: date-time responses: Error: description: Structured error with user-safe detail only. content: application/problem+json: schema: $ref: '#/components/schemas/ApiError' NotFound: description: Missing or unauthorized resource; the response does not distinguish them. content: application/problem+json: schema: $ref: '#/components/schemas/ApiError' IdempotencyConflict: description: The key exists with a different canonical request fingerprint. content: application/problem+json: schema: $ref: '#/components/schemas/ApiError' PreconditionFailed: description: Durable authority, fence, phase, or lifecycle precondition failed. content: application/problem+json: schema: $ref: '#/components/schemas/ApiError' schemas: OpaqueId: type: string pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$' Target: type: object additionalProperties: false required: [store, bucket, prefix] properties: store: type: string minLength: 1 maxLength: 128 bucket: type: string pattern: '^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$' prefix: type: string maxLength: 1024 HttpSource: type: object additionalProperties: false required: [kind, url] properties: kind: const: http url: type: string format: uri maxLength: 8192 credential_ref: type: string maxLength: 512 ObjectStoreSource: type: object additionalProperties: false required: [kind, store, bucket, key] properties: kind: const: object_store store: type: string minLength: 1 maxLength: 128 bucket: type: string minLength: 3 maxLength: 63 key: type: string minLength: 1 maxLength: 4096 version_id: type: string maxLength: 1024 credential_ref: type: string maxLength: 512 HuggingFaceSource: type: object additionalProperties: false required: [kind, repository_type, repository_id, revision] properties: kind: const: hugging_face repository_type: type: string enum: [model, dataset, space] repository_id: type: string pattern: '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$' revision: type: string minLength: 1 maxLength: 256 include: type: array maxItems: 1024 uniqueItems: true items: type: string minLength: 1 maxLength: 1024 credential_ref: type: string maxLength: 512 Source: oneOf: - $ref: '#/components/schemas/HttpSource' - $ref: '#/components/schemas/ObjectStoreSource' - $ref: '#/components/schemas/HuggingFaceSource' discriminator: propertyName: kind mapping: http: '#/components/schemas/HttpSource' object_store: '#/components/schemas/ObjectStoreSource' hugging_face: '#/components/schemas/HuggingFaceSource' JobSubmission: type: object additionalProperties: false required: [source, target] properties: source: $ref: '#/components/schemas/Source' target: $ref: '#/components/schemas/Target' label: type: string minLength: 1 maxLength: 200 JobPhase: type: string enum: [Queued, Running, RetryWait, Cancelling, Succeeded, Failed, Cancelled] JobLifecycle: type: string enum: [visible, soft_deleted, hard_deleting, hard_deleted] AttemptAuthority: type: object additionalProperties: false required: [attempt_id, fence, worker_incarnation] properties: attempt_id: $ref: '#/components/schemas/OpaqueId' fence: type: integer minimum: 1 worker_incarnation: $ref: '#/components/schemas/OpaqueId' Progress: type: object additionalProperties: false required: [attempt_id, fence, completed_bytes, completed_members, observed_at] properties: attempt_id: $ref: '#/components/schemas/OpaqueId' fence: type: integer minimum: 1 completed_bytes: type: integer minimum: 0 total_bytes: type: [integer, 'null'] minimum: 0 completed_members: type: integer minimum: 0 total_members: type: [integer, 'null'] minimum: 0 observed_at: type: string format: date-time CheckpointProjection: type: object additionalProperties: false required: [reusable, validated_bytes, validated_members] properties: reusable: type: boolean validated_bytes: type: integer minimum: 0 validated_members: type: integer minimum: 0 JobFailure: oneOf: - type: object additionalProperties: false required: [class, retryable, message] properties: class: type: string enum: [validation, authorization, source_not_found, source_changed, cancelled] retryable: const: false message: type: string maxLength: 500 safe_details: type: object additionalProperties: type: [string, integer, boolean, 'null'] - type: object additionalProperties: false required: [class, retryable, message] properties: class: type: string enum: [transient_source, transient_target, integrity, capacity, internal, lost_attempt] retryable: const: true message: type: string maxLength: 500 safe_details: type: object additionalProperties: type: [string, integer, boolean, 'null'] AttemptRuntimeFailure: type: object additionalProperties: false required: [class, message] properties: class: type: string enum: [authorization, source_not_found, source_changed, transient_source, transient_target, integrity, capacity, internal] message: type: string maxLength: 500 safe_details: type: object additionalProperties: type: [string, integer, boolean, 'null'] ResultDescriptor: type: object additionalProperties: false required: [kind, manifest, member_count, total_bytes] properties: kind: const: manifest manifest: $ref: '#/components/schemas/ObjectIdentity' member_count: type: integer minimum: 1 total_bytes: type: integer minimum: 0 ObjectIdentity: type: object additionalProperties: false required: [store, bucket, key] properties: store: type: string bucket: type: string key: type: string version_id: type: string checksum: type: string Job: type: object additionalProperties: false required: [id, owner, revision, phase, lifecycle, source, target, authoritative_attempt, progress, checkpoint, result, failure, created_at, updated_at, event_cursor] properties: id: $ref: '#/components/schemas/OpaqueId' owner: $ref: '#/components/schemas/OpaqueId' revision: type: integer minimum: 1 phase: $ref: '#/components/schemas/JobPhase' lifecycle: $ref: '#/components/schemas/JobLifecycle' source: $ref: '#/components/schemas/Source' target: $ref: '#/components/schemas/Target' authoritative_attempt: oneOf: - $ref: '#/components/schemas/AttemptAuthority' - type: 'null' progress: oneOf: - $ref: '#/components/schemas/Progress' - type: 'null' checkpoint: oneOf: - $ref: '#/components/schemas/CheckpointProjection' - type: 'null' result: oneOf: - $ref: '#/components/schemas/ResultDescriptor' - type: 'null' failure: oneOf: - $ref: '#/components/schemas/JobFailure' - type: 'null' created_at: type: string format: date-time updated_at: type: string format: date-time event_cursor: type: string minLength: 16 JobPage: type: object additionalProperties: false required: [items, next_cursor, event_cursor] properties: items: type: array items: $ref: '#/components/schemas/Job' next_cursor: type: [string, 'null'] event_cursor: type: string minLength: 16 DeletionOperation: type: object additionalProperties: false required: [id, job_id, state, created_at, updated_at] properties: id: $ref: '#/components/schemas/OpaqueId' job_id: $ref: '#/components/schemas/OpaqueId' state: type: string enum: [queued, running, retry_wait, completed] created_at: type: string format: date-time updated_at: type: string format: date-time ApiError: type: object additionalProperties: false required: [code, message, request_id] properties: code: type: string enum: [invalid_request, unauthenticated, forbidden, not_found, idempotency_conflict, precondition_failed, cursor_invalid, cursor_expired, rate_limited, unavailable, internal] message: type: string maxLength: 500 request_id: $ref: '#/components/schemas/OpaqueId' fields: type: array maxItems: 100 items: type: object additionalProperties: false required: [pointer, code] properties: pointer: type: string maxLength: 500 code: type: string maxLength: 100 ClaimRequest: type: object additionalProperties: false required: [operation_id, worker_incarnation] properties: operation_id: $ref: '#/components/schemas/OpaqueId' worker_incarnation: $ref: '#/components/schemas/OpaqueId' AttemptMutation: type: object required: [operation_id, fence, worker_incarnation] properties: operation_id: $ref: '#/components/schemas/OpaqueId' fence: type: integer minimum: 1 worker_incarnation: $ref: '#/components/schemas/OpaqueId' AttemptProgressRequest: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/AttemptMutation' - type: object required: [progress] properties: progress: $ref: '#/components/schemas/Progress' AttemptOutcomeRequest: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/AttemptMutation' - type: object required: [outcome] properties: outcome: oneOf: - type: object additionalProperties: false required: [kind, result] properties: kind: const: success result: $ref: '#/components/schemas/ResultDescriptor' - type: object additionalProperties: false required: [kind, failure] properties: kind: const: failure failure: $ref: '#/components/schemas/AttemptRuntimeFailure'