{
  "openapi": "3.1.0",
  "info": {
    "title": "Tools For Free",
    "version": "1",
    "summary": "391 conversions and 13 tools across 38 formats.",
    "description": "No key, no account, no client library. Post the bytes, poll the job, take the file.\n\nFiles are capped at 536870912 bytes, clients at 200 jobs an hour,\nand every engine runs one job at a time. Nothing is stored after a job settles.",
    "license": {
      "name": "Free to use",
      "url": "https://toolsforfree.app/api"
    }
  },
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "servers": [
    {
      "url": "https://toolsforfree.app"
    }
  ],
  "x-tff-container-codecs": {
    "mp4": {
      "video": [
        "h264",
        "h265",
        "av1"
      ],
      "audio": [
        "aac",
        "copy",
        "none"
      ]
    },
    "mov": {
      "video": [
        "h264",
        "h265"
      ],
      "audio": [
        "aac",
        "copy",
        "none"
      ]
    },
    "mkv": {
      "video": [
        "h264",
        "h265",
        "vp9",
        "av1"
      ],
      "audio": [
        "aac",
        "mp3",
        "opus",
        "flac",
        "copy",
        "none"
      ]
    },
    "avi": {
      "video": [
        "h264"
      ],
      "audio": [
        "mp3",
        "copy",
        "none"
      ]
    },
    "ts": {
      "video": [
        "h264",
        "h265"
      ],
      "audio": [
        "aac",
        "copy",
        "none"
      ]
    },
    "webm": {
      "video": [
        "vp9",
        "av1"
      ],
      "audio": [
        "opus",
        "copy",
        "none"
      ]
    }
  },
  "paths": {
    "/api/limits": {
      "get": {
        "summary": "What this deployment will accept.",
        "description": "The per-file cap and the per-client rate limits, as the running container has them rather than as the documentation remembers them. Worth reading before sending something large: the relay refuses an oversized upload from its Content-Length, which is faster than sending it but slower than not trying.",
        "operationId": "getLimits",
        "responses": {
          "200": {
            "description": "The limits in force.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "maxUploadBytes",
                    "jobsPerHour",
                    "bytesPerDay",
                    "gifMaxSeconds"
                  ],
                  "properties": {
                    "maxUploadBytes": {
                      "type": "integer"
                    },
                    "jobsPerHour": {
                      "type": "integer"
                    },
                    "bytesPerDay": {
                      "type": "integer"
                    },
                    "gifMaxSeconds": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs": {
      "post": {
        "summary": "Start a conversion.",
        "description": "The file is the raw request body, not multipart. Content-Length is required, because the quota is checked against the declared size before a byte is read. Returns 202 with a task id and the engine holding it.\n\nStart an operation that keeps the format.\nCompressing an MP4 produces an MP4, which is not a conversion and has no entry in the pair matrix. Otherwise identical to the call above.",
        "operationId": "createJob",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Source format. Required unless `operation` is given.",
            "schema": {
              "type": "string",
              "enum": [
                "mp4",
                "webm",
                "mkv",
                "mov",
                "avi",
                "wmv",
                "flv",
                "mpeg",
                "ts",
                "3gp",
                "ogv",
                "mp3",
                "aac",
                "m4a",
                "wav",
                "flac",
                "ogg",
                "opus",
                "wma",
                "ac3",
                "mka",
                "caf",
                "aiff",
                "jpg",
                "png",
                "webp",
                "avif",
                "gif",
                "tiff",
                "heic",
                "svg",
                "zip",
                "7z",
                "tar",
                "tgz",
                "tbz2",
                "txz",
                "pdf"
              ]
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Target format. Required unless `operation` is given.",
            "schema": {
              "type": "string",
              "enum": [
                "mp4",
                "webm",
                "mkv",
                "mov",
                "avi",
                "ts",
                "mp3",
                "aac",
                "m4a",
                "wav",
                "flac",
                "ogg",
                "opus",
                "ac3",
                "mka",
                "caf",
                "aiff",
                "jpg",
                "png",
                "webp",
                "avif",
                "gif",
                "tiff",
                "zip",
                "7z",
                "tar",
                "tgz",
                "tbz2",
                "txz",
                "pdf"
              ]
            }
          },
          {
            "name": "operation",
            "in": "query",
            "description": "An operation that keeps the format, instead of `from` and `to`.",
            "schema": {
              "type": "string",
              "enum": [
                "compress-video",
                "resize-video",
                "trim-video",
                "rotate-video",
                "mute-video",
                "compress-audio",
                "compress-image",
                "resize-image",
                "rotate-image",
                "crop-image",
                "compress-pdf",
                "split-pdf",
                "rotate-pdf"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "The format being operated on. Required with `operation`.",
            "schema": {
              "type": "string",
              "enum": [
                "mp4",
                "webm",
                "mkv",
                "mov",
                "avi",
                "wmv",
                "flv",
                "mpeg",
                "ts",
                "3gp",
                "ogv",
                "mp3",
                "aac",
                "m4a",
                "wav",
                "flac",
                "ogg",
                "opus",
                "wma",
                "ac3",
                "mka",
                "caf",
                "aiff",
                "jpg",
                "png",
                "webp",
                "avif",
                "gif",
                "tiff",
                "heic",
                "svg",
                "zip",
                "7z",
                "tar",
                "tgz",
                "tbz2",
                "txz",
                "pdf"
              ]
            }
          },
          {
            "name": "x-tff-options",
            "in": "header",
            "description": "Conversion settings as JSON. Unrecognised keys are rejected rather than ignored.",
            "schema": {
              "type": "string",
              "contentMediaType": "application/json",
              "contentSchema": {
                "$ref": "#/components/schemas/ConversionOptions"
              }
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The file itself, as the raw body. Not multipart: parsing multipart would buffer the whole upload before anyone had decided to accept it.",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the job with the returned id and engine.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "taskId",
                    "engine"
                  ],
                  "properties": {
                    "taskId": {
                      "type": "string"
                    },
                    "engine": {
                      "type": "string",
                      "enum": [
                        "media",
                        "image",
                        "doc",
                        "archive"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing formats, unparseable options, or a failed upload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No engine implements this conversion or operation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "411": {
            "description": "Content-Length is required: the quota is checked before reading.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Larger than the per-file cap, which the body states.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "maxBytes"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "file-too-large"
                      ]
                    },
                    "maxBytes": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Over the hourly job count or the daily byte allowance. The wait is in `Retry-After` and in the body.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "too-many-jobs",
                        "daily-bytes"
                      ]
                    },
                    "retryAfterSeconds": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Every engine of that kind is busy, or none is reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs/{id}": {
      "get": {
        "summary": "Poll a job.",
        "description": "Returns state, progress and, once failed, a classified reason. The engine comes from the create response; asking all four on every poll would be a fan-out.",
        "operationId": "getJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The task id returned when the job was created.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{22}$"
            }
          },
          {
            "name": "engine",
            "in": "query",
            "required": true,
            "description": "The engine named in the create response. Polling all of them would be a fan-out.",
            "schema": {
              "type": "string",
              "enum": [
                "media",
                "image",
                "doc",
                "archive"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job as it stands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "state"
                  ],
                  "properties": {
                    "state": {
                      "type": "string",
                      "enum": [
                        "reserved",
                        "receiving",
                        "running",
                        "done",
                        "failed",
                        "cancelled"
                      ]
                    },
                    "progress": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "outputBytes": {
                      "type": "integer"
                    },
                    "failure": {
                      "type": "string",
                      "description": "Classified rather than quoted: an engine message can name a path.",
                      "enum": [
                        "decode-failed",
                        "encode-failed",
                        "timed-out",
                        "unsupported-input",
                        "cancelled",
                        "internal"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed id, or an engine that does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such job. Finished jobs are forgotten shortly after.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "That engine is not reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Cancel a job and free the engine.",
        "description": "Always 204. Cancelling a job that has already finished is not an error.",
        "operationId": "cancelJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The task id returned when the job was created.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{22}$"
            }
          },
          {
            "name": "engine",
            "in": "query",
            "required": true,
            "description": "The engine named in the create response. Polling all of them would be a fan-out.",
            "schema": {
              "type": "string",
              "enum": [
                "media",
                "image",
                "doc",
                "archive"
              ]
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled, or already finished. Either way the slot is free."
          },
          "400": {
            "description": "Malformed id, or an engine that does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs/{id}/output": {
      "get": {
        "summary": "Download the result.",
        "description": "409 until the job is done. The task id is 128 bits of randomness and there are no accounts, so holding the id is the whole authorisation story.",
        "operationId": "getJobOutput",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The task id returned when the job was created.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{22}$"
            }
          },
          {
            "name": "engine",
            "in": "query",
            "required": true,
            "description": "The engine named in the create response. Polling all of them would be a fan-out.",
            "schema": {
              "type": "string",
              "enum": [
                "media",
                "image",
                "doc",
                "archive"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "The target format, used to name the download and set its media type.",
            "schema": {
              "type": "string",
              "enum": [
                "mp4",
                "webm",
                "mkv",
                "mov",
                "avi",
                "ts",
                "mp3",
                "aac",
                "m4a",
                "wav",
                "flac",
                "ogg",
                "opus",
                "ac3",
                "mka",
                "caf",
                "aiff",
                "jpg",
                "png",
                "webp",
                "avif",
                "gif",
                "tiff",
                "zip",
                "7z",
                "tar",
                "tgz",
                "tbz2",
                "txz",
                "pdf"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The converted file.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed id, or an engine that does not exist."
          },
          "409": {
            "description": "The job has not finished."
          },
          "503": {
            "description": "That engine is not reachable."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ConversionOptions": {
        "type": "object",
        "properties": {
          "video": {
            "type": "object",
            "properties": {
              "codec": {
                "type": "string",
                "enum": [
                  "h264",
                  "h265",
                  "vp9",
                  "av1"
                ]
              },
              "mode": {
                "type": "string",
                "enum": [
                  "quality",
                  "bitrate",
                  "size"
                ]
              },
              "crf": {
                "type": "integer",
                "minimum": 0,
                "maximum": 63
              },
              "bitrateKbps": {
                "type": "integer",
                "minimum": 16,
                "maximum": 200000
              },
              "targetBytes": {
                "type": "integer",
                "minimum": 100000,
                "maximum": 10000000000
              },
              "effort": {
                "default": "balanced",
                "type": "string",
                "enum": [
                  "fastest",
                  "fast",
                  "balanced",
                  "slow",
                  "slowest"
                ]
              },
              "pixelFormat": {
                "default": "yuv420p",
                "type": "string",
                "enum": [
                  "yuv420p",
                  "yuv420p10le"
                ]
              }
            },
            "required": [
              "codec",
              "mode"
            ],
            "additionalProperties": false
          },
          "resize": {
            "type": "object",
            "properties": {
              "width": {
                "type": "integer",
                "minimum": 16,
                "maximum": 7680,
                "multipleOf": 2
              },
              "height": {
                "type": "integer",
                "minimum": 16,
                "maximum": 4320,
                "multipleOf": 2
              },
              "fit": {
                "default": "contain",
                "type": "string",
                "enum": [
                  "contain",
                  "cover",
                  "exact"
                ]
              },
              "fps": {
                "type": "number",
                "minimum": 1,
                "maximum": 240
              },
              "rotate": {
                "default": 0,
                "anyOf": [
                  {
                    "type": "number",
                    "const": 0
                  },
                  {
                    "type": "number",
                    "const": 90
                  },
                  {
                    "type": "number",
                    "const": 180
                  },
                  {
                    "type": "number",
                    "const": 270
                  }
                ]
              },
              "flip": {
                "default": "none",
                "type": "string",
                "enum": [
                  "none",
                  "horizontal",
                  "vertical"
                ]
              }
            },
            "additionalProperties": false
          },
          "trim": {
            "type": "object",
            "properties": {
              "startMs": {
                "type": "integer",
                "minimum": 0,
                "maximum": 86400000
              },
              "endMs": {
                "type": "integer",
                "minimum": 0,
                "maximum": 86400000
              }
            },
            "additionalProperties": false
          },
          "audio": {
            "type": "object",
            "properties": {
              "codec": {
                "type": "string",
                "enum": [
                  "aac",
                  "mp3",
                  "opus",
                  "flac",
                  "copy",
                  "none"
                ]
              },
              "bitrateKbps": {
                "type": "integer",
                "minimum": 8,
                "maximum": 2048
              },
              "sampleRate": {
                "anyOf": [
                  {
                    "type": "number",
                    "const": 8000
                  },
                  {
                    "type": "number",
                    "const": 16000
                  },
                  {
                    "type": "number",
                    "const": 22050
                  },
                  {
                    "type": "number",
                    "const": 24000
                  },
                  {
                    "type": "number",
                    "const": 32000
                  },
                  {
                    "type": "number",
                    "const": 44100
                  },
                  {
                    "type": "number",
                    "const": 48000
                  },
                  {
                    "type": "number",
                    "const": 96000
                  }
                ]
              },
              "channels": {
                "anyOf": [
                  {
                    "type": "number",
                    "const": 1
                  },
                  {
                    "type": "number",
                    "const": 2
                  }
                ]
              }
            },
            "required": [
              "codec"
            ],
            "additionalProperties": false
          },
          "image": {
            "type": "object",
            "properties": {
              "quality": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100
              },
              "lossless": {
                "type": "boolean"
              },
              "effort": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9
              },
              "width": {
                "type": "integer",
                "minimum": 1,
                "maximum": 20000
              },
              "height": {
                "type": "integer",
                "minimum": 1,
                "maximum": 20000
              },
              "fit": {
                "default": "contain",
                "type": "string",
                "enum": [
                  "contain",
                  "cover",
                  "exact"
                ]
              },
              "allowEnlarge": {
                "default": false,
                "type": "boolean"
              },
              "stripMetadata": {
                "default": true,
                "type": "boolean"
              },
              "rotate": {
                "default": 0,
                "anyOf": [
                  {
                    "type": "number",
                    "const": 0
                  },
                  {
                    "type": "number",
                    "const": 90
                  },
                  {
                    "type": "number",
                    "const": 180
                  },
                  {
                    "type": "number",
                    "const": 270
                  }
                ]
              },
              "flip": {
                "default": "none",
                "type": "string",
                "enum": [
                  "none",
                  "horizontal",
                  "vertical"
                ]
              },
              "crop": {
                "type": "object",
                "properties": {
                  "left": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20000
                  },
                  "top": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20000
                  },
                  "width": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 20000
                  },
                  "height": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 20000
                  }
                },
                "required": [
                  "left",
                  "top",
                  "width",
                  "height"
                ],
                "additionalProperties": false
              }
            },
            "additionalProperties": false
          },
          "doc": {
            "type": "object",
            "properties": {
              "pages": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "pattern": "^\\d+(-\\d+)?(,\\d+(-\\d+)?)*$"
              },
              "rotate": {
                "anyOf": [
                  {
                    "type": "number",
                    "const": 90
                  },
                  {
                    "type": "number",
                    "const": 180
                  },
                  {
                    "type": "number",
                    "const": 270
                  }
                ]
              },
              "quality": {
                "type": "string",
                "enum": [
                  "screen",
                  "ebook",
                  "printer",
                  "prepress"
                ]
              },
              "dpi": {
                "type": "integer",
                "minimum": 36,
                "maximum": 600
              }
            },
            "additionalProperties": false
          },
          "archive": {
            "type": "object",
            "properties": {
              "level": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9
              }
            },
            "additionalProperties": false
          }
        },
        "additionalProperties": false,
        "description": "Conversion settings, sent as JSON in the `x-tff-options` header.\n\n4 rules cannot be expressed in JSON Schema and are enforced anyway,\nwith a 400 rather than silently:\n- `video.crf` applies only when `video.mode` is `quality`.\n- `image.quality` may not be sent alongside `image.lossless`.\n- `trim.endMs` must be greater than `trim.startMs`.\n- `audio.bitrateKbps` is refused for `flac`, which is lossless.\n\nA fifth depends on the target format rather than on this object: a codec\nhas to be legal for the container it goes in. The permitted combinations\nare published as `x-tff-container-codecs` at the root of this document."
      }
    }
  }
}