> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylon.mortgage/llms.txt
> Use this file to discover all available pages before exploring further.

# Support tickets

> File and track loan-specific support tickets with Pylon's support team over GraphQL — including attachments and threaded replies

The Support API lets your loan officers and processors file support tickets about a specific loan directly from your own tooling, instead of emailing or messaging the Pylon team. Tickets are created in Pylon's support platform (Plain), where the support team triages and replies — and your integration can read the ticket's status and full conversation back over the same API.

Everything on this page lives under the `support` GraphQL namespaces:

* `support` **mutation** — file tickets, add messages, request attachment uploads. Authorized with the `create:support-ticket` scope.
* `support` **query** — discover issue types, read tickets and their conversations, list tickets. Authorized with the `read:support-ticket` scope.

<Note>
  The Support API is **experimental**: both namespaces additionally require the `use:experimental-api` scope, and their shape may change.
</Note>

## Discovering issue types

Every ticket is filed under an **issue type** — a stable key that routes the ticket to the right team. Query `issueTypes` to list the ones available:

```graphql theme={null}
query {
  support {
    issueTypes {
      id
      displayName
    }
  }
}
```

The `id` (e.g. `PRE_LOCK`) is what you pass as `issueType` when filing a ticket. Issue types can change over time, so discover them at runtime rather than hard-coding the list.

<Warning>
  To request a change to a loan's terms (a change of circumstance), use [`loan.openChangeRequest`](/guides/getting-started/change-requests) rather than filing a ticket under a change-of-circumstance issue type. `openChangeRequest` files the support ticket for you and also records the change request on the loan; it returns the ticket ID as `plainTicketId`, which you then read back with the queries below.
</Warning>

## Filing a ticket

Use the `createSupportTicket` mutation. A ticket is always about one loan, and carries the identity of the person filing it (the loan officer or processor) so support replies reach the right person:

```graphql theme={null}
mutation FileTicket($input: CreateSupportTicketInput!) {
  support {
    createSupportTicket(input: $input) {
      ticket {
        id
        reference
        status
        title
        loanApplicationId
      }
    }
  }
}
```

Example variables:

```json theme={null}
{
  "input": {
    "loanApplicationId": "app_XXXXXXXX",
    "issueType": "PRE_LOCK",
    "requesterName": "Jane Doe",
    "requesterEmail": "jane.doe@yourcompany.com",
    "body": "The borrower's lock expires Friday — can we extend by 5 days?",
    "additionalRecipients": [
      { "email": "sam.processor@yourcompany.com", "name": "Sam Processor" }
    ]
  }
}
```

### Input fields

| Field                   | Required | Description                                                                                                                            |
| ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `loanApplicationId`     | Yes      | The loan the ticket is about — its Pylon ID or friendly loan ID.                                                                       |
| `issueType`             | Yes      | An issue type key from the [`issueTypes` query](#discovering-issue-types).                                                             |
| `requesterName`         | Yes      | Name of the person filing the ticket.                                                                                                  |
| `requesterEmail`        | Yes      | Email of the person filing the ticket.                                                                                                 |
| `body`                  | Yes      | The question or issue description.                                                                                                     |
| `attachmentDocumentIds` | No       | Documents to attach — see [Attachments](#attachments). At most 20 per message.                                                         |
| `additionalRecipients`  | No       | Extra addresses to copy on the ticket's opening message; at most 10. Later messages carry the recipients given to `addSupportMessage`. |

The response's `ticket.id` is the ticket's unique identifier (the underlying support thread ID) — hold on to it to read the ticket back or add follow-up messages. `reference` is the human-readable ticket number (e.g. `T-1234`) the support team will also use.

<Note>
  Additional recipients are CC'd on the email for that message only — each message carries its own list, and a later message does not inherit an earlier one's. Each entry is an `email` and an optional `name` shown on the email, the address must be a valid one, and at most 10 are accepted per message.
</Note>

## Attachments

Tickets and messages can carry file attachments — a rate-sheet screenshot, a borrower document, a PDF. There are two ways to reference a document in `attachmentDocumentIds`:

1. **A document already on the loan** — pass its document ID.
2. **A new file** — upload it through the two-step flow below, then pass the returned document ID.

### Uploading a new attachment

First, request an upload slot with the `requestSupportDocumentUpload` mutation:

```graphql theme={null}
mutation RequestUpload($input: RequestSupportDocumentUploadInput!) {
  support {
    requestSupportDocumentUpload(input: $input) {
      upload {
        uploadUrl
        expiresAt
      }
    }
  }
}
```

```json theme={null}
{
  "input": {
    "loanApplicationId": "app_XXXXXXXX",
    "fileName": "rate-sheet.pdf"
  }
}
```

Then POST the file to the returned `uploadUrl` as `multipart/form-data` with a single `files` field, authenticated the same way as your GraphQL requests:

```javascript theme={null}
async function uploadSupportDocument(uploadUrl, file) {
  const formData = new FormData();
  formData.append("files", file);

  const response = await fetch(uploadUrl, {
    method: "POST",
    headers: { Authorization: `Bearer ${accessToken}` },
    body: formData,
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.statusText}`);
  }

  const { documentId } = await response.json();
  return documentId;
}
```

The response contains the stored document's ID, which you pass in `attachmentDocumentIds` when filing a ticket or adding a message.

Attached documents are posted on the ticket as an internal note that the support team sees inline in the thread. The email copy of the message that the requester and any `additionalRecipients` receive contains the text only, not the files.

<Note>
  Attachment constraints:

  * Exactly one file per upload slot; request a new slot for each file.
  * Files must be between 1 byte and 50 MB. Larger files are rejected with a `400` and `errorDetails.code` of `SUPPORT_ATTACHMENT_TOO_LARGE`.
  * Executable and script file types (e.g. `.exe`, `.js`, `.sh`, `.ps1`) are rejected.
  * At most 20 attachments per ticket or message.
  * An upload URL expires 24 hours after it is requested (see `expiresAt`); request a new one if it lapses. Once uploaded, the document is stored on the loan and can be attached to tickets and messages any number of times.
</Note>

## Adding a message to a ticket

Follow up on an existing ticket with the `addSupportMessage` mutation:

```graphql theme={null}
mutation Reply($input: AddSupportMessageInput!) {
  support {
    addSupportMessage(input: $input) {
      ticketId
    }
  }
}
```

`additionalRecipients` works the same way here, copying people on this reply only.

```json theme={null}
{
  "input": {
    "ticketId": "<ticket-id>",
    "requesterName": "Jane Doe",
    "requesterEmail": "jane.doe@yourcompany.com",
    "body": "Adding the borrower's updated payoff statement.",
    "attachmentDocumentIds": ["<document-id>"],
    "additionalRecipients": [
      { "email": "sam.processor@yourcompany.com", "name": "Sam Processor" }
    ]
  }
}
```

### Resolved tickets are locked

Once the support team resolves a ticket (its `status` becomes `DONE`), the support platform locks the thread and it no longer accepts replies. Calling `addSupportMessage` on a locked ticket fails without sending anything:

```json theme={null}
{
  "errors": [
    {
      "message": "This ticket has been locked by the support desk and no longer accepts replies; open a new ticket instead",
      "extensions": {
        "code": "CONFLICT",
        "errorId": "error_xyz789",
        "errorDetails": {
          "code": "SUPPORT_TICKET_LOCKED"
        }
      }
    }
  ]
}
```

Check `extensions.errorDetails.code` for `SUPPORT_TICKET_LOCKED` and do not retry: the ticket will stay locked. File a new ticket for the loan with `createSupportTicket` instead, and mention the earlier ticket's `reference` (e.g. `T-1234`) in the body so the support team can connect the two. To avoid the round trip, read the ticket's `status` first and hide the reply action in your UI when it is `DONE`. See [Error handling](/playground/error-handling) for the general error format.

## Reading tickets back

### A single ticket

```graphql theme={null}
query Ticket($id: ID!) {
  support {
    supportTicket(id: $id) {
      id
      reference
      status
      title
      loanApplicationId
    }
  }
}
```

`status` reflects the ticket's current state in the support platform (e.g. whether it is waiting on support, snoozed, or done). A ticket with `status` of `DONE` has been resolved and [no longer accepts replies](#resolved-tickets-are-locked). This is also how you track a [change request](/guides/getting-started/change-requests#tracking-a-change-request): pass its `plainTicketId` here.

### A ticket's conversation

`supportTicketMessages` returns the full thread, oldest first — both the messages filed through this API (`author: REQUESTER`) and the support team's replies (`author: SUPPORT`):

```graphql theme={null}
query Messages($ticketId: ID!) {
  support {
    supportTicketMessages(ticketId: $ticketId) {
      id
      author
      authorName
      authorEmail
      text
      sentAt
      additionalRecipients {
        email
        name
      }
    }
  }
}
```

`additionalRecipients` on a message is who was copied on it, so your UI can show the recipients alongside the sender.

Poll this query to surface support replies in your own UI.

### All tickets for a loan

```graphql theme={null}
query LoanTickets($loanApplicationId: ID!) {
  support {
    supportTicketsForLoan(loanApplicationId: $loanApplicationId) {
      id
      reference
      status
      title
    }
  }
}
```

### All of your organization's tickets

`supportTickets` is a paginated connection ordered by creation time, newest first by default. Pages are capped at 50 tickets:

```graphql theme={null}
query AllTickets($first: Int, $after: String) {
  support {
    supportTickets(first: $first, after: $after) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        node {
          id
          reference
          status
          title
          loanApplicationId
        }
      }
    }
  }
}
```

## Looking up the loan's Pylon team

Sometimes the right move isn't a ticket but a conversation with the people working the loan. The `pylonTeam` query returns the loan's assigned team with contact details and booking links where available:

```graphql theme={null}
query Team($loanApplicationId: ID!) {
  support {
    pylonTeam(loanApplicationId: $loanApplicationId) {
      accountManager {
        name
        email
        schedulingUrl
      }
      members {
        name
        email
        nmlsId
        roles
        schedulingUrl
      }
      underwriter {
        schedulingUrl
      }
    }
  }
}
```

| Field            | Description                                                                                                                                                                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accountManager` | The primary account manager, or `null` when one can't be determined unambiguously.                                                                                                                                                          |
| `members`        | Every assigned team member (account managers, processor, closer, ...). Underwriters are never listed here. The full list is exposed per organization — until it is enabled for yours, `members` is empty.                                   |
| `underwriter`    | An anonymous scheduling handle for the loan's underwriter: only a `schedulingUrl` (booking link), never a name or email. `null` when no underwriter is assigned; `{ "schedulingUrl": null }` when one is assigned but has no bookable link. |

## Operations reference

All operations also require `use:experimental-api`.

| Operation                      | Type     | Scope                   | Description                                 |
| ------------------------------ | -------- | ----------------------- | ------------------------------------------- |
| `createSupportTicket`          | Mutation | `create:support-ticket` | File a loan-specific ticket.                |
| `addSupportMessage`            | Mutation | `create:support-ticket` | Add a message to an existing ticket.        |
| `requestSupportDocumentUpload` | Mutation | `create:support-ticket` | Get a URL to upload an attachment to.       |
| `issueTypes`                   | Query    | `read:support-ticket`   | List the issue types available when filing. |
| `supportTicket`                | Query    | `read:support-ticket`   | Read a single ticket by ID.                 |
| `supportTicketMessages`        | Query    | `read:support-ticket`   | Read a ticket's conversation, oldest first. |
| `supportTicketsForLoan`        | Query    | `read:support-ticket`   | All tickets for a loan.                     |
| `supportTickets`               | Query    | `read:support-ticket`   | Paginated list of all your tickets.         |
| `pylonTeam`                    | Query    | `read:support-ticket`   | The loan's assigned Pylon team.             |
