{
  "openapi": "3.1.0",
  "info": {
    "title": "Forms API",
    "version": "0.0.0",
    "description": "Service for viewer form submissions (Report It)"
  },
  "components": {
    "schemas": {
      "UploadsRequest": {
        "type": "object",
        "description": "Presigned upload request for a submission's media",
        "additionalProperties": false,
        "properties": {
          "formId": {
            "type": "string",
            "description": "The form being submitted",
            "maxLength": 64
          },
          "requestId": {
            "type": "string",
            "description": "Client-minted submission id (uuid)",
            "format": "uuid"
          },
          "files": {
            "type": "array",
            "description": "The files the viewer attached",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "contentType": {
                  "type": "string",
                  "description": "MIME type, e.g. image/jpeg",
                  "maxLength": 128
                },
                "size": {
                  "type": "number",
                  "description": "File size in bytes",
                  "minimum": 1
                },
                "name": {
                  "type": "string",
                  "description": "Original file name, shown in the DAM",
                  "maxLength": 255
                },
                "description": {
                  "type": "string",
                  "description": "Viewer caption for the file, shown in the DAM",
                  "maxLength": 200
                }
              },
              "required": [
                "contentType",
                "size"
              ]
            }
          }
        },
        "required": [
          "formId",
          "requestId",
          "files"
        ]
      },
      "UploadsResult": {
        "type": "object",
        "description": "Presigned upload URLs, one per requested file",
        "properties": {
          "uploads": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "assetId": {
                  "type": "string",
                  "description": "Asset id to reference at submit"
                },
                "url": {
                  "type": "string",
                  "description": "Presigned S3 PUT URL"
                },
                "method": {
                  "type": "string",
                  "enum": [
                    "PUT"
                  ]
                },
                "expires": {
                  "type": "string",
                  "description": "Presign expiry (ISO 8601)"
                },
                "assetUrl": {
                  "type": "string",
                  "description": "Public URL the asset serves from once uploaded"
                }
              },
              "required": [
                "assetId",
                "url",
                "method"
              ]
            }
          }
        },
        "required": [
          "uploads"
        ]
      },
      "RequestBody": {
        "type": "object",
        "description": "A form submission",
        "additionalProperties": false,
        "properties": {
          "formId": {
            "type": "string",
            "description": "The form being submitted",
            "maxLength": 64
          },
          "flowVersion": {
            "type": "string",
            "description": "The flowVersion of the form document the viewer walked",
            "maxLength": 64
          },
          "path": {
            "type": "array",
            "description": "Choice steps in walk order, as \"<nodeId>:<optionId>\"",
            "maxItems": 32,
            "items": {
              "type": "string",
              "maxLength": 128
            }
          },
          "answers": {
            "type": "object",
            "description": "Keyed field answers for the walked path"
          },
          "media": {
            "type": "array",
            "description": "Uploaded assets to attach, from POST /v1/uploads",
            "maxItems": 20,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "assetId": {
                  "type": "string",
                  "format": "uuid"
                },
                "type": {
                  "type": "string",
                  "maxLength": 128
                }
              },
              "required": [
                "assetId",
                "type"
              ]
            }
          }
        },
        "required": [
          "formId",
          "flowVersion",
          "path",
          "answers"
        ]
      },
      "RequestResult": {
        "type": "object",
        "description": "A received submission",
        "properties": {
          "requestId": {
            "type": "string",
            "description": "The submission id"
          },
          "reference": {
            "type": "string",
            "description": "Viewer-facing reference, e.g. RI-20260818-K4M2Q"
          },
          "confirmation": {
            "type": "object",
            "description": "Confirmation copy from the form's terminal node"
          },
          "routing": {
            "type": "object",
            "description": "Where the submission was dispatched",
            "properties": {
              "destination": {
                "type": "string"
              },
              "state": {
                "type": "string",
                "enum": [
                  "pending",
                  "delivered",
                  "failed"
                ]
              }
            }
          }
        },
        "required": [
          "requestId",
          "reference",
          "routing"
        ]
      },
      "AnonSessionRequest": {
        "type": "object",
        "description": "Request for an anonymous submission session",
        "additionalProperties": false,
        "properties": {
          "formId": {
            "type": "string",
            "description": "The form being submitted",
            "maxLength": 64
          },
          "requestId": {
            "type": "string",
            "description": "Client-minted submission id (uuid) the session is bound to",
            "format": "uuid"
          },
          "humanToken": {
            "type": "string",
            "description": "Proof-of-human token, when a provider is configured",
            "maxLength": 4096
          },
          "saveContent": {
            "type": "boolean",
            "description": "Whether the submission is written to the content stage (the Studio queue) as well as emailed to the desk. Omit for the deployment default advertised by GET /v1/forms."
          }
        },
        "required": [
          "formId",
          "requestId"
        ]
      },
      "AnonSessionResult": {
        "type": "object",
        "description": "A short-lived anonymous submission session",
        "additionalProperties": false,
        "properties": {
          "token": {
            "type": "string",
            "description": "Bearer token for this submission only"
          },
          "expiresIn": {
            "type": "number",
            "description": "Lifetime in seconds"
          }
        },
        "required": [
          "token",
          "expiresIn"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string"
              },
              "stack": {
                "type": "string"
              },
              "nonce": {
                "type": "string"
              }
            },
            "required": [
              "message"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "publicJWT": {
        "name": "Public JWT",
        "description": "My WRAL audience identity token (Cognito ID token)",
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "anonSession": {
        "name": "Anonymous session",
        "description": "Submission-scoped token from POST /v1/anon/sessions",
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "paths": {
    "/forms": {
      "get": {
        "summary": "List forms",
        "description": "All published form documents except standalone page forms (see GET /v1/forms/{formId}). Each entry-screen category is one form carrying its own flow tree. Also reports whether anonymous submission is enabled, which proof-of-human provider, if any, the client must satisfy, and whether anonymous submissions are saved to the content stage by default.",
        "operationId": "listForms",
        "tags": [
          "Forms"
        ],
        "responses": {
          "200": {
            "description": "The published forms and anonymous-submission capability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "forms": {
                      "type": "array"
                    },
                    "anonymous": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "humanCheck": {
                          "description": "null when no verifier is configured",
                          "oneOf": [
                            {
                              "type": "null"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "provider": {
                                  "type": "string"
                                },
                                "siteKey": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "provider"
                              ]
                            }
                          ]
                        },
                        "saveContent": {
                          "type": "boolean",
                          "description": "Default for whether an anonymous submission is written to the content stage; a session may choose otherwise at POST /v1/anon/sessions"
                        }
                      },
                      "required": [
                        "enabled",
                        "humanCheck",
                        "saveContent"
                      ]
                    }
                  },
                  "required": [
                    "forms",
                    "anonymous"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/forms/{formId}": {
      "get": {
        "summary": "Get one form",
        "description": "One published form document by formId, standalone page forms included, with the anonymous-submission capability block of GET /v1/forms.",
        "operationId": "getForm",
        "tags": [
          "Forms"
        ],
        "parameters": [
          {
            "name": "formId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The form and the anonymous-submission capability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "form": {
                      "type": "object"
                    },
                    "anonymous": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "humanCheck": {
                          "description": "null when no verifier is configured",
                          "oneOf": [
                            {
                              "type": "null"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "provider": {
                                  "type": "string"
                                },
                                "siteKey": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "provider"
                              ]
                            }
                          ]
                        },
                        "saveContent": {
                          "type": "boolean",
                          "description": "Default for whether an anonymous submission is written to the content stage; a session may choose otherwise at POST /v1/anon/sessions"
                        }
                      },
                      "required": [
                        "enabled",
                        "humanCheck",
                        "saveContent"
                      ]
                    }
                  },
                  "required": [
                    "form",
                    "anonymous"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "formId is not a form id"
          },
          "404": {
            "description": "Unknown or disabled form"
          }
        }
      }
    },
    "/routes": {
      "get": {
        "summary": "List routes",
        "description": "Every routing destination id this deployment can dispatch, in display order, with a human-readable label.",
        "operationId": "listRoutes",
        "tags": [
          "Forms"
        ],
        "responses": {
          "200": {
            "description": "The routing destinations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "routes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "label"
                        ]
                      }
                    }
                  },
                  "required": [
                    "routes"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/anon/sessions": {
      "post": {
        "summary": "Start an anonymous submission",
        "description": "Mint a short-lived session bound to one requestId for a viewer submitting without a My WRAL account. Presented to /v1/uploads and /v1/requests/{requestId} in place of an identity token. The session fixes whether the submission is written to the content stage or only emailed (saveContent, defaulting to the deployment setting). Rate limited per caller; returns 404 where anonymous submission is not enabled.",
        "operationId": "createAnonSession",
        "tags": [
          "Forms"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnonSessionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "An anonymous session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnonSessionResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, or missing human verification"
          },
          "403": {
            "description": "This form cannot be submitted anonymously"
          },
          "404": {
            "description": "Unknown form, or anonymous submission disabled"
          },
          "429": {
            "description": "Too many anonymous submissions from this caller"
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "summary": "Mint upload URLs",
        "description": "Mint presigned PUT URLs for a submission's media. Called at submit time only; assets are stamped with the submission id and account id for verification at the record write, and with the name, description, credit and copyright the DAM displays.",
        "operationId": "createUploads",
        "tags": [
          "Forms"
        ],
        "security": [
          {
            "publicJWT": []
          },
          {
            "anonSession": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadsRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Presigned upload URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadsResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, budget violation, or media on an anonymous session that does not save the submission"
          },
          "401": {
            "description": "Missing or invalid identity token or anonymous session"
          },
          "403": {
            "description": "Anonymous session bound to another submission"
          },
          "404": {
            "description": "Unknown form"
          },
          "429": {
            "description": "Submission limit reached"
          },
          "502": {
            "description": "The DAM rejected the presign request"
          }
        }
      }
    },
    "/requests/{requestId}": {
      "put": {
        "summary": "Submit a form request",
        "description": "The atomic commit of a submission: validates the walked path and answers against the form document, verifies uploaded media provenance, writes the form-request record, and dispatches the desk notification. Idempotent on requestId.",
        "operationId": "putRequest",
        "tags": [
          "Forms"
        ],
        "security": [
          {
            "publicJWT": []
          },
          {
            "anonSession": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RequestBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission already received (idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestResult"
                }
              }
            }
          },
          "201": {
            "description": "Submission received",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid path, answers, or media; or media / a queue-only route on a submission that is not saved"
          },
          "401": {
            "description": "Missing or invalid identity token or anonymous session"
          },
          "403": {
            "description": "requestId belongs to another account, or the anonymous session is bound to a different submission"
          },
          "404": {
            "description": "Unknown form"
          },
          "409": {
            "description": "The form changed; review and resubmit"
          },
          "429": {
            "description": "Submission limit reached"
          },
          "502": {
            "description": "The submission record could not be written, or an unsaved submission could not be delivered"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "GET /openapi.json",
        "description": "Returns the OpenAPI JSON specification for the content api.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "https://spec.openapis.org/oas/3.1/schema/2021-09-28"
                }
              }
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://api.wral.com/forms/v1"
    }
  ]
}