openapi: 3.0.3 info: title: Kererū Forms API version: 1.0.0 description: | API for **Kererū forms**, a simple survey/form capture system. Forms are versioned and can be inactivated. A form contains an ordered set of questions whose answers are one of: true/false, numeric, a drop-down selection, date or free-form text. Responses are identified by a user identifier captured at the start of the form. A form may optionally require a single-use access code. contact: name: Kererū Forms email: support@kereru.forms license: name: MIT servers: - url: http://localhost:8080 description: Local development - url: http://localhost:8080/api/v1 description: Local development (v1 base) tags: - name: system description: Health and meta endpoints - name: forms description: Form definition and management - name: submissions description: Form submissions and management of captured data - name: access-codes description: Single-use access codes - name: auth description: Password fallback and OIDC-backed bearer authentication paths: /health: get: tags: [system] operationId: getHealth summary: Service health responses: '200': description: Service is healthy content: application/json: schema: $ref: '#/components/schemas/Health' /forms: get: tags: [forms] operationId: listForms summary: List forms description: Returns form summaries. Use `includeInactive` to include inactivated forms (management view). parameters: - name: includeInactive in: query required: false schema: type: boolean default: false - name: activeOnly in: query required: false deprecated: true schema: type: boolean responses: '200': description: A list of forms content: application/json: schema: type: array items: $ref: '#/components/schemas/FormSummary' '500': $ref: '#/components/responses/Error' post: tags: [forms] operationId: createForm summary: Create a form security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateFormRequest' responses: '201': description: Form created content: application/json: schema: $ref: '#/components/schemas/Form' '400': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}: parameters: - $ref: '#/components/parameters/FormId' get: tags: [forms] operationId: getForm summary: Get a form by id responses: '200': description: The form content: application/json: schema: $ref: '#/components/schemas/Form' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' put: tags: [forms] operationId: updateForm summary: Update a form definition security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateFormRequest' responses: '200': description: Updated form content: application/json: schema: $ref: '#/components/schemas/Form' '400': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' delete: tags: [forms] operationId: deleteForm summary: Delete a form definition security: - bearerAuth: [] responses: '204': description: Deleted '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/inactivate: parameters: - $ref: '#/components/parameters/FormId' post: tags: [forms] operationId: inactivateForm summary: Inactivate a form security: - bearerAuth: [] description: Inactivated forms remain visible in management but cannot accept new submissions. responses: '200': description: Inactivated form content: application/json: schema: $ref: '#/components/schemas/Form' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/activate: parameters: - $ref: '#/components/parameters/FormId' post: tags: [forms] operationId: activateForm summary: Activate a form security: - bearerAuth: [] responses: '200': description: Activated form content: application/json: schema: $ref: '#/components/schemas/Form' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/versions: parameters: - $ref: '#/components/parameters/FormId' post: tags: [forms] operationId: createFormVersion summary: Create a new version of a form security: - bearerAuth: [] description: Clones the form and its questions into a new version with the version number incremented. The new version is active; the previous version is inactivated. responses: '201': description: New form version content: application/json: schema: $ref: '#/components/schemas/Form' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/pdf: parameters: - $ref: '#/components/parameters/FormId' get: tags: [forms] operationId: getFormPdf summary: Download a PDF rendering of the form definition responses: '200': description: PDF document generated from the form definition content: application/pdf: schema: type: string format: binary '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/submissions: parameters: - $ref: '#/components/parameters/FormId' get: tags: [submissions] operationId: listFormSubmissions summary: List submissions for a form (management) security: - bearerAuth: [] responses: '200': description: Submissions content: application/json: schema: type: array items: $ref: '#/components/schemas/SubmissionSummary' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' post: tags: [submissions] operationId: submitForm summary: Submit an answer set for a form description: | Captures a completed form. The `userId` (captured at the start of the form) identifies the respondent. If the form requires an access code, a valid, unused `accessCode` must be supplied and is consumed on success. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SubmitFormRequest' responses: '201': description: Submission accepted content: application/json: schema: $ref: '#/components/schemas/Submission' '400': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '410': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /submissions/{submissionId}: parameters: - name: submissionId in: path required: true schema: type: string format: uuid get: tags: [submissions] operationId: getSubmission summary: Get a submission by id (management) security: - bearerAuth: [] responses: '200': description: Submission content: application/json: schema: $ref: '#/components/schemas/Submission' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /submissions/{submissionId}/offline-pdf: parameters: - name: submissionId in: path required: true schema: type: string format: uuid get: tags: [submissions] operationId: getSubmissionOfflinePdf summary: Download the offline PDF uploaded with a submission security: - bearerAuth: [] responses: '200': description: The uploaded PDF content: application/pdf: schema: type: string format: binary '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /forms/{formId}/access-codes: parameters: - $ref: '#/components/parameters/FormId' get: tags: [access-codes] operationId: listAccessCodes summary: List access codes for a form security: - bearerAuth: [] responses: '200': description: Access codes content: application/json: schema: type: array items: $ref: '#/components/schemas/AccessCode' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' post: tags: [access-codes] operationId: createAccessCodes summary: Create one or more access codes for a form security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAccessCodesRequest' responses: '201': description: Created access codes content: application/json: schema: type: array items: $ref: '#/components/schemas/AccessCode' '400': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' /auth/config: get: tags: [auth] operationId: getAuthConfig summary: Get browser authentication configuration description: | When `mode` is `oidc`, the SPA performs an authorization-code flow with PKCE against the configured provider and then presents the returned ID token as a bearer token to this API. When `mode` is `password`, the SPA uses `/auth/login`. responses: '200': description: Authentication configuration content: application/json: schema: $ref: '#/components/schemas/AuthConfig' /auth/login: post: tags: [auth] operationId: login summary: Log in with the local password fallback description: Only available when no OIDC issuer is configured. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: Signed local JWT content: application/json: schema: $ref: '#/components/schemas/LoginResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' /auth/me: get: tags: [auth] operationId: getCurrentUser summary: Get the current authenticated user security: - bearerAuth: [] responses: '200': description: Current authentication state content: application/json: schema: $ref: '#/components/schemas/AuthMeResponse' '401': $ref: '#/components/responses/Error' /auth/logout: post: tags: [auth] operationId: logout summary: Log out description: Stateless tokens are discarded by the client; this endpoint exists for symmetry. security: - bearerAuth: [] responses: '204': description: Logged out components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | Local password-mode JWT, or an OIDC ID token obtained by the SPA using the authorization-code flow with PKCE. parameters: FormId: name: formId in: path required: true schema: type: string format: uuid responses: Error: description: Error response content: application/json: schema: $ref: '#/components/schemas/Error' schemas: AuthConfig: type: object required: [mode, requireSubmissionAuth] properties: mode: type: string enum: [password, oidc] requireSubmissionAuth: type: boolean oidc: $ref: '#/components/schemas/OIDCClientConfig' OIDCClientConfig: type: object required: [issuerUrl, authorizationEndpoint, tokenEndpoint, clientId, scopes] properties: issuerUrl: type: string format: uri authorizationEndpoint: type: string format: uri tokenEndpoint: type: string format: uri clientId: type: string scopes: type: array items: type: string LoginRequest: type: object required: [password] properties: password: type: string format: password LoginResponse: type: object required: [token, user] properties: token: type: string user: $ref: '#/components/schemas/AuthUser' AuthUser: type: object required: [id, roles, canAdmin, canView] properties: id: type: string name: type: string email: type: string format: email roles: type: array items: type: string canAdmin: type: boolean canView: type: boolean AuthMeResponse: type: object required: [authenticated] properties: authenticated: type: boolean user: $ref: '#/components/schemas/AuthUser' Health: type: object required: [status, version] properties: status: type: string example: ok version: type: string example: 1.0.0 uptimeSeconds: type: integer format: int64 QuestionType: type: string description: | * `boolean` – true/false * `number` – numeric answer * `select` – one option from a configured list * `text` – free-form text (single or multiline) * `date` - calendar date in `YYYY-MM-DD` form enum: [boolean, number, select, text, date] QuestionOption: type: object required: [value, label] properties: value: type: string label: type: string Question: type: object required: [id, position, key, label, type, required] properties: id: type: string format: uuid position: type: integer key: type: string description: Stable machine key for this question within the form label: type: string description: The question text (may be multiline, no formatting directives) description: type: string type: $ref: '#/components/schemas/QuestionType' required: type: boolean options: type: array description: Present for `select` questions items: $ref: '#/components/schemas/QuestionOption' minValue: type: number description: Minimum allowed value for `number` questions maxValue: type: number description: Maximum allowed value for `number` questions placeholder: type: string multiline: type: boolean description: Whether a `text` answer may span multiple lines FormSummary: type: object required: [id, formGroupId, version, title, isActive, questionCount, createdAt, updatedAt] properties: id: type: string format: uuid formGroupId: type: string format: uuid description: Groups all versions of the same logical form version: type: integer title: type: string description: type: string logoUrl: type: string pdfUrl: type: string description: Optional absolute URL of a prebuilt PDF form linked to this definition externalReferenceRequired: type: boolean description: Whether a reference identifier is required before the form can be submitted externalReferenceLabel: type: string description: Label shown for the external reference, for example Invoice number isActive: type: boolean questionCount: type: integer createdAt: type: string format: date-time updatedAt: type: string format: date-time Form: type: object required: [id, formGroupId, version, title, isActive, requiresAccessCode, questions, createdAt, updatedAt] properties: id: type: string format: uuid formGroupId: type: string format: uuid version: type: integer title: type: string description: type: string introduction: type: string description: Introduction section explaining purpose, background and support details isActive: type: boolean requiresAccessCode: type: boolean signatureRequired: type: boolean description: Whether a digital signature is captured at the end of the form logoUrl: type: string pdfUrl: type: string description: Optional absolute URL of a prebuilt PDF form linked to this definition externalReferenceRequired: type: boolean description: Whether a reference identifier is required before the form can be submitted externalReferenceLabel: type: string description: Label shown for the external reference, for example Invoice number companyName: type: string contactEmail: type: string contactPhone: type: string contactUrl: type: string questions: type: array items: $ref: '#/components/schemas/Question' createdAt: type: string format: date-time updatedAt: type: string format: date-time QuestionInput: type: object required: [key, label, type, required] properties: key: type: string label: type: string description: type: string type: $ref: '#/components/schemas/QuestionType' required: type: boolean options: type: array items: $ref: '#/components/schemas/QuestionOption' minValue: type: number maxValue: type: number placeholder: type: string multiline: type: boolean CreateFormRequest: type: object required: [title, questions] properties: title: type: string description: type: string introduction: type: string requiresAccessCode: type: boolean signatureRequired: type: boolean logoUrl: type: string pdfUrl: type: string externalReferenceRequired: type: boolean externalReferenceLabel: type: string companyName: type: string contactEmail: type: string contactPhone: type: string contactUrl: type: string questions: type: array items: $ref: '#/components/schemas/QuestionInput' UpdateFormRequest: type: object required: [title, questions] properties: title: type: string description: type: string introduction: type: string requiresAccessCode: type: boolean signatureRequired: type: boolean logoUrl: type: string pdfUrl: type: string externalReferenceRequired: type: boolean externalReferenceLabel: type: string companyName: type: string contactEmail: type: string contactPhone: type: string contactUrl: type: string questions: type: array items: $ref: '#/components/schemas/QuestionInput' UserIdType: type: string enum: [email, username, phone, foreignkey guid] Answer: type: object required: [questionId, value] properties: questionId: type: string format: uuid value: description: Answer value. String, number, boolean or list of strings depending on the question type. SubmitFormRequest: type: object required: [userId, userIdType, answers] properties: userId: type: string description: Respondent identifier captured at the start (email, username, phone, foreignkey or GUID) userIdType: $ref: '#/components/schemas/UserIdType' accessCode: type: string description: Required when the form has `requiresAccessCode = true` externalReference: type: string description: |- External reference identifier used to link this response to a record in another system, for example an invoice, account or policy number. answers: type: array items: $ref: '#/components/schemas/Answer' signature: $ref: '#/components/schemas/SignatureInput' offlinePdf: type: string description: | Base64 data URL of a PDF completed offline (for example `data:application/pdf;base64,...`). Maximum decoded size is 10 MB. offlinePdfName: type: string description: Original filename of the uploaded offline PDF tracking: $ref: '#/components/schemas/ClientTracking' ClientTracking: type: object description: Client supplied tracking details. IP and user agent are captured server-side. properties: locale: type: string timezone: type: string place: type: string referer: type: string SignatureType: type: string description: | * `drawn` - signature drawn in the browser * `image` - uploaded image containing a signature enum: [drawn, image] SignatureInput: type: object required: [type, dataUrl, name] properties: type: $ref: '#/components/schemas/SignatureType' dataUrl: type: string description: PNG or JPEG image data URL (maximum decoded size 2 MB) name: type: string description: Name of the person signing title: type: string description: Optional title of the person signing Signature: type: object required: [type, dataUrl, name, signedAt] properties: type: $ref: '#/components/schemas/SignatureType' dataUrl: type: string name: type: string title: type: string signedAt: type: string format: date-time AnswerRecord: type: object required: [questionId, questionKey, value] properties: questionId: type: string format: uuid questionKey: type: string value: description: The captured value Tracking: type: object properties: ipAddress: type: string userAgent: type: string locale: type: string timezone: type: string place: type: string referer: type: string Submission: type: object required: [id, formId, formVersion, userId, userIdType, answers, submittedAt] properties: id: type: string format: uuid formId: type: string format: uuid formVersion: type: integer userId: type: string userIdType: $ref: '#/components/schemas/UserIdType' externalReference: type: string answers: type: array items: $ref: '#/components/schemas/AnswerRecord' signature: $ref: '#/components/schemas/Signature' offlinePdfName: type: string description: Present when the respondent uploaded a completed offline PDF submittedAt: type: string format: date-time tracking: $ref: '#/components/schemas/Tracking' SubmissionSummary: type: object required: [id, formId, formVersion, userId, userIdType, submittedAt] properties: id: type: string format: uuid formId: type: string format: uuid formVersion: type: integer userId: type: string userIdType: $ref: '#/components/schemas/UserIdType' externalReference: type: string submittedAt: type: string format: date-time AccessCode: type: object required: [id, formId, code, used, createdAt] properties: id: type: string format: uuid formId: type: string format: uuid code: type: string used: type: boolean usedAt: type: string format: date-time createdAt: type: string format: date-time CreateAccessCodesRequest: type: object required: [count] properties: count: type: integer minimum: 1 maximum: 100 prefix: type: string Error: type: object required: [error] properties: error: type: string code: type: string