openapi: 3.1.0
info:
  title: Alistia Public Share API
  version: "1.0.0"
  description: >
    Read-only, token-based delivery of shared Alistia lists and saved views.


    An Alistia user can publish a list (or, later, a saved view) as a **public
    share**. Publishing produces a random token. Anyone holding the token can
    read a **cleaned snapshot** of the shared data — never the raw internal
    data model. The same snapshot powers both the human-facing web page
    (`https://alistia.app/s/{token}`) and this machine-readable JSON API.


    Only whitelisted fields are exposed. Profiles, members, circles, sync
    operations, conflicts and private cloud URLs are never included. Links are
    revocable and can carry an expiry date. Tokens are stored server-side only
    as a SHA-256 hash.


    This is version `v1`. Changes within `v1` are additive only.
  contact:
    name: Alistia
    url: https://alistia.app
  license:
    name: Proprietary
servers:
  - url: https://link.alistia.app
    description: Production (public data / function domain)
tags:
  - name: views
    description: Read shared list/view snapshots.
paths:
  /v1/views/{token}:
    get:
      tags: [views]
      operationId: getSharedView
      summary: Get a shared view snapshot as JSON
      description: >
        Returns the latest cleaned snapshot for the share identified by
        `token`. The token is matched server-side against a stored SHA-256
        hash. Disabled or expired links do not return data.
      parameters:
        - $ref: '#/components/parameters/Token'
      responses:
        '200':
          description: The shared snapshot.
          headers:
            Cache-Control:
              schema: { type: string }
              description: Snapshots are cacheable for a short time (e.g. 60s).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ShareSnapshot' }
              examples:
                travel:
                  $ref: '#/components/examples/TravelSnapshot'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Disabled'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Expired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/views/{token}.json:
    get:
      tags: [views]
      operationId: getSharedViewJson
      summary: Get a shared view snapshot (explicit .json data link)
      description: >
        The machine-readable Data Link form. Identical payload to
        `GET /v1/views/{token}`, but this explicit `.json` form is only served
        when the share owner has enabled the JSON data link (a Premium feature).
        Otherwise it returns `403 json_disabled`.
      parameters:
        - $ref: '#/components/parameters/Token'
      responses:
        '200':
          description: The shared snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ShareSnapshot' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/JsonDisabled' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Expired' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/views/{token}.csv:
    get:
      tags: [views]
      operationId: getSharedViewCsv
      summary: Get a shared view as CSV
      description: >
        The same data as CSV: one row per entry, first column `id`, then one
        column per field. Gated like the JSON data link.
      parameters:
        - $ref: '#/components/parameters/Token'
      responses:
        '200':
          description: CSV export.
          content:
            text/csv:
              schema: { type: string }
        '403': { $ref: '#/components/responses/JsonDisabled' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Expired' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/views/{token}.ics:
    get:
      tags: [views]
      operationId: getSharedViewIcs
      summary: Get a shared view as iCalendar
      description: >
        A calendar feed (RFC 5545). Each entry that has a date field becomes a
        VEVENT. Subscribe from any calendar app. Gated like the JSON data link.
      parameters:
        - $ref: '#/components/parameters/Token'
      responses:
        '200':
          description: iCalendar feed.
          content:
            text/calendar:
              schema: { type: string }
        '403': { $ref: '#/components/responses/JsonDisabled' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Expired' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/entries:
    servers:
      - url: https://write.alistia.app
        description: Authenticated write API
    post:
      tags: [write]
      operationId: createEntries
      summary: Create one or many entries
      description: >
        Creates entries in the token's list. Body is a single object with
        `values`, or `{ "entries": [ {values}, ... ] }` for bulk. Values are
        keyed by field key (from the read view) and given as natural values —
        the API coerces them. Required fields are enforced; unknown or
        non-whitelisted keys are rejected by key. Bulk writes send one
        coalesced notification.
      security: [{ writeToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateEntriesRequest' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateEntriesResponse' }
        '400': { $ref: '#/components/responses/WriteBadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/WriteForbidden' }
  /v1/entries/{entryId}:
    servers:
      - url: https://write.alistia.app
        description: Authenticated write API
    patch:
      tags: [write]
      operationId: updateEntry
      summary: Update an entry
      description: Updates the given fields. Pass `revision` for optimistic concurrency.
      security: [{ writeToken: [] }]
      parameters:
        - { name: entryId, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                values: { type: object, additionalProperties: true }
                revision: { type: integer }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  revision: { type: integer }
        '400': { $ref: '#/components/responses/WriteBadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/WriteForbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Expired' }
    delete:
      tags: [write]
      operationId: deleteEntry
      summary: Delete an entry (soft-delete)
      security: [{ writeToken: [] }]
      parameters:
        - { name: entryId, in: path, required: true, schema: { type: string } }
        - { name: revision, in: query, required: false, schema: { type: integer } }
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  deleted: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/WriteForbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /v1/forms/{token}:
    servers:
      - url: https://forms.alistia.app
        description: Public forms
    get:
      tags: [forms]
      operationId: getFormConfig
      summary: Get a public form's render config
      description: Public — no auth. Returns title, description, fields and success message for rendering.
      parameters:
        - $ref: '#/components/parameters/Token'
      responses:
        '200':
          description: Form config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FormConfig' }
        '403': { $ref: '#/components/responses/Disabled' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Expired' }
  /v1/forms/{token}/submit:
    servers:
      - url: https://forms.alistia.app
        description: Public forms
    post:
      tags: [forms]
      operationId: submitForm
      summary: Submit a form response
      description: >
        Creates a new list entry from the submission. `_hp` is a honeypot (must
        be empty); very fast submissions are silently dropped. Field ids must be
        in the form config; required fields are enforced.
      parameters:
        - $ref: '#/components/parameters/Token'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [values]
              properties:
                values: { type: object, additionalProperties: true }
                _hp: { type: string, description: Honeypot — leave empty. }
                _elapsedMs: { type: integer, description: Milliseconds since the form loaded. }
      responses:
        '200':
          description: Accepted (or silently dropped as spam).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  entryId: { type: string }
                  successMessage: { type: string }
        '400': { $ref: '#/components/responses/WriteBadRequest' }
        '403': { $ref: '#/components/responses/Disabled' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Expired' }
webhooks:
  entryChanged:
    post:
      operationId: onEntryChanged
      summary: Entry change notification
      description: >
        When a webhook is configured for a list in the Alistia app, Alistia
        POSTs this payload to the target URL whenever an entry is created,
        updated or deleted. Headers include `x-alistia-event` and, when a secret
        is set, `x-alistia-signature` (HMAC-SHA256 hex over the raw body). No
        field values are sent — fetch the data link for details. Deliveries are
        retried once; repeated failures disable the webhook.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
            example:
              app: alistia
              event: entry.created
              listId: "66f8...f01"
              entryId: "66f8...a12"
              occurredAt: "2026-07-22T12:18:36.498Z"
      responses:
        '2XX':
          description: Any 2xx acknowledges receipt.
components:
  securitySchemes:
    writeToken:
      type: http
      scheme: bearer
      description: A write token created in the Alistia app (list → Write-API).
  parameters:
    Token:
      name: token
      in: path
      required: true
      description: The public share token (random, at least 16 characters).
      schema:
        type: string
        minLength: 16
        example: AbCdEfGh12345678
  responses:
    BadRequest:
      description: Token missing, empty or malformed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: bad_request, message: "Missing or invalid token." } }
    Disabled:
      description: The share link is disabled (revoked by its owner).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: disabled, message: "This share link is disabled." } }
    JsonDisabled:
      description: The JSON data link is not enabled for this share (Premium feature).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: json_disabled, message: "The JSON data link is not enabled for this share." } }
    NotFound:
      description: No share link matches the token, or no snapshot exists yet.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: not_found, message: "Share link not found." } }
    Expired:
      description: The share link has passed its expiry date.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: expired, message: "This share link has expired." } }
    RateLimited:
      description: Too many requests for this token (reserved for Data Links).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: rate_limited, message: "Too many requests." } }
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: internal_error, message: "Internal error." } }
    Unauthorized:
      description: Missing or invalid write token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: unauthorized, message: "Missing or invalid write token." } }
    WriteForbidden:
      description: Token disabled or lacks the required scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: forbidden, message: "Token lacks create scope." } }
    WriteBadRequest:
      description: Validation error (missing/invalid field, batch too large).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: missing_required_field, message: "Missing required field: field_title" } }
    Conflict:
      description: Revision mismatch (optimistic concurrency).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: conflict, message: "Revision mismatch (current 2)." } }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable, machine-readable error code (part of the contract).
              enum: [bad_request, disabled, json_disabled, not_found, expired, rate_limited, internal_error]
            message:
              type: string
              description: Human-readable message. Not stable — do not switch on it.
    ShareSnapshot:
      type: object
      required: [list, fields, entries]
      description: >
        Lean, machine-readable view. Values are keyed by the stable field key
        (a slug of the field name, e.g. "titel", "laenge_min") — never internal
        ids — and each field carries exactly one canonical, typed value
        (dates as YYYY-MM-DD, numbers as numbers, …). No display/preview data.
      properties:
        list:
          $ref: '#/components/schemas/ListMeta'
        generatedAt:
          type: string
          format: date-time
          description: When this snapshot was generated (ISO-8601 UTC).
        fields:
          type: array
          items: { $ref: '#/components/schemas/Field' }
        entries:
          type: array
          items: { $ref: '#/components/schemas/Entry' }
    ListMeta:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description:
          oneOf:
            - type: string
            - type: "null"
    Field:
      type: object
      required: [key, name, type]
      properties:
        key:
          type: string
          description: >
            Stable public field key (slug of the field name). Use this as the
            key in entry values on read AND write. Does not change when the
            field's display name is edited.
          example: erscheinungsdatum
        name:
          type: string
          description: Human-readable, localized field label.
        type:
          type: string
          description: Field type, e.g. text, longText, number, boolean, date, datetime, select, status, email, phone, link, imageUrl, videoUrl, ean, rating, multiSelect, tags, color.
          example: date
    Entry:
      type: object
      required: [id, values]
      properties:
        id:
          type: string
          description: Opaque entry id — needed to update or delete the entry.
        values:
          type: object
          description: >
            Canonical, typed values keyed by field key. Only shareable,
            whitelisted fields appear. Dates are YYYY-MM-DD, numbers are
            numbers, booleans are booleans, coordinates are {lat, lon, address}.
          additionalProperties: true
        group:
          oneOf: [{ type: string }, { type: "null" }]
          description: >
            Section label when the list is displayed grouped in the app.
            Entries are pre-sorted; consecutive equal labels form one section.
    CreateEntriesRequest:
      description: >
        Values are keyed by field key (see the read view's field.key). Send
        natural values — dates as "2026-08-01", numbers as 4, booleans as true,
        selects as the option text. The API coerces them into storage format;
        you never need to know internal ids or formats. Unknown or non-writable
        keys are rejected with the offending key in the error message.
      oneOf:
        - type: object
          required: [values]
          properties:
            values:
              type: object
              additionalProperties: true
              description: Field key → value.
        - type: object
          required: [entries]
          properties:
            entries:
              type: array
              items:
                type: object
                required: [values]
                properties:
                  values: { type: object, additionalProperties: true, description: Field key → value. }
    CreateEntriesResponse:
      type: object
      properties:
        ok: { type: boolean }
        count: { type: integer }
        created:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              revision: { type: integer }
    FormConfig:
      type: object
      required: [title, fields]
      properties:
        title: { type: string }
        description: { oneOf: [{ type: string }, { type: "null" }] }
        successMessage: { type: string }
        redirectUrl:
          oneOf: [{ type: string }, { type: "null" }]
          description: If set, the page redirects here after a successful submission.
        privacyUrl:
          oneOf: [{ type: string }, { type: "null" }]
          description: If set, the page requires consent to this privacy policy before submitting.
        fields:
          type: array
          items:
            type: object
            required: [fieldId, label, type, required]
            properties:
              fieldId: { type: string }
              label: { type: string }
              type: { type: string, description: "text, longText, number, boolean, date, time, datetime, select, status, email, phone, link, imageUrl, videoUrl, ean, rating, multiSelect, tags, color" }
              required: { type: boolean }
              placeholder: { oneOf: [{ type: string }, { type: "null" }] }
              helpText: { oneOf: [{ type: string }, { type: "null" }] }
              options: { type: array, items: { type: string } }
    WebhookEvent:
      type: object
      required: [app, event, listId, occurredAt]
      properties:
        app:
          type: string
          const: alistia
        event:
          type: string
          description: >
            Per-entry events for immediate webhooks; `entries.changed` for
            coalescing webhooks (one batched delivery after a burst of changes).
          enum: [entry.created, entry.updated, entry.deleted, entries.changed]
        listId:
          type: string
          description: The list the change belongs to.
        entryId:
          type: string
          description: The changed entry. Present on per-entry events, absent on entries.changed.
        since:
          type: string
          format: date-time
          description: On entries.changed — when the batched window started. Re-fetch the data link for details.
        occurredAt:
          type: string
          format: date-time
  examples:
    TravelSnapshot:
      summary: A shared travel-planning list (.json)
      value:
        list:
          name: "Mallorca Trip"
          description: "Everything for the summer holiday"
        generatedAt: "2026-07-22T12:00:00.000Z"
        fields:
          - { key: aufgabe, name: "Aufgabe", type: text }
          - { key: datum, name: "Datum", type: date }
        entries:
          - id: entry_1
            values:
              aufgabe: "Mietwagen buchen"
              datum: "2026-08-01"
