# PayPerFax OpenAPI
#
# This file is a copy of the OpenAPI spec from the payperfax-core repo,
# production branch, commit 0ef4ebd (API version 1.5.0). Core is the source of truth; re-copy
# when core changes the API.
#
openapi: 3.0.3
info:
  title: PayPerFax API
  version: 1.5.0
  license:
    name: Proprietary
    url: https://payperfax.com/policy/terms-of-service/
  description: |
    Send a fax through PayPerFax, and check its delivery status.

    The API is for a person who works with an AI agent. The agent sends the
    fax with `POST /api/fax` and gets a link. The person opens the link, sees
    the preview with the price, and pays in the browser. The agent can then
    read the status of the fax.

    No authentication. It is not for a system that embeds faxing: it has no
    accounts, no webhooks and no way to pay without a person.

    The same service is an MCP server at `https://fax.payperfax.com/mcp`,
    with the tools `create_fax`, `get_fax_status` and
    `list_supported_formats`.
  contact:
    name: PayPerFax Support
    url: https://payperfax.com
    email: hi@payperfax.com

servers:
  - url: https://fax.payperfax.com
    description: Production

paths:
  /api/fax:
    post:
      operationId: createFax
      summary: Send a fax (the person pays through a link)
      description: |
        Takes the fax fields and returns a link for the person. Nothing is
        sent and nothing is charged before the person pays in the browser.

        - A complete fax (a file, a letter as inline `markdown` or a cover
          page, and no `file_expected`)
          makes a draft. The answer is `201`, and the link opens the preview
          with the price. A draft that is not paid is abandoned after 4 hours.
        - A fax that still needs a file (nothing to send yet: no file, no
          letter and no cover page; or `file_expected: true`)
          makes no fax yet. The answer is `200`, and the link opens the web
          form with what was sent. The person adds the file there. The link
          is good for 24 hours.
      security: []  # Open access - no authentication required
      tags:
        - Send a Fax
      requestBody:
        required: true
        description: |
          Send JSON when the document is inline Markdown, or when the fax is
          a cover page alone. Send `multipart/form-data` for files.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewFax'
            examples:
              letter:
                summary: A letter that the agent wrote
                value:
                  to: "+14155550101"
                  email: person@example.com
                  markdown: "# Request for records\n\nDear Sir or Madam,\n\nPlease send me a copy of my records.\n\nJane Doe"
              cover_page_alone:
                summary: A cover page alone
                value:
                  to: "+14155550101"
                  email: person@example.com
                  cover_page: true
                  cover_message: "Please call me about my account."
                  sender_name: Jane Doe
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NewFax'
      responses:
        '200':
          description: |
            No fax is made yet: the person must add a file. The link opens
            the web form with what was sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormLink'
        '201':
          description: The draft is made
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedFax'
        '422':
          description: |
            A field is wrong or not known, or the body cannot be read (for
            example a raw line break in a JSON string). The messages say how
            to fix the call.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '429':
          description: |
            Too many calls from one address in an hour. The header
            `Retry-After` has the seconds to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "Too Many Attempts."
        '503':
          description: New orders are paused for a short time.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "New orders are paused. Try again later."

  /api/fax/{transaction}/status:
    get:
      operationId: getFaxStatus
      summary: Get fax delivery status
      description: |
        Returns the current status of a fax transaction. Poll this endpoint
        to track a fax from submission through to final delivery or failure.

        **Polling recommendations, by `stage`:**
        - `rendering`: read again after a few seconds
        - `awaiting_payment`: do not poll. A person must pay, and a draft is
          kept for 4 hours. Read again when the person says the payment is done
        - `sending`: read every 15 seconds. Typical fax delivery takes 1-2
          minutes per page
        - Stop once `is_terminal` is `true`
      security: []  # Open access - no authentication required
      tags:
        - Fax Status
      parameters:
        - name: transaction
          in: path
          required: true
          description: |
            The unique transaction identifier (UUID format) provided when
            the fax was submitted. This ID is included in confirmation
            emails and webhook payloads.
          schema:
            type: string
            format: uuid
            example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
      responses:
        '200':
          description: Fax status retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FaxStatus'
              examples:
                rendering:
                  summary: Not paid yet. The documents are being prepared
                  value:
                    status: pending
                    is_terminal: false
                    is_failed: false
                    duration: ""
                    remoteCSID: ""
                    processing: true
                    stage: rendering
                awaiting_payment:
                  summary: Ready and priced. The customer has not paid yet
                  value:
                    status: pending
                    is_terminal: false
                    is_failed: false
                    duration: ""
                    remoteCSID: ""
                    ready: true
                    pages: 3
                    price:
                      cents: 200
                      currency: USD
                      formatted: "$2.00"
                    price_unavailable: false
                    tentative: true
                    stage: awaiting_payment
                pending:
                  summary: Paid. The fax is being transmitted
                  value:
                    status: pending
                    is_terminal: false
                    is_failed: false
                    duration: ""
                    remoteCSID: ""
                    submitted_at: "2025-12-21T16:06:38+00:00"
                    completed_at: null
                    pages_sent: null
                    stage: sending
                delivered:
                  summary: Fax delivered successfully
                  value:
                    status: delivered
                    is_terminal: true
                    is_failed: false
                    duration: "85"
                    remoteCSID: "IRS TREASURY LINE 03"
                    submitted_at: "2025-12-21T16:06:38+00:00"
                    completed_at: "2025-12-21T16:08:03+00:00"
                    pages_sent: 2
                failed:
                  summary: Fax could not be delivered
                  value:
                    status: failed
                    is_terminal: true
                    is_failed: true
                    duration: ""
                    remoteCSID: ""
                    submitted_at: "2025-12-21T16:06:38+00:00"
                    completed_at: "2025-12-21T16:15:55+00:00"
                    pages_sent: 0
                    failure:
                      code: "undeliverable"
                      message: "InterFAX error code: 3931"
                expired:
                  summary: Fax data no longer available
                  value:
                    status: expired
                    is_terminal: true
                    is_failed: false
                    message: "This fax is no longer available."
        '404':
          description: |
            No fax has this id. The body is JSON also when the request has no
            `Accept` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "Not found."

