{
  "openapi": "3.1.0",
  "info": {
    "title": "Doqlo Bulk Fill Public API",
    "version": "1.0.0",
    "description": "Public export-job API for Doqlo Bulk Fill.",
    "contact": {
      "name": "Doqlo Support",
      "url": "https://doqlo.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://api.doqlo.com"
    }
  ],
  "externalDocs": {
    "description": "Doqlo API documentation",
    "url": "https://docs.doqlo.com/api"
  },
  "tags": [
    {
      "name": "Export Jobs",
      "description": "Create, poll, and download Bulk Fill export jobs."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/bulkfill/export-jobs": {
      "post": {
        "tags": [
          "Export Jobs"
        ],
        "summary": "Create a Bulk Fill export job",
        "operationId": "createBulkFillExportJob",
        "description": "Submit a source PDF, a reusable `.doqlo` configuration package exported from the web Bulk Fill editor, exactly one row-data input (`rows_json` or `csv_file`), and a required `max_failed_row_percent`. Optionally supply `filename_columns` to append up to 3 CSV-driven suffix segments to each produced row PDF name while keeping the existing `row_{rowNumber}` prefix. Create requests must include `Idempotency-Key` for logical replay and may also include optional `X-Request-Id` for tracing.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Caller-provided logical create key. After a request passes auth, security checks, and front-door rate limiting, the same `Idempotency-Key` plus the same effective request body returns the same logical job state for 24 hours. The same key plus a different effective request body returns `409 IDEMPOTENCY_KEY_PAYLOAD_MISMATCH`. The same key after 24 hours is treated as a new request. Exactly one case-sensitive value is accepted. Allowed characters: `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, `-`. Length: `1..128`. UUID v4 or another high-entropy random string is recommended.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9._:-]+$"
            }
          },
          {
            "name": "X-Request-Id",
            "in": "header",
            "required": false,
            "description": "Optional tracing and support correlation ID. This header is not used for idempotency. Repeated values are allowed and never trigger replay, payload mismatch, or logical create uniqueness. If omitted or invalid, Doqlo generates a request ID and returns it in the response header. The same validation envelope is accepted: exactly one case-sensitive value, `1..128` characters, pattern `^[A-Za-z0-9._:-]+$`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9._:-]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreateExportJobRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The job settled inside the sync wait window. In response mode this can include an inline completed result or a failed job envelope. In webhook mode the body can still return a completed or failed terminal job state, but webhook delivery remains the notification channel for downstream systems.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportJob"
                },
                "examples": {
                  "completed": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "completed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "result": {
                        "delivery_mode": "direct",
                        "download_url": "/v1/bulkfill/export-jobs/550e8400-e29b-41d4-a716-446655440000/download",
                        "expires_at": "2026-07-02T13:00:15Z",
                        "file_size_bytes": 1048576
                      }
                    }
                  },
                  "webhook_acknowledgement": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "completed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z"
                    }
                  },
                  "webhook_threshold_failed": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "FAILED_ROW_THRESHOLD_EXCEEDED",
                        "message": "The export job exceeded max_failed_row_percent and was aborted.",
                        "details": {
                          "failure_class": "threshold_failed",
                          "input_row_count": 3,
                          "produced_row_count": 0,
                          "failed_row_count": 1,
                          "failed_row_percent": 33.3333,
                          "max_failed_row_percent": 0
                        }
                      }
                    }
                  },
                  "webhook_processing_anomaly": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "PROCESSING_ANOMALY",
                        "message": "The export was stopped because an anomaly was detected during processing. Please retry. If this continues, contact support."
                      }
                    }
                  },
                  "webhook_signature_overlay_limit_exceeded": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "SIGNATURE_OVERLAY_LIMIT_EXCEEDED",
                        "message": "This export has too many image or signature overlays. Remove some signatures or images, then try again."
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "The job was accepted but is still queued or processing. Keep the `job_id` and poll or wait for the terminal webhook if you chose webhook mode.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportJob"
                }
              }
            }
          },
          "400": {
            "description": "Validation or request-shape failure, including missing or invalid `Idempotency-Key` (`INVALID_IDEMPOTENCY_KEY`), malformed or wrong-shape `filename_columns` (`INVALID_FILENAME_COLUMNS`), and `filename_columns` arrays longer than 3 (`TOO_MANY_FILENAME_COLUMNS`).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Doqlo Public API key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The authenticated account or key is not eligible for this request.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency payload mismatch for `Idempotency-Key` (`IDEMPOTENCY_KEY_PAYLOAD_MISMATCH`).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Uploaded input or generated output would exceed current limits.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The input document or batch request could not be accepted as valid work.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded, or new create requests are temporarily blocked after failure-heavy usage. These front-door protections run before idempotency replay lookup, so a retry can still return 429 before Doqlo evaluates a prior logical job.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              },
              "Retry-After": {
                "description": "Seconds until the next create attempt is expected to succeed. Present for fixed rate limits and temporary safeguard cooldowns.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/GenericRateLimitedErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/CreateCooldownErrorResponse"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server-side failure, or a response-mode job that settled failed inside the sync wait window.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestIdResponseHeader"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ExportJob"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "threshold_failed": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "FAILED_ROW_THRESHOLD_EXCEEDED",
                        "message": "The export job exceeded max_failed_row_percent and was aborted.",
                        "details": {
                          "failure_class": "threshold_failed",
                          "input_row_count": 3,
                          "produced_row_count": 0,
                          "failed_row_count": 1,
                          "failed_row_percent": 33.3333,
                          "max_failed_row_percent": 0
                        }
                      }
                    }
                  },
                  "processing_anomaly": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "PROCESSING_ANOMALY",
                        "message": "The export was stopped because an anomaly was detected during processing. Please retry. If this continues, contact support."
                      }
                    }
                  },
                  "signature_overlay_limit_exceeded": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "SIGNATURE_OVERLAY_LIMIT_EXCEEDED",
                        "message": "This export has too many image or signature overlays. Remove some signatures or images, then try again."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/bulkfill/export-jobs/{job_id}": {
      "get": {
        "tags": [
          "Export Jobs"
        ],
        "summary": "Read the current state of one export job.",
        "operationId": "getBulkFillExportJob",
        "description": "Use this route to poll a running job or re-read the final completed or failed state. Completed jobs expose a ZIP artifact whose `manifest.json` reports top-level diagnostics and per-row `success`, `partial`, or `failed` results. Failed jobs remain readable and may include threshold diagnostics when the runtime aborted after exceeding `max_failed_row_percent`. Processing-anomaly failures stay generic and intentionally omit public diagnostics.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "UUID for the accepted export job.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current job state for an owned export job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportJob"
                },
                "examples": {
                  "completed": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "completed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "started_at": "2026-07-02T12:00:02Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "result": {
                        "delivery_mode": "direct",
                        "download_url": "/v1/bulkfill/export-jobs/550e8400-e29b-41d4-a716-446655440000/download",
                        "expires_at": "2026-07-02T13:00:15Z",
                        "file_size_bytes": 1048576
                      }
                    }
                  },
                  "failed": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "FAILED_ROW_THRESHOLD_EXCEEDED",
                        "message": "The export job exceeded max_failed_row_percent and was aborted.",
                        "details": {
                          "failure_class": "threshold_failed",
                          "input_row_count": 3,
                          "produced_row_count": 0,
                          "failed_row_count": 1,
                          "failed_row_percent": 33.3333,
                          "max_failed_row_percent": 0
                        }
                      }
                    }
                  },
                  "processing_anomaly": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "PROCESSING_ANOMALY",
                        "message": "The export was stopped because an anomaly was detected during processing. Please retry. If this continues, contact support."
                      }
                    }
                  },
                  "signature_overlay_limit_exceeded": {
                    "value": {
                      "job_id": "550e8400-e29b-41d4-a716-446655440000",
                      "status": "failed",
                      "created_at": "2026-07-02T12:00:00Z",
                      "completed_at": "2026-07-02T12:00:15Z",
                      "error": {
                        "code": "SIGNATURE_OVERLAY_LIMIT_EXCEEDED",
                        "message": "This export has too many image or signature overlays. Remove some signatures or images, then try again."
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid `job_id` format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Doqlo Public API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The authenticated key is not active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Job not found or not owned by the authenticated account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server-side failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bulkfill/export-jobs/{job_id}/download": {
      "get": {
        "tags": [
          "Export Jobs"
        ],
        "summary": "Redeem the Doqlo-controlled download handle for a completed job.",
        "operationId": "downloadBulkFillExportJob",
        "description": "Use `-L` or equivalent redirect-following support in your client because Doqlo may stream the file directly or return a redirect, depending on the current delivery mode. The ZIP artifact contains one produced row PDF per successful or partial row plus `manifest.json` version `2` with row-level status, counts, warnings, and row-fatal errors. Threshold-failed jobs never expose this route as a deliverable artifact.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "UUID for the completed export job.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ZIP archive is streamed directly from Doqlo.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Doqlo resolved the authenticated download and redirected the client to the current provider handoff URL.",
            "headers": {
              "Location": {
                "description": "Resolved provider download URL for the authenticated delivery.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "Invalid `job_id` format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Doqlo Public API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The authenticated key is not active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No active delivery is currently available for that job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "The delivery exists but the current handoff failed. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer authentication using a Doqlo Public API key."
      }
    },
    "headers": {
      "XRequestIdResponseHeader": {
        "description": "Request tracing and support correlation ID for this HTTP response. If the client omitted or sent an invalid `X-Request-Id`, Doqlo generates one.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[A-Za-z0-9._:-]+$"
        }
      }
    },
    "schemas": {
      "CreateExportJobRequest": {
        "type": "object",
        "description": "Multipart form body for a Bulk Fill export-job create request. The request uploads a reusable `.doqlo` configuration package, declares an explicit failed-row threshold, and executes it best-effort against the submitted PDF. You may also provide `filename_columns` as one JSON string field containing up to 3 CSV column keys to append sanitized suffixes after the default `row_{rowNumber}` prefix in produced row PDF filenames. Empty row values or values sanitized to empty are skipped for that row, and rows with no usable filename segments fall back to row-only naming. Read [What a .doqlo project file is](https://docs.doqlo.com/bulk-fill/doqlo-project-files) for the basic package model. Read [Reuse .doqlo Project Files Across PDFs](https://docs.doqlo.com/bulk-fill/reuse-across-pdfs) for the product rationale and practical examples behind changed-PDF reuse.",
        "required": [
          "pdf",
          "doqlo_file",
          "max_failed_row_percent"
        ],
        "oneOf": [
          {
            "required": [
              "rows_json"
            ]
          },
          {
            "required": [
              "csv_file"
            ]
          }
        ],
        "properties": {
          "pdf": {
            "type": "string",
            "format": "binary",
            "description": "Source PDF file to execute against. It does not need to exactly match `.doqlo` source metadata; compatible PDF-context differences are reported in `manifest.json` when execution still succeeds. Read [Reuse .doqlo Project Files Across PDFs](https://docs.doqlo.com/bulk-fill/reuse-across-pdfs) for practical examples of this changed-PDF behavior."
          },
          "doqlo_file": {
            "type": "string",
            "format": "binary",
            "description": "Server-produced Bulk Fill `.doqlo` package exported from the web Bulk Fill editor. The public API executes this reusable configuration package; it does not accept field placements or mapping definitions directly. Read [What a .doqlo project file is](https://docs.doqlo.com/bulk-fill/doqlo-project-files) for the basic package model."
          },
          "max_failed_row_percent": {
            "type": "string",
            "pattern": "^0*(100|[1-9]?[0-9])$",
            "description": "Required integer percent from `0` to `100`. This is your explicit acceptance boundary for failed rows. Threshold evaluation is strict `failed_row_percent > max_failed_row_percent`. `0` aborts on the first failed row. `100` allows all requested rows to fail without threshold failure if the manifest/container still completes. It does not switch billing to pay-per-produced-file semantics."
          },
          "rows_json": {
            "type": "string",
            "description": "JSON string containing an array of row objects. Provide exactly one of `rows_json` or `csv_file`. Use object keys such as `column_0`, `column_1`, and `column_2` to match the positional column IDs from your Bulk Fill layout."
          },
          "csv_file": {
            "type": "string",
            "format": "binary",
            "description": "CSV upload. Provide exactly one of `rows_json` or `csv_file`. The public CSV contract is positional and does not auto-detect headers, so keep your mapped columns aligned to `column_0`, `column_1`, and later positions."
          },
          "filename_columns": {
            "type": "string",
            "description": "Optional JSON string array of up to 3 CSV column keys to include in each exported filename. Use positional keys such as `column_0`, `column_1`, and `column_2`. Send exactly one multipart field when present. Example: [`\"column_0\"`,`\"column_1\"`]. Invalid JSON, wrong shapes, repeated multipart fields, or non-string entries return `400 INVALID_FILENAME_COLUMNS`. More than 3 entries returns `400 TOO_MANY_FILENAME_COLUMNS`. Exported filenames always keep the row number, and unusable values are skipped automatically."
          },
          "include_stickers": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "description": "Optional boolean string. Current values are `true` or `false`. Omit to use `false`. Set this to `true` when you want exported PDFs to keep visual stickers or markers, including sign-related visual markers where applicable. Leave it `false` when you want only the generated document content without those visual markers."
          },
          "flatten_forms": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "description": "Optional boolean string. Current values are `true` or `false`. Omit to use `false`. Some PDFs contain interactive form fields, and generated overlays or content can otherwise appear behind those form layers. Set this to `true` to flatten those fields into static content so they do not cover the generated output."
          },
          "delivery_mode": {
            "type": "string",
            "enum": [
              "response",
              "webhook"
            ],
            "description": "Optional delivery mode. Omit to use `response`. Use `response` when you want the create call to return a terminal result inline when it settles quickly. Use `webhook` when you want terminal completion or failure sent to your HTTPS receiver. This controls how terminal job results are delivered, not how threshold or billing semantics work."
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Required when `delivery_mode=webhook`. Must be a public HTTPS receiver that can accept Doqlo terminal event delivery. The URL must use HTTPS, must not include userinfo, must not target localhost or private network addresses, and must not put credentials or signatures in query parameters."
          },
          "webhook_secret": {
            "type": "string",
            "description": "Required when `delivery_mode=webhook`. Used to sign webhook deliveries so your receiver can verify authenticity. Omit it when `delivery_mode=response`."
          }
        }
      },
      "ExportJob": {
        "type": "object",
        "description": "Current state of one public export job. Completed jobs can still contain `partial` or `failed` rows; inspect `manifest.json` in the ZIP artifact for row-level details.",
        "required": [
          "job_id",
          "status",
          "created_at"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "description": "Accepted export-job identifier."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "completed",
              "failed"
            ],
            "description": "Current export-job status."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When Doqlo created the job."
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When processing started, when available."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the job reached a terminal state, when available."
          },
          "result": {
            "$ref": "#/components/schemas/ExportJobResult",
            "description": "Present only when the job completed and a current delivery record is available."
          },
          "error": {
            "$ref": "#/components/schemas/ExportJobError",
            "description": "Present only when the job reached a terminal failed state."
          }
        }
      },
      "ExportJobResult": {
        "type": "object",
        "description": "Result payload returned for completed jobs. Use the ZIP artifact and `manifest.json` to understand produced, partial, failed, and skipped outcomes.",
        "properties": {
          "delivery_mode": {
            "type": "string",
            "enum": [
              "direct",
              "cloud"
            ],
            "description": "Current delivery handoff mode."
          },
          "download_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Doqlo-controlled download handle for the completed job."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Current delivery expiry timestamp."
          },
          "file_size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Current ZIP size in bytes."
          }
        }
      },
      "ExportJobError": {
        "type": "object",
        "description": "Terminal error payload returned for failed jobs. `details` is present only for threshold failures.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Terminal error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "details": {
            "$ref": "#/components/schemas/ThresholdFailedJobErrorDetails",
            "description": "Threshold-failure diagnostics exposed when the job aborted after exceeding `max_failed_row_percent`. Omitted for other failure classes such as `PROCESSING_ANOMALY` and `SIGNATURE_OVERLAY_LIMIT_EXCEEDED`."
          }
        }
      },
      "ThresholdFailedJobErrorDetails": {
        "type": "object",
        "description": "Present when the job failed because failed rows exceeded the declared runtime threshold.",
        "required": [
          "failure_class",
          "input_row_count",
          "produced_row_count",
          "failed_row_count",
          "failed_row_percent",
          "max_failed_row_percent"
        ],
        "properties": {
          "failure_class": {
            "type": "string",
            "enum": [
              "threshold_failed"
            ],
            "description": "Structured failure class for jobs that aborted because the declared failed-row threshold was exceeded."
          },
          "input_row_count": {
            "type": "integer",
            "description": "Total requested input rows in the job."
          },
          "produced_row_count": {
            "type": "integer",
            "description": "Row PDFs produced before the job aborted."
          },
          "failed_row_count": {
            "type": "integer",
            "description": "Rows that failed to produce an output PDF before or at the abort point."
          },
          "failed_row_percent": {
            "type": "number",
            "description": "Observed failed-row percentage at the moment the threshold was exceeded."
          },
          "max_failed_row_percent": {
            "type": "integer",
            "description": "The customer-declared failed-row threshold for this job."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Standard Public API error envelope.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        }
      },
      "GenericRateLimitedErrorResponse": {
        "type": "object",
        "description": "Generic fixed-rate-limit denial envelope.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "RATE_LIMITED"
            ],
            "description": "Stable fixed-rate-limit error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        }
      },
      "CreateCooldownErrorResponse": {
        "type": "object",
        "description": "Create-only cooldown denial envelope returned after failure-heavy usage. Applies only to new create requests and does not affect status polling, downloads, or webhook delivery.",
        "required": [
          "code",
          "message",
          "retry_after_seconds"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "REPEATED_FAILED_JOBS_COOLDOWN",
              "HIGH_FAIL_RATIO_COOLDOWN"
            ],
            "description": "Stable create-only cooldown error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          },
          "retry_after_seconds": {
            "type": "integer",
            "minimum": 1,
            "description": "Seconds until the next create attempt is expected to succeed if no new blocking conditions are introduced."
          }
        }
      }
    }
  }
}
