{
  "openapi": "3.0.3",
  "info": {
    "title": "Audio Audit REST API",
    "version": "1.0.0",
    "description": "REST API for Audio Audit — list podcasts and episodes, create and poll reports,\nand check credits.\n\n**Authentication.** Send an API key as `Authorization: Bearer <key>`. Keys are\ncreated on the [developers settings page](/settings/developers)\nand each key is bound to exactly one workspace: a personal key reaches only your\nown podcasts and reports, an organisation key only that organisation's. The\nworkspace is never a request parameter — it is a property of the credential.\n\nA browser session is accepted as a second scheme so that a logged-in user can use\n\"Try it out\" on this page against their personal workspace. Session authentication\nis **read-only**; writes require an API key.\n\n**Conventions.** Field names are `snake_case`, timestamps are ISO-8601 UTC, and\nlist endpoints return `{\"items\": [...], \"limit\": n, \"offset\": n, \"has_more\": bool}`.\n\n**Errors** are always `{\"error\": {\"code\": \"...\", \"message\": \"...\"}}` with a stable\nmachine-readable `code`; a response is never partial data plus an error.\n\n**Compatibility.** This version evolves additively: new endpoints, new optional\nparameters and new response fields can appear at any time, so clients must ignore\nfields they do not recognise. Anything that would break an existing client gets a\nnew `/api/v2/` route instead."
  },
  "servers": [
    {
      "url": "/api/rest/v1",
      "description": "This deployment"
    },
    {
      "url": "https://audioaudit.io/api/rest/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    },
    {
      "session": []
    }
  ],
  "tags": [
    {
      "name": "me"
    },
    {
      "name": "podcasts"
    },
    {
      "name": "reports"
    },
    {
      "name": "uploads"
    },
    {
      "name": "credits"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "me",
        "summary": "Identify the workspace and key this credential belongs to",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "Identify the workspace and key this credential belongs to",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace",
                    "key"
                  ],
                  "properties": {
                    "workspace": {
                      "type": "object",
                      "required": [
                        "type",
                        "id",
                        "name"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "organisation",
                            "personal"
                          ],
                          "description": "Whether this key acts for an organisation or a personal workspace."
                        },
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The organisation or user this key is bound to."
                        },
                        "name": {
                          "type": "string",
                          "description": "Display name of the workspace."
                        }
                      }
                    },
                    "key": {
                      "type": "object",
                      "nullable": true,
                      "required": [
                        "id",
                        "description",
                        "masked_key",
                        "created_at",
                        "last_used_at"
                      ],
                      "description": "The API key that authenticated this request; null when the caller used a browser session.",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "description": {
                          "type": "string"
                        },
                        "masked_key": {
                          "type": "string",
                          "description": "The first eight characters of the key followed by an ellipsis, exactly as shown on the developers page — enough to tell two keys apart, never enough to use one."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "last_used_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Stamped at most once a minute, so it can lag a live integration by up to 60 seconds."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/podcasts": {
      "get": {
        "operationId": "podcasts",
        "summary": "List the podcasts in this workspace",
        "tags": [
          "podcasts"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List the podcasts in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "limit",
                    "offset",
                    "has_more"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "feed_url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "feed_url": {
                            "type": "string"
                          },
                          "automated_reporting": {
                            "type": "boolean",
                            "nullable": true,
                            "description": "Whether the feed is polled and reports generated automatically for the workspace the query named — the organisation given as `organisationId`, or the calling user personally when none was given."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        },
                        "nullable": true
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "description": "The page size that was applied."
                    },
                    "offset": {
                      "type": "integer",
                      "description": "The offset this page starts at."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Whether a further page exists."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "3a0e5b9b-409c-4bde-8fef-dfea858fda14",
                      "title": "Today in Focus",
                      "feed_url": "https://www.theguardian.com/news/series/todayinfocus/podcast.xml",
                      "automated_reporting": true,
                      "created_at": "2026-03-02T09:14:27.482913+00:00"
                    }
                  ],
                  "limit": 50,
                  "offset": 0,
                  "has_more": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/podcasts/{podcast_id}": {
      "get": {
        "operationId": "podcast",
        "summary": "Fetch one podcast from this workspace",
        "tags": [
          "podcasts"
        ],
        "parameters": [
          {
            "name": "podcast_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fetch one podcast from this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "feed_url"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    },
                    "feed_url": {
                      "type": "string"
                    },
                    "automated_reporting": {
                      "type": "boolean",
                      "nullable": true,
                      "description": "Whether the feed is polled and reports generated automatically for the workspace the query named — the organisation given as `organisationId`, or the calling user personally when none was given."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "id": "3a0e5b9b-409c-4bde-8fef-dfea858fda14",
                  "title": "Today in Focus",
                  "feed_url": "https://www.theguardian.com/news/series/todayinfocus/podcast.xml",
                  "automated_reporting": true,
                  "created_at": "2026-03-02T09:14:27.482913+00:00"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/podcasts/{podcast_id}/episodes": {
      "get": {
        "operationId": "podcast_episodes",
        "summary": "List the episodes seen for a podcast, newest first",
        "tags": [
          "podcasts"
        ],
        "parameters": [
          {
            "name": "podcast_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List the episodes seen for a podcast, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "limit",
                    "offset",
                    "has_more"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "audio_url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "guid": {
                            "type": "string",
                            "nullable": true
                          },
                          "audio_url": {
                            "type": "string"
                          },
                          "published_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "first_seen_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        },
                        "nullable": true
                      },
                      "description": "Episodes of this show, newest first. `limit` defaults to 20; pass `offset` to page through the rest."
                    },
                    "limit": {
                      "type": "integer",
                      "description": "The page size that was applied."
                    },
                    "offset": {
                      "type": "integer",
                      "description": "The offset this page starts at."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Whether a further page exists."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "75e341ec-7f90-423a-80ec-282bc7afca71",
                      "title": "The people v the datacentre? The fight for Brick Lane",
                      "guid": "6a8476818f084e346fe0dfea",
                      "audio_url": "https://audio.guim.co.uk/2026/08/20260819tiftruman.mp3",
                      "published_on": "2026-08-04",
                      "first_seen_at": "2026-08-04T11:02:56.117402+00:00"
                    }
                  ],
                  "limit": 50,
                  "offset": 0,
                  "has_more": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports": {
      "get": {
        "operationId": "reports",
        "summary": "List reports in this workspace, newest first",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "podcast_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only reports for this podcast, within this workspace. A podcast id from `GET /podcasts`."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "SCHEDULED",
                "IN_PROGRESS",
                "COMPLETE",
                "FAILED",
                "OUT_OF_QUOTA"
              ]
            },
            "description": "Only reports with this status — the same value the `status` field of a report carries, so a value read out of a response can be sent straight back."
          },
          {
            "name": "created_since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only reports **created** strictly after this instant — exclusive, so a value read out of a `created_at` is not returned again. For \"what has finished since I last looked\", use `updated_since` instead: a report is created before it is run and reaches `COMPLETE` through an *update*, so a report that finishes after a later-created one has already moved a `created_at` watermark on would never be returned. ISO-8601; a value with no offset is read as UTC. Percent-encode a `+` offset as `%2B`, or use `Z` — an unencoded `+` decodes to a space."
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only reports **updated** strictly after this instant — exclusive, like `created_since`, and the bound to poll on. A report reaches its final status through an update, and reports finish out of order, so `updated_at` is the only column a poller can advance without stepping over a report that is still running. Read **every page** of the filtered listing and store `max(updated_at)` across all of them: this listing is ordered by `created_at` descending, so the first item is not the most recently updated one, and a watermark taken from it redelivers the same reports on every poll. If you cannot finish paging, keep the watermark you started with rather than advancing it part way. ISO-8601; a value with no offset is read as UTC. Percent-encode a `+` offset as `%2B`, or use `Z` — an unencoded `+` decodes to a space."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List reports in this workspace, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "limit",
                    "offset",
                    "has_more"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "status",
                          "original_filename"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "PENDING",
                              "SCHEDULED",
                              "IN_PROGRESS",
                              "COMPLETE",
                              "FAILED",
                              "OUT_OF_QUOTA"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "original_filename": {
                            "type": "string"
                          },
                          "podcast": {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "title": {
                                "type": "string",
                                "nullable": true
                              }
                            },
                            "nullable": true,
                            "description": "The show this report was generated for, when it came from a monitored feed. Null for a direct upload."
                          },
                          "episode": {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "title": {
                                "type": "string",
                                "nullable": true
                              },
                              "published_on": {
                                "type": "string",
                                "format": "date",
                                "nullable": true
                              }
                            },
                            "nullable": true
                          }
                        },
                        "nullable": true
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "description": "The page size that was applied."
                    },
                    "offset": {
                      "type": "integer",
                      "description": "The offset this page starts at."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Whether a further page exists."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "379ccf98-c409-424e-86a3-966ed70a2774",
                      "status": "COMPLETE",
                      "created_at": "2026-08-04T11:32:07.918204+00:00",
                      "updated_at": "2026-08-04T11:41:52.660311+00:00",
                      "original_filename": "20260819tiftruman.mp3",
                      "podcast": {
                        "id": "3a0e5b9b-409c-4bde-8fef-dfea858fda14",
                        "title": "Today in Focus"
                      },
                      "episode": {
                        "id": "75e341ec-7f90-423a-80ec-282bc7afca71",
                        "title": "The people v the datacentre? The fight for Brick Lane",
                        "published_on": "2026-08-04"
                      }
                    }
                  ],
                  "limit": 50,
                  "offset": 0,
                  "has_more": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "create_report",
        "summary": "Create a report — from a feed URL, from a podcast in this workspace, or from an upload",
        "tags": [
          "reports"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "feed_url": {
                        "type": "string",
                        "description": "The RSS feed to report on. If this workspace does not already hold the show, reporting on it adds it — which is why a workspace at its plan’s podcast limit is refused with `plan_limit_reached`."
                      },
                      "episode_index": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 2147483647,
                        "default": 0,
                        "description": "Which episode of the feed to report on, newest first — `0` is the latest. An index past the end of the feed is a `validation_error`."
                      }
                    },
                    "required": [
                      "feed_url"
                    ],
                    "title": "From a feed URL"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "podcast_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "A podcast already in this workspace, from `GET /podcasts`. Its feed is fetched exactly as `feed_url` would be."
                      },
                      "episode_index": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 2147483647,
                        "default": 0,
                        "description": "Which episode of the feed to report on, newest first — `0` is the latest. An index past the end of the feed is a `validation_error`."
                      }
                    },
                    "required": [
                      "podcast_id"
                    ],
                    "title": "From a podcast in this workspace"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "report_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "The `report_id` from `POST /uploads`, after the audio has been PUT to the `upload_url`. It must have been minted by this same workspace within the last 7 days and not already used — anything else is a `404`, and an id that has already produced a report is a `409`."
                      },
                      "original_filename": {
                        "type": "string",
                        "description": "The name of the file that was uploaded. Shown in the dashboard and on the report."
                      },
                      "duration_ms": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 2147483647,
                        "description": "Length of the audio in milliseconds. Used for the pre-flight credit estimate; the amount actually charged is measured from the file when the report completes."
                      }
                    },
                    "required": [
                      "report_id",
                      "original_filename",
                      "duration_ms"
                    ],
                    "title": "From a file uploaded with POST /uploads"
                  }
                ],
                "description": "Send exactly one of `feed_url`, `podcast_id`, `report_id`. The one you send selects which kind of report is created; sending none, or more than one, is a `validation_error`."
              }
            }
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "201": {
            "description": "Create a report — from a feed URL, from a podcast in this workspace, or from an upload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "COMPLETE",
                        "FAILED",
                        "IN_PROGRESS",
                        "OUT_OF_QUOTA",
                        "PENDING",
                        "SCHEDULED"
                      ]
                    },
                    "estimated_credits": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "id": "379ccf98-c409-424e-86a3-966ed70a2774",
                  "status": "PENDING"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/{report_id}": {
      "get": {
        "operationId": "report",
        "summary": "Fetch one report from this workspace",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "report_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fetch one report from this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status",
                    "original_filename",
                    "personal_report"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "PENDING",
                        "SCHEDULED",
                        "IN_PROGRESS",
                        "COMPLETE",
                        "FAILED",
                        "OUT_OF_QUOTA"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "nullable": true,
                      "description": "Display name for the report: the title tag read from the audio, else the podcast episode title, else the uploaded filename."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "started_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "original_filename": {
                      "type": "string"
                    },
                    "personal_report": {
                      "type": "boolean"
                    },
                    "credits_charged": {
                      "type": "string",
                      "format": "decimal",
                      "nullable": true,
                      "description": "Analysis credits debited at completion. Null until charged."
                    },
                    "audio_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL of the analysed audio file."
                    },
                    "stats": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    },
                    "results": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    },
                    "file_info": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    },
                    "metadata": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    },
                    "cover_image": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    },
                    "podcast": {
                      "type": "object",
                      "required": [
                        "id",
                        "feed_url"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "title": {
                          "type": "string",
                          "nullable": true
                        },
                        "feed_url": {
                          "type": "string"
                        }
                      },
                      "nullable": true,
                      "description": "The show this report was generated for, when it came from a monitored feed. Null for a direct upload."
                    },
                    "episode": {
                      "type": "object",
                      "required": [
                        "id",
                        "audio_url"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "title": {
                          "type": "string",
                          "nullable": true
                        },
                        "guid": {
                          "type": "string",
                          "nullable": true
                        },
                        "audio_url": {
                          "type": "string"
                        },
                        "published_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        }
                      },
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "id": "379ccf98-c409-424e-86a3-966ed70a2774",
                  "status": "COMPLETE",
                  "name": "The people v the datacentre? The fight for Brick Lane",
                  "created_at": "2026-08-04T11:32:07.918204+00:00",
                  "updated_at": "2026-08-04T11:41:52.660311+00:00",
                  "started_at": "2026-08-04T11:32:19.204411+00:00",
                  "original_filename": "20260819tiftruman.mp3",
                  "personal_report": false,
                  "credits_charged": "306.00",
                  "audio_url": "https://storage.example.com/artifacts/audio/podcast-episode/75e341ec-7f90-423a-80ec-282bc7afca71.mp3",
                  "stats": {
                    "passes": 13,
                    "failures": 4,
                    "total": 17,
                    "percent": 76,
                    "time_remaining": 0
                  },
                  "results": [
                    {
                      "name": "loudness_lufs",
                      "verbose_name": "Loudness (LUFS)",
                      "status": "FAIL",
                      "value": -21.4,
                      "measurement_unit": "LUFS",
                      "description": "You may have noticed having to turn up the volume to listen to a particular podcast but then have your ears blasted when you receive a phone notification…",
                      "warning_message": "Average volume is too quiet (below -17 LUFS).",
                      "link": "/articles/podcast/loudness-lufs"
                    },
                    {
                      "name": "peak",
                      "verbose_name": "Peak Volume",
                      "status": "PASS",
                      "value": -2.411563447672172,
                      "measurement_unit": "dB",
                      "description": "Peak volume refers to the point in an audio file which has the highest signal strength. It isn’t representative of overall loudness because it might only last for a millisecond…",
                      "warning_message": null,
                      "link": ""
                    }
                  ],
                  "file_info": {
                    "size": 47185920,
                    "duration": 1834000,
                    "codec_name": "mp3",
                    "format_name": "mp3",
                    "channels": 2,
                    "sample_rate": 44100
                  },
                  "metadata": {
                    "title": "The people v the datacentre? The fight for Brick Lane",
                    "artist": "The Guardian",
                    "album": "Today in Focus",
                    "track_num": null,
                    "year": "2026",
                    "copyright": null,
                    "chapters": [],
                    "comments": null
                  },
                  "cover_image": {
                    "width": 3000,
                    "height": 3000,
                    "file_size": 6558805,
                    "url": "https://storage.example.com/artifacts/cover-images/379ccf98-c409-424e-86a3-966ed70a2774.jpg"
                  },
                  "podcast": {
                    "id": "3a0e5b9b-409c-4bde-8fef-dfea858fda14",
                    "title": "Today in Focus",
                    "feed_url": "https://www.theguardian.com/news/series/todayinfocus/podcast.xml"
                  },
                  "episode": {
                    "id": "75e341ec-7f90-423a-80ec-282bc7afca71",
                    "title": "The people v the datacentre? The fight for Brick Lane",
                    "guid": "6a8476818f084e346fe0dfea",
                    "audio_url": "https://audio.guim.co.uk/2026/08/20260819tiftruman.mp3",
                    "published_on": "2026-08-04"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/{report_id}/transcript": {
      "get": {
        "operationId": "report_transcript",
        "summary": "Fetch a report’s transcript",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "report_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fetch a report’s transcript",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report_id"
                  ],
                  "properties": {
                    "report_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "transcript": {
                      "nullable": true,
                      "description": "Decoded JSON value. GraphQL exposes this field as a JSON-encoded string; REST returns the value itself. Null when the encoded string was absent or was itself `null`."
                    }
                  }
                },
                "example": {
                  "report_id": "379ccf98-c409-424e-86a3-966ed70a2774",
                  "transcript": [
                    {
                      "start": 1200,
                      "end": 4360,
                      "text": "welcome back to Today in Focus I'm your host"
                    },
                    {
                      "start": 4360,
                      "end": 9020,
                      "text": "and today we are talking about the fight over the Brick Lane datacentre"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "operationId": "create_upload",
        "summary": "Mint a report id and a URL to upload an audio file to",
        "tags": [
          "uploads"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "file_name": {
                    "type": "string",
                    "description": "The name of the file you are about to upload. Only its extension matters — it decides the object’s name and, with `file_type`, how the audio is read."
                  },
                  "file_type": {
                    "type": "string",
                    "description": "The file’s MIME type, e.g. `audio/mpeg`. It is signed into the upload URL, so the `Content-Type` of your PUT must match it exactly."
                  },
                  "duration_ms": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 2147483647,
                    "description": "Length of the audio in milliseconds. Send the same value to `POST /reports`, which is where it is used."
                  }
                },
                "required": [
                  "file_name",
                  "file_type",
                  "duration_ms"
                ]
              }
            }
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "201": {
            "description": "Mint a report id and a URL to upload an audio file to",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "upload_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "report_id": "f607eba2-8e39-4330-92dc-2086a94c59e8",
                  "upload_url": "https://storage.googleapis.com/audioaudit-artifacts/audio/f607eba2-8e39-4330-92dc-2086a94c59e8.mp3?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=900&X-Goog-Signature=..."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/credits": {
      "get": {
        "operationId": "credits",
        "summary": "Credit balance and plan allowance for this workspace",
        "tags": [
          "credits"
        ],
        "responses": {
          "200": {
            "description": "Credit balance and plan allowance for this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Credit balance and plan allowance for this workspace, decoded from the JSON-encoded string GraphQL exposes. **Untyped by design**: GraphQL types this field as a `String`, so no object schema can be derived from it and hand-writing one here would publish a promise the API does not check. The example below is the payload shape; it can gain keys additively, so read the ones you need and ignore the rest."
                },
                "example": {
                  "balance": 41720.0,
                  "plan_allowance": 120000,
                  "plan_used": 78280.0,
                  "plan_remaining": 41720.0,
                  "topup_balance": 0.0,
                  "progress": 0.652,
                  "finish_date": "2026-08-29T00:00:00+00:00",
                  "charging_enabled": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An organisation or personal API key from the developers settings page. The key determines the workspace; there is no workspace parameter."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "sessionid",
        "description": "A logged-in browser session, for trying endpoints from this page. Read-only: safe methods only, because the API views are CSRF-exempt for the Bearer scheme."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "conflict",
                  "forbidden",
                  "insufficient_credits",
                  "internal_error",
                  "invalid_api_key",
                  "method_not_allowed",
                  "not_found",
                  "plan_limit_reached",
                  "rate_limited",
                  "validation_error"
                ],
                "description": "Stable machine-readable code — match on this, not on `message`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. May change without notice."
              }
            }
          }
        }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "The request was not valid. `code`: `validation_error`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, unknown or disabled API key. `code`: `invalid_api_key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The workspace does not have enough credits. `code`: `insufficient_credits`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The workspace is not allowed to do this, or a plan limit has been reached. `code`: `forbidden`, `plan_limit_reached`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource in this workspace — or the resource exists but belongs to another workspace, which is reported the same way deliberately. `code`: `not_found`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The route exists but not for this method. `code`: `method_not_allowed`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Allow": {
            "description": "Methods this route does accept.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "Conflict": {
        "description": "The request has already been carried out and cannot be repeated — retrying it is safe but will keep returning this. `code`: `conflict`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Retry after the number of seconds in `Retry-After`. `code`: `rate_limited`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "InternalError": {
        "description": "Something went wrong on our side. `code`: `internal_error`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}