{
  "openapi": "3.1.0",
  "info": {
    "title": "SureDocs files API, version 1",
    "version": "1",
    "description": "Version 1 of the files API. It changes only in ways that are safe to ignore, and is never broken in place."
  },
  "servers": [
    {
      "url": "https://api.suredocs.ie"
    }
  ],
  "tags": [
    {
      "name": "files"
    }
  ],
  "paths": {
    "/files/v1": {
      "post": {
        "operationId": "uploadFile",
        "tags": [
          "files"
        ],
        "summary": "Upload a PDF, an image, a text file or a recording for the brokerage your access token names.",
        "description": "Files one document with the brokerage the access token was issued for. There is no broker in the request and none may be sent: the token says which. The answer says whether the document was stored, was one we already hold, or was refused and why. Send the same document again and the answer is duplicate, so a retry is safe. A duplicate is found by CONTENT, and by name: the same bytes under any name are a duplicate; the same name with different bytes is a different document, and is stored under a name derived from the original (storedAs) with the earlier one named in nameCollisionWith. We compare against documents stored in the last 18 days only, and do not remember for longer: the same bytes sent after that are stored again. A stored document is then classified and reaches the brokerage as a post, typed Unknown, from \"Partner upload\". It is served back to the brokerage as the contentType it was sent under, so an image opens as an image, a text file as text and a recording as a recording, not as a broken PDF.\n\nNeeds the `upload_file` scope, on the token and in the brokerage's grant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "upload_file"
        ],
        "responses": {
          "200": {
            "description": "Stored, or a duplicate of a document we already hold. The status field says which.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadFileResponse"
                }
              }
            }
          },
          "400": {
            "description": "missing_field, invalid_field, invalid_base64, not_a_pdf, not_a_jpeg, not_plain_text, not_a_png, not_a_wav, not_an_mp3, invalid_phone_number, invalid_json, unresolved_reference or filename_would_be_rewritten.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_token: the access token is absent, malformed, expired or not ours.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope: the token lacks upload_file. not_granted: the brokerage has no live grant for this integration. reference_not_permitted: the filename names a destination this token is not bound to.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_stored_at_path: something is already stored at the exact name this document would be stored under.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "file_too_large (more than 24,000,000 decoded bytes for a PDF, or 10,000,000 for a JPEG or a PNG, 2,000,000 for a text file, or 24,000,000 for a WAV or MP3 recording) or payload_too_large.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "unsupported_file_type: contentType is not one this version accepts.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota refused the call: rate_limited_burst, rate_limited_daily, rate_limited_broker_uploads or rate_limited_broker_daily. Retry-After says how many seconds to wait.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: our fault. Nothing is stored unless the answer says so; quote the requestId.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/Request-Id"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadFileRequest"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "UploadFileResponse": {
        "title": "UploadFileResponse",
        "type": "object",
        "required": [
          "status",
          "fileId",
          "requestId"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "stored",
              "duplicate"
            ],
            "description": "stored: the document is new to us and has been filed. duplicate: we already hold it, and nothing was stored."
          },
          "fileId": {
            "type": "string",
            "description": "An opaque id for the document, and for the earlier one when the status is duplicate. Compare it; never parse it."
          },
          "matchedOn": {
            "type": "array",
            "description": "With status duplicate: what matched. Content is always among them.",
            "items": {
              "type": "string",
              "enum": [
                "filename",
                "content"
              ]
            }
          },
          "duplicateOf": {
            "type": "string",
            "description": "With status duplicate: the fileId of the document we already hold."
          },
          "storedAs": {
            "type": "string",
            "description": "When the name was already in use by a different document, the name this one was filed under."
          },
          "nameCollisionWith": {
            "type": "string",
            "description": "With storedAs: the fileId of the different document that held the name."
          },
          "requestId": {
            "type": "string",
            "description": "The id of this request, which is also the Request-Id header. Quote it if you report a problem."
          }
        }
      },
      "Error": {
        "title": "Error",
        "type": "object",
        "required": [
          "status",
          "reason",
          "message",
          "requestId"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "description": "The HTTP status, repeated."
          },
          "reason": {
            "type": "string",
            "enum": [
              "missing_field",
              "invalid_field",
              "invalid_base64",
              "unsupported_file_type",
              "not_a_pdf",
              "not_a_jpeg",
              "not_plain_text",
              "not_a_png",
              "not_a_wav",
              "not_an_mp3",
              "file_too_large",
              "invalid_phone_number",
              "unresolved_reference",
              "reference_not_permitted",
              "filename_would_be_rewritten",
              "already_stored_at_path",
              "invalid_token",
              "insufficient_scope",
              "not_granted",
              "rate_limited_burst",
              "rate_limited_daily",
              "rate_limited_broker_uploads",
              "rate_limited_broker_daily",
              "rate_limited_unauthenticated",
              "invalid_json",
              "payload_too_large",
              "unsupported_charset",
              "unsupported_encoding",
              "internal_error"
            ],
            "description": "What to branch on. A reason you do not recognise is to be handled by its HTTP status."
          },
          "message": {
            "type": "string",
            "description": "For a person to read. Never branch on it."
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "UploadFileRequest": {
        "title": "UploadFileRequest",
        "type": "object",
        "required": [
          "filename",
          "contentType",
          "contentBase64"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "minLength": 1,
            "examples": [
              "renewal-notice.pdf",
              "meter-photo.jpeg",
              "call-transcript.txt",
              "call-recording.mp3"
            ],
            "description": "The name the partner knows the document by. Path separators and control characters are removed, it is cut to 150 characters and it always ends with the extension its contentType implies (.pdf, .jpeg, .png, .txt, .wav, .mp3), whatever extension you sent. It is stored as the document's name, so it is what the brokerage sees, and the original is kept alongside it. Never put a name in a URL: this is a body field for that reason."
          },
          "contentType": {
            "type": "string",
            "enum": [
              "application/pdf",
              "image/jpeg",
              "text/plain",
              "image/png",
              "audio/wav",
              "audio/mpeg"
            ],
            "description": "application/pdf, image/jpeg for a photo, image/png for an image such as a screenshot, text/plain for a UTF-8 text file such as a call transcript, or audio/wav or audio/mpeg for a WAV or MP3 recording such as a recorded call: each is stored as what it is, not converted, so what a brokerage can later download depends on what you sent. More types may be added later; that is never a breaking change."
          },
          "contentBase64": {
            "type": "string",
            "contentEncoding": "base64",
            "description": "The file, in standard base64 (RFC 4648 section 4) with padding and no line breaks. It must match contentType: a PDF begins %PDF-, of at most 24,000,000 decoded bytes; a JPEG begins with the JPEG SOI marker, of at most 10,000,000 decoded bytes; a text file is UTF-8, optionally beginning with a byte order mark, with no control characters but tab, line feed, carriage return and form feed, and some text in it, of at most 2,000,000 decoded bytes; a PNG begins with the PNG signature, of at most 10,000,000 decoded bytes; a WAV file begins with a RIFF header of form WAVE and an MP3 file with an ID3 tag or an MPEG audio Layer III frame header, each of at most 24,000,000 decoded bytes. Every limit is on the decoded file."
          },
          "phoneNumber": {
            "type": "string",
            "pattern": "^\\+[1-9][0-9]{6,14}$",
            "description": "Optional. E.164: a plus sign, the country code and the number, digits only, 7 to 15 of them. Stored; nothing uses it yet."
          },
          "externalId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Optional. Your own id for this document, kept with it."
          }
        }
      }
    },
    "headers": {
      "Request-Id": {
        "description": "The id of this request. Quote it if you report a problem: it is what we start an investigation from.",
        "schema": {
          "type": "string"
        },
        "example": "2f4a5f8e-9d3c-4b6e-8f4a-0c5b1e7d2a90"
      },
      "RateLimit-Policy": {
        "description": "Every limit that applies to the request, by name, with its quota q and window w in seconds. An HTTP structured field (RFC 9651): read it with a structured-field parser. Advisory; see Rate limits.",
        "schema": {
          "type": "string"
        },
        "example": "\"burst\";q=60;w=60, \"daily\";q=5000;w=86400"
      },
      "RateLimit": {
        "description": "For each of those limits, how many requests remain (r) and how many seconds until the window resets (t). An HTTP structured field (RFC 9651). Advisory; see Rate limits.",
        "schema": {
          "type": "string"
        },
        "example": "\"burst\";r=41;t=23, \"daily\";r=4870;t=51300"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The access token from POST /oauth/token, sent as `Authorization: Bearer <token>`. It is opaque: never parse it."
      }
    }
  }
}