components:
  schemas:
    FaxStatus:
      type: object
      required:
        - status
        - is_terminal
        - is_failed
      properties:
        status:
          type: string
          enum:
            - pending
            - delivered
            - failed
            - expired
          description: |
            The current status of the fax:
            - `pending`: Fax is queued or being transmitted to the destination
            - `delivered`: Fax was successfully received by the destination fax machine
            - `failed`: Fax could not be delivered after all retry attempts
            - `expired`: Fax data has been deleted (retained for 30 days after submission)
        is_terminal:
          type: boolean
          description: |
            Whether the fax has reached a final state. When `true`, the status
            will not change and you can stop polling. Terminal states are:
            `delivered`, `failed`, and `expired`. One exception: a fax with
            `render_failed` is terminal, but the customer can change the
            document and submit it again; the status is then `pending` again.
        is_failed:
          type: boolean
          description: |
            Whether the fax failed to deliver. This is `true` only when the
            status is `failed`. Use this for quick conditional checks without
            comparing status strings.
        stage:
          type: string
          enum:
            - rendering
            - awaiting_payment
            - sending
          description: |
            Where a pending fax is. Present only while `status` is `pending`:
            - `rendering`: the documents are being prepared
            - `awaiting_payment`: the fax is ready and priced, and the customer has not paid yet
            - `sending`: paid, and the fax is being sent
        processing:
          type: boolean
          description: |
            `true` while the documents of an unpaid fax are being prepared.
        ready:
          type: boolean
          description: |
            `true` when an unpaid fax is prepared and priced. `pages` and
            `price` come with it.
        pages:
          type: integer
          minimum: 1
          description: |
            The number of pages of the prepared fax. Present with `ready`.
          example: 3
        price:
          type: object
          nullable: true
          description: |
            The price of the prepared fax. Present with `ready`. Null when no
            price is configured for the currency (see `price_unavailable`).
          properties:
            cents:
              type: integer
              description: The amount in the smallest unit of the currency.
              example: 200
            currency:
              type: string
              description: ISO 4217 currency code.
              example: USD
            formatted:
              type: string
              description: The price as text, for display.
              example: "$2.00"
        price_unavailable:
          type: boolean
          description: |
            `true` when the price could not be computed. Present with `ready`.
            The customer cannot pay until a price is available.
        tentative:
          type: boolean
          description: |
            `true` when `pages` and `price` are not final: the customer can
            still change the fax before payment. Present with `ready`.
        render_failed:
          type: string
          enum:
            - document
            - machinery
          description: |
            Present when the documents of an unpaid fax could not be prepared.
            The status is then `failed`, with `is_failed` false: nothing was
            sent and nothing was charged. The customer can change the document
            and submit the same fax again.
        duration:
          type: string
          description: |
            The duration of the successful fax transmission in seconds.
            Empty string if the fax hasn't completed or failed.
          example: "85"
        remoteCSID:
          type: string
          description: |
            The Called Subscriber Identification (CSID) of the receiving fax
            machine. This is typically the fax number or company name configured
            on the recipient's machine. Empty if not available or fax failed.
          example: "IRS TREASURY LINE 03"
        submitted_at:
          type: string
          format: date-time
          nullable: true
          description: |
            ISO 8601 timestamp of when the fax was submitted to the carrier
            for transmission. Null if the fax hasn't been submitted yet.
          example: "2025-12-21T16:06:38+00:00"
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: |
            ISO 8601 timestamp of when the fax transmission completed
            (successfully or unsuccessfully). Null while the fax is still
            in progress.
          example: "2025-12-21T16:08:03+00:00"
        pages_sent:
          type: integer
          nullable: true
          minimum: 0
          description: |
            The number of pages successfully transmitted. For a successful
            delivery, this should match the total pages in the document.
            Null until the carrier reports it. May be 0 if the fax failed
            before any pages were sent.
          example: 2
        failure:
          $ref: '#/components/schemas/FailureInfo'
        message:
          type: string
          description: |
            Human-readable message. Currently only used for expired faxes
            to explain that the data is no longer available.
          example: "This fax is no longer available."

    FailureInfo:
      type: object
      description: |
        Details about why a fax failed to deliver. Only present when
        `is_failed` is `true`.
      properties:
        code:
          type: string
          description: |
            Machine-readable failure code:
            - `undeliverable`: the destination did not take the fax (busy,
              no answer, not a fax machine)
            - `error`: the fax could not be sent
            - `given_up`: no final answer from the carrier in time
          enum:
            - undeliverable
            - error
            - given_up
          example: "undeliverable"
        message:
          type: string
          description: |
            Human-readable explanation of why the fax failed. For
            `undeliverable` it is "InterFAX error code: N" when the carrier
            gave a code; otherwise "The fax could not be sent."
          example: "InterFAX error code: 3931"

    Error:
      type: object
      properties:
        message:
          type: string
          description: Human-readable error message
          example: "Not found."

    NewFax:
      type: object
      required:
        - to
      properties:
        to:
          type: string
          description: |
            The fax number in international format: `+`, the country code,
            the number.
          example: "+14155550101"
        email:
          type: string
          format: email
          description: |
            The person's email address. The confirmation and the receipt go
            there. Ask the person; do not guess. Required when a draft is
            made; optional when the answer is a link to the form.
          example: person@example.com
        documents[]:
          type: array
          description: |
            The files of the fax, in the order they are sent: PDF, DOC, DOCX,
            JPG, PNG, TIFF or Markdown (`.md`, 100 KB). At most 10 MB for all
            files together; the inline `markdown` text counts. Without a file,
            a letter or a cover page, the answer is a link to the form.
          items:
            type: string
            format: binary
        markdown:
          type: string
          description: |
            The document as inline Markdown text, when the agent wrote it.
            UTF-8, not empty, at most 100 KB. It becomes the first document
            of the fax, a file named `letter.md`. Headings, lists and tables
            work; raw HTML is printed as text. Do not retype a person's file
            as text.
          example: "# Request for records\n\nDear Sir or Madam,\n\nPlease send me a copy of my records."
        file_expected:
          type: boolean
          default: false
          description: |
            `true` when the person must still add a file. No fax is made
            yet: the answer is a link to the web form, with what was sent.
        cover_page:
          type: boolean
          default: false
          description: Adds a cover page.
        cover_message:
          type: string
          maxLength: 5000
          description: |
            Plain text for the cover page. Line breaks are kept. Keep it
            short: a long message is cut off. It needs `cover_page: true`:
            without a cover page the call is refused, so no message is lost.
        sender_name:
          type: string
          maxLength: 100
          description: For the cover page.
        recipient_name:
          type: string
          maxLength: 100
          description: For the cover page.
        recipient_company:
          type: string
          maxLength: 100
          description: For the cover page.
        language:
          type: string
          enum: [en, es, de, ja, ko, fr]
          default: en
          description: The language of the pages and emails for the person.
        timezone:
          type: string
          description: An IANA time zone name, for the times in the emails.
          example: America/New_York
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: |
            The person's country as a two-letter code (ISO 3166-1 alpha-2),
            for the currency of the price and for the date format on the
            cover page and in the emails. Without it, the country of the
            caller's address is used.
          example: DE

    CreatedFax:
      type: object
      required:
        - id
        - url
        - status_url
      properties:
        id:
          type: string
          format: uuid
          description: The id of the fax.
          example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
        url:
          type: string
          format: uri
          description: |
            The link for the person: the preview with the price, then the
            payment. Good for 4 hours.
          example: https://fax.payperfax.com/fax/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/continue
        status_url:
          type: string
          format: uri
          description: The status call for this fax.
          example: https://fax.payperfax.com/api/fax/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/status

    FormLink:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: |
            The link for the person: the web form, with what was sent. The
            person adds the file there. Good for 24 hours.
          example: https://fax.payperfax.com/fax/start/Jq3kV0c2mXb7RtY9pLs4HnW1eZa6UdG8fQi5oKjT

    ValidationError:
      type: object
      properties:
        message:
          type: string
          example: "Give the fax number in international format: \"+\", the country code, the number. Example: +14155550101. In a urlencoded body, write the \"+\" as %2B."
        errors:
          type: object
          description: The messages for each field that is wrong.
          additionalProperties:
            type: array
            items:
              type: string

tags:
  - name: Send a Fax
    description: |
      The call that takes a fax and returns a payment link for the person.
  - name: Fax Status
    description: |
      Endpoints for checking fax delivery status. Use these to track
      the progress of faxes sent through PayPerFax.
