openapi: 3.1.0
info:
  title: Send to Nifty API
  version: "1"
  description: |
    Send tasks, notes, events, documents and emails to someone's Nifty (a Nifty Plus feature). Each item is checked,
    encrypted for their copy of Nifty as soon as it arrives, and waits up to 7 days until Nifty collects it (every 2
    minutes while it's running). Nifty adds it under the `id` this API answers with.

    You can't give ids for lists, projects, people and so on: give their names, and Nifty matches them when it
    collects the item. Names are matched ignoring case. A name that matches nothing (or, for a person, more than one
    person) is left out, and the item goes in the usual place: Unsorted, no category, no list. Nothing is created,
    except a note's new tags.

    Errors are `{"error": {"code", "message", "details"?}}`; `details` names each field's problems on a `422`.
    The guide: https://help.thenifty.app/api/send-to-nifty/
servers:
  - url: https://api.nifty.fyi
security:
  - token: []
paths:
  /v1/tasks:
    post:
      operationId: sendTask
      summary: Send a task
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Task" }
            example: { title: "Renew passport", deadline_on: "2026-11-30", list: "Admin", assignee: "ada@example.com", source: "Zapier" }
      responses: &deposit_responses
        "202":
          description: Accepted, and waiting for Nifty to collect it.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deposit" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "411": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "415": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
        "507": { $ref: "#/components/responses/Error" }
  /v1/notes:
    post:
      operationId: sendNote
      summary: Send a note
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Note" }
            example: { title: "Boiler", body: "Booked for **Tuesday**.\n\n- [ ] Clear the cupboard", category: "Home", tags: ["house"] }
      responses: *deposit_responses
  /v1/events:
    post:
      operationId: sendEvent
      summary: Send an event
      description: It's added to My Nifty Calendar. An event is one day, all day or from a start to an end time.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
            example: { title: "Dentist", date: "2026-11-04", start_time: "09:30", end_time: "10:15", time_zone: "Europe/London" }
      responses: *deposit_responses
  /v1/documents:
    post:
      operationId: sendDocument
      summary: Send a document or an image
      description: A PDF, or a PNG, JPEG or WebP image of at least 200 × 200 pixels and at most 40 megapixels (not HEIC).
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [ file ]
              properties:
                file: { type: string, format: binary, description: "Up to 20 MB." }
                metadata:
                  allOf: [ { $ref: "#/components/schemas/Document" } ]
                  unevaluatedProperties: false
            encoding:
              metadata: { contentType: application/json }
          application/json:
            schema:
              description: The file by its link, with the details beside it.
              allOf:
                - { $ref: "#/components/schemas/Document" }
                - { $ref: "#/components/schemas/FileUrl" }
              unevaluatedProperties: false
      responses: *deposit_responses
  /v1/emails:
    post:
      operationId: sendEmail
      summary: Send an email
      description: An email message (`.eml`), added as an email dropped into Nifty is, with any PDFs it has attached.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [ file ]
              properties:
                file: { type: string, format: binary, description: "Up to 20 MB." }
                metadata:
                  allOf: [ { $ref: "#/components/schemas/Email" } ]
                  unevaluatedProperties: false
            encoding:
              metadata: { contentType: application/json }
          application/json:
            schema:
              description: The file by its link, with the details beside it.
              allOf:
                - { $ref: "#/components/schemas/Email" }
                - { $ref: "#/components/schemas/FileUrl" }
              unevaluatedProperties: false
      responses: *deposit_responses
  /v1/token:
    get:
      operationId: checkToken
      summary: Check a token
      description: Says whether the token works and items can be sent with it, without sending anything.
      responses:
        "200":
          description: The token works, and items can be sent.
          content:
            application/json:
              schema:
                type: object
                required: [ id, name, ending, created_at ]
                properties:
                  id: { type: string }
                  name: { type: string, examples: [ "Zapier" ] }
                  ending: { type: string, description: The token's last four characters. }
                  created_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /v1/deposits/{id}:
    get:
      operationId: getDeposit
      summary: Ask what became of an item
      description: Items are remembered for 30 days after they're collected or expire.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: What became of it.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deposit" }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
components:
  securitySchemes:
    token:
      type: http
      scheme: bearer
      description: "A token from Nifty: Settings → Connections → Send to Nifty API → New token (`nsk_…`). Make one for each app."
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: |
        1–255 visible ASCII characters, such as a UUID. Sending the same request with the same key within 7 days answers
        with the first item (and `Idempotent-Replayed: true`) rather than sending another; the same key with a different
        request is a `422 idempotency_key_reused`.
      schema: { type: string, minLength: 1, maxLength: 255 }
  responses:
    Error:
      description: |
        `400 invalid_json` / `invalid_body` / `invalid_idempotency_key`; `401 invalid_token`; `403 not_entitled` (the
        Nifty Plus subscription isn't active); `404 not_found`; `409 not_ready` (Nifty hasn't been opened since connecting);
        `413 too_large`; `415 unsupported_media_type`; `422 invalid` (with `details`) or `idempotency_key_reused`;
        `429 rate_limited` (60 requests a minute per token, 300 per account; see `Retry-After`); `502 file_url_unreachable`
        (the file's server failed or didn't answer within 30 seconds: try again later); `507 full` (470 API items, or 1 GB
        of them, are waiting: Nifty needs to collect them first); `411 length_required` (a form without a Content-Length).
      content:
        application/json:
          schema:
            type: object
            required: [ error ]
            properties:
              error:
                type: object
                required: [ code, message ]
                properties:
                  code: { type: string }
                  message: { type: string }
                  details: { type: object, additionalProperties: { type: array, items: { type: string } } }
  schemas:
    Deposit:
      type: object
      required: [ id, kind, status, received_at ]
      properties:
        id: { type: string, description: "The item's id, which it keeps in Nifty: a lower-case ULID.", examples: [ "01k7z9d3f8g2h4j6k8m0n2p4q6" ] }
        kind: { type: string, enum: [ task, note, event, document, email ] }
        status:
          type: string
          enum: [ waiting, added, skipped, expired ]
          description: "`waiting` to be collected; `added`; `skipped`: Nifty couldn't add it (it didn't pass Nifty's checks, or was sent before Nifty Plus moved to another copy of Nifty); `expired`: not collected within 7 days."
        received_at: { type: string, format: date-time }
    FileUrl:
      type: object
      required: [ file_url ]
      properties:
        file_url:
          type: string
          format: uri
          description: |
            An https link straight to the file, which Nifty Plus downloads (up to 20 MB, 30 seconds, 3 redirects). A link
            to an IP address, localhost or Nifty's own addresses, or to a web page rather than a file, is a `422`. The
            file is named from Content-Disposition, else the link.
    Source:
      type: string
      maxLength: 50
      description: Your app's name, shown in the item's details ("Sent to Nifty by Zapier"). Without one, the token's name is used.
    Names:
      type: array
      maxItems: 20
      items: { type: string }
    Task:
      type: object
      required: [ title ]
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 200 }
        description: { type: string, description: "Markdown: GitHub's, with `- [ ]` lists as checklists. Raw HTML is kept as text; over 32 KB, it's all read as plain text." }
        start_on: { type: string, format: date }
        deadline_on: { type: string, format: date }
        expires_on: { type: string, format: date, description: "On or after the start and the deadline. A task whose expiry passes before it's collected is added as expired." }
        list: { type: string, maxLength: 100, description: "A task list's name. It goes there, else in the project, else in neither." }
        project: { type: string, maxLength: 100, description: "A project's name." }
        assignee: { type: string, maxLength: 254, description: "Who it's delegated to: an email address, name or alias of a person in Nifty." }
        source: { $ref: "#/components/schemas/Source" }
    Note:
      type: object
      additionalProperties: false
      description: A title, a body, or both.
      properties:
        title: { type: string, maxLength: 200 }
        body: { type: string, description: "Markdown (over 32 KB, read as plain text), or plain text with `format: text`. Up to 256 KB with the rest." }
        format: { type: string, enum: [ markdown, text ], default: markdown }
        category: { type: string, maxLength: 50 }
        tags:
          allOf: [ { $ref: "#/components/schemas/Names" } ]
          description: "Tags' names (no commas): found, or made if they're new."
        people:
          allOf: [ { $ref: "#/components/schemas/Names" } ]
          description: "People's email addresses, names or aliases, mentioned at the end of the note."
        projects:
          allOf: [ { $ref: "#/components/schemas/Names" } ]
          description: "Projects' names, referenced at the end of the note."
        source: { $ref: "#/components/schemas/Source" }
    Event:
      type: object
      required: [ title, date ]
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 200 }
        date: { type: string, format: date }
        all_day: { type: boolean, description: "Without it, an event with a start_time is timed and one without is all day." }
        start_time: { type: string, pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$", description: "HH:MM, for a timed event (then end_time is required too). Left out of an all-day one." }
        end_time: { type: string, pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$", description: "HH:MM, after start_time on the same day." }
        time_zone: { type: string, description: "An IANA zone's name (Europe/London), not an offset; the Nifty owner's own without it. Left out of an all-day event." }
        location: { type: string, maxLength: 200 }
        notes: { type: string, maxLength: 4000, description: Plain text. }
        source: { $ref: "#/components/schemas/Source" }
    Document:
      type: object
      description: Its details. Where it's used, no other fields are allowed.
      properties:
        name: { type: string, maxLength: 200, description: "Its name in Nifty; the file's name without its extension otherwise." }
        folder: { type: string, maxLength: 1000, description: "A folder's name, or a path of them (Bills/Energy)." }
        dated_on: { type: string, format: date, description: "The document's own date (a bill's, say)." }
        people: { allOf: [ { $ref: "#/components/schemas/Names" } ], description: "People's email addresses, names or aliases, linked to it." }
        projects: { allOf: [ { $ref: "#/components/schemas/Names" } ], description: "Projects' names, linked to it." }
        source: { $ref: "#/components/schemas/Source" }
    Email:
      type: object
      description: Its details. Where it's used, no other fields are allowed.
      properties:
        folder: { type: string, maxLength: 1000, description: "A folder's name, or a path of them (Bills/Energy)." }
        people: { allOf: [ { $ref: "#/components/schemas/Names" } ], description: "People's email addresses, names or aliases, linked to it." }
        projects: { allOf: [ { $ref: "#/components/schemas/Names" } ], description: "Projects' names, linked to it." }
        source: { $ref: "#/components/schemas/Source" }
