> For the complete documentation index, see [llms.txt](https://docs.usepylon.com/pylon-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.usepylon.com/pylon-docs/developer/custom-apps.md).

# Custom Apps

Show a custom widget in the issue sidebar or account notebooks with information pulled from your own system or external sources.

<figure><img src="https://982123570-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9hXBoCTTNw5flARlUVJf%2Fuploads%2FS1rpYgP8eQVgFdnWTPks%2Fimage.png?alt=media&amp;token=96f638b3-2104-498b-8bdb-30883fa9939d" alt=""><figcaption></figcaption></figure>

## Setup <a href="#setup" id="setup"></a>

Go to **Settings > Integrations & dev > Custom apps**.

{% hint style="warning" %}
You will need to have an API endpoint that can be called to fetch the data for the custom app. When configuring the app, the URL of the API endpoint will be a required field. To verify a custom app request came from Pylon, see [Verifying requests](/pylon-docs/developer/custom-apps.md#verifying-requests).
{% endhint %}

1. **Create the App**\
   \
   Create a new custom app by clicking on the “Create App” button. Give your app a descriptive name, provide the endpoint URL of the API that will be called to fetch the data, and configure any custom headers that need to be sent with the request.\
   \
   Choose a **Widget location**: **Issue sidebar** or **Account overview**. Account overview requires Account Intelligence.<br>
2. **Connect the App**\
   \
   For security purposes, Pylon expects the API endpoint to complete a one-time handshake. Pylon will send a GET request to the endpoint with the following query params:

   | Param          | Value                              |
   | -------------- | ---------------------------------- |
   | `request_type` | `verify`                           |
   | `code`         | `0bcb6f583f0e3ca5f034fdd960d6105c` |

   The code will be a unique code that is generated for each handshake. The endpoint should respond with one of the following responses:

   1. Set the `Content-Type` header to `application/json` and respond with a JSON object containing the `code` value that was sent in the query params.

   ```
   HTTP 200 OK
   Content-type: application/json
   {"code":"0bcb6f583f0e3ca5f034fdd960d6105c"}
   ```

   2. Set the `Content-Type` header to `text/plain` and respond with the `code` value as the response body.

   ```
   HTTP 200 OK
   Content-type: text/plain
   0bcb6f583f0e3ca5f034fdd960d6105c
   ```

   \
   Once the API endpoint is configured to respond to the handshake, open the app and click **Verify endpoint** to send the handshake request. If the handshake succeeds, Pylon shows "Successfully verified". If it fails, the status stays `Unverified` and Pylon shows the error, so fix the endpoint and click **Verify endpoint** again.\
   \
   **Connect** stays disabled until the endpoint is verified. If you change the endpoint URL later, you need to verify it again.

{% hint style="warning" %}
If the app is disconnected, the handshake will need to be completed again to reconnect the app.
{% endhint %}

3. **Load and render data from the API**

The final step is to configure the endpoint to return the data that you want to display. Pylon sends a GET request to the endpoint each time the app loads. For the **Issue sidebar** location, the request has the following query params:

| Param             | Value                                            |
| ----------------- | ------------------------------------------------ |
| `request_type`    | `fetch_data`                                     |
| `app_id`          | id of the custom app                             |
| `widget_id`       | id of the widget associated with the custom app  |
| `issue_id`        | id of the issue that the app is being loaded for |
| `organization_id` | id of the organization that the issue belongs to |
| `account_id`      | id of the account that the issue belongs to      |
| `requester_id`    | user id of the requester of the issue            |
| `requester_email` | email of the requester of the issue              |
| `user_id`         | id of the Pylon user viewing the app             |
| `widget_type`     | `issue_sidebar`                                  |

For the **Account overview** location, the request has these query params:

| Param             | Value                                              |
| ----------------- | -------------------------------------------------- |
| `request_type`    | `fetch_data`                                       |
| `app_id`          | id of the custom app                               |
| `widget_id`       | id of the widget associated with the custom app    |
| `organization_id` | id of your Pylon organization                      |
| `account_id`      | id of the account that the app is being loaded for |
| `user_id`         | id of the Pylon user viewing the app               |
| `widget_type`     | `account_overview`                                 |

`account_id` is omitted when the issue has no account, and `requester_id` and `requester_email` are omitted when it has no requester. Your endpoint must respond within 60 seconds.

The endpoint should respond with a JSON object containing the data that you want to display. For more information about the schema of the data, look at the [schema section](/pylon-docs/developer/custom-apps.md#response-schema) below.

{% hint style="warning" %}
You can use the IDs from the request to fetch any additional information you might need from our [API](/pylon-docs/developer/api/api-reference.md).
{% endhint %}

## Verifying requests <a href="#verifying-requests" id="verifying-requests"></a>

Each custom app has a **Signing secret**, shown on the app's page. Use **Regenerate secret** to replace it; the old secret stops being used right away.

Every request Pylon sends to your endpoint, including the handshake, includes an `X-Pylon-Signature` header. It is the hex-encoded HMAC-SHA256 of the request's **query params**, keyed with the signing secret. The params are encoded as a JSON object that maps each param name to an array of its values, with the keys sorted alphabetically and no whitespace:

```json
{"app_id":["<app id>"],"organization_id":["<org id>"],"request_type":["fetch_data"],"widget_id":["<widget id>"],"widget_type":["issue_sidebar"]}
```

To verify a request, rebuild that JSON from the query params you received, compute the HMAC-SHA256 with your signing secret, and compare it to `X-Pylon-Signature` using a constant-time comparison.

```python
import hashlib
import hmac
import json
from urllib.parse import parse_qs

def verify_pylon_custom_app(secret: str, query_string: str, signature: str) -> bool:
    params = parse_qs(query_string, keep_blank_values=True)
    payload = json.dumps(params, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

## Components <a href="#components" id="components"></a>

The custom app can be built using the following components. If the component type is not recognized, the custom app will render an error state.

## [​](https://docs.usepylon.com/developer/custom-apps#card)Card <a href="#card" id="card"></a>

Render a related set of data together. Custom apps can render multiple cards. All components except cards can be used in the card component.

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-card.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type": "card",
    "header": {
      "title": "Additional Info"
    },
    "components": [
      {
        "type":     "text",
        "label":    "Requester title",
        "value":    "POTUS",
        "icon_url": "https://upload.wikimedia.org/wikipedia/commons/3/36/Seal_of_the_President_of_the_United_States.svg"
      },
      {
        "type":  "text",
        "label": "Requester address",
        "value": "1600 Pennsylvania Avenue NW\nWashington, DC 20500\nUnited States of America"
      },
      {
        "type":  "date_time",
        "label": "Time of request",
        "value": "2024-07-15T12:34:56Z"
      },
      {
        "type":  "badge",
        "label": "Labels",
        "items": [
          {
            "value": "IMPORTANT",
            "color": "red"
          },
          {
            "value": "POTUS",
            "color": "blue"
          },
        ]
      },
      {
        "type":  "link",
        "label": "Home Page",
        "url":   "https://www.whitehouse.gov/"
      },
      {
        "type":        "button",
        "button_type": "link",
        "label":       "Contact",
        "url":         "https://www.whitehouse.gov/"
      }
    ],
  }
]
```

</details>

## Text

The text value can be a multiline string. Any text that overflows the container will be truncated with an ellipsis.

<br>

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-text.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":     "text",
    "label":    "Requester title",
    "value":    "POTUS",
    "icon_url": "https://upload.wikimedia.org/wikipedia/commons/3/36/Seal_of_the_President_of_the_United_States.svg"
  },
  {
    "type":  "text",
    "label": "Requester address",
    "value": "1600 Pennsylvania Avenue NW\nWashington, DC 20500\nUnited States of America"
  }
]
```

</details>

## [​](https://docs.usepylon.com/developer/custom-apps#date-time)Date Time <a href="#date-time" id="date-time"></a>

Date time value should be in ISO date time format [(RFC 3339)](https://datatracker.ietf.org/doc/html/rfc3339).

<br>

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-datetime.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":  "date_time",
    "label": "Sample Date Time",
    "value": "2024-07-15T12:34:56Z"
  },
  {
    "type":     "date_time",
    "label":    "Date time with icon",
    "value":    "2024-07-15T12:34:56Z",
    "icon_url": "https://upload.wikimedia.org/wikipedia/commons/3/36/Seal_of_the_President_of_the_United_States.svg"
  }
]
```

</details>

## [​](https://docs.usepylon.com/developer/custom-apps#badge)Badge <a href="#badge" id="badge"></a>

The badge component can have any number of badges. If the value of the badge is too long, the text in the badge will be truncated with an ellipsis.

<br>

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-badge.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":  "badge",
    "label": "Annotations",
    "items": [
      {
        "value": "Informative",
        "color": "blue"
      },
      {
        "value": "Warning",
        "color": "orange"
      },
      {
        "value": "Danger!",
        "color": "red"
      },
      {
        "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
        "color": "gray"
      },
      {
        "value": "Success :)",
        "color": "green"
      }
    ]
  }
]
```

</details>

## [​](https://docs.usepylon.com/developer/custom-apps#button)Button <a href="#button" id="button"></a>

The button component can be used to show a link, for example to an admin dashboard.

<br>

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-button.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":        "button",
    "button_type": "link",
    "label":       "Click Me",
    "url":         "http://google.com"
  }
]
```

</details>

[​](https://docs.usepylon.com/developer/custom-apps#link)Link\ <a href="#link" id="link"></a>
---------------------------------------------------------------------------------------------

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-link.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":  "link",
    "label": "Example Link",
    "url":   "http://example.com"
  },
  {
    "type":  "link",
    "label": "Long Link",
    "url":   "https://app.usepylon.com/issues?conversationID=abc"
  }
]
```

</details>

## [​](https://docs.usepylon.com/developer/custom-apps#divider)Divider <a href="#divider" id="divider"></a>

<figure><img src="https://mintlify.s3-us-west-1.amazonaws.com/pylon/images/embedded-app-divider.png" alt=""><figcaption></figcaption></figure>

<details>

<summary>Config</summary>

```json
"components": [
  {
    "type":  "divider"
  }
]
```

</details>

## Response Schema

## Version 1.0.0

<details>

<summary>Schema</summary>

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Custom App Data Schema",
  "description": "Data to be displayed in the issue sidebar.",
  "type": "object",
  "properties": {
    "version": {
      "type": "string",
      "description": "The version of the schema.",
      "example": "1.0.0"
    },
    "header": {
      "type": "object",
      "description": "Header information.",
      "properties": {
        "title": {
          "type": "string",
          "description": "Title of the header.",
          "example": "Sample Header"
        },
        "icon_url": {
          "type": "string",
          "description": "URL to the header icon.",
          "format": "uri",
          "example": "http://example.com/icon.png"
        }
      }
    },
    "components": {
      "type": "array",
      "description": "List of components.",
      "items": {
        "oneOf": [
          { "$ref": "#/definitions/cardComponent" },
          { "$ref": "#/definitions/textComponent" },
          { "$ref": "#/definitions/dateTimeComponent" },
          { "$ref": "#/definitions/badgeComponent" },
          { "$ref": "#/definitions/buttonComponent" },
          { "$ref": "#/definitions/linkComponent" },
          { "$ref": "#/definitions/dividerComponent" }
        ]
      }
    }
  },
  "required": ["version", "header", "components"],
  "definitions": {
    "cardComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["card"],
          "description": "Component type.",
          "example": "card"
        },
        "header": {
          "type": "object",
          "description": "Header information.",
          "properties": {
            "title": {
              "type": "string",
              "description": "Title of the card header.",
              "example": "Sample Header"
            },
            "icon_url": {
              "type": "string",
              "description": "URL to the card header icon.",
              "format": "uri",
              "example": "http://example.com/icon.png"
            }
          }
        },
        "components": {
          "type": "array",
          "description": "List of components.",
          "items": {
            "oneOf": [
              { "$ref": "#/definitions/textComponent" },
              { "$ref": "#/definitions/dateTimeComponent" },
              { "$ref": "#/definitions/badgeComponent" },
              { "$ref": "#/definitions/buttonComponent" },
              { "$ref": "#/definitions/linkComponent" },
              { "$ref": "#/definitions/dividerComponent" }
            ]
          }
        }
      },
      "required": ["type", "components"]
    },
    "textComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["text"],
          "description": "Component type.",
          "example": "text"
        },
        "label": {
          "type": "string",
          "description": "Label for the text component.",
          "example": "Sample Text"
        },
        "value": {
          "type": "string",
          "description": "Value for the text component.",
          "example": "This is a sample text"
        },
        "icon_url": {
          "type": "string",
          "description": "URL to the text icon.",
          "format": "uri",
          "example": "http://example.com/text-icon.png"
        }
      },
      "required": ["type", "label", "value"]
    },
    "dateTimeComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["date_time"],
          "description": "Component type.",
          "example": "date_time"
        },
        "label": {
          "type": "string",
          "description": "Label for the date_time component.",
          "example": "Sample Date"
        },
        "value": {
          "type": "string",
          "description": "Value for the date_time component.",
          "format": "date-time",
          "example": "2024-07-15T12:34:56Z"
        },
        "icon_url": {
          "type": "string",
          "description": "URL to the date_time icon.",
          "format": "uri",
          "example": "http://example.com/date-icon.png"
        }
      },
      "required": ["type", "label", "value"]
    },
    "badgeComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["badge"],
          "description": "Component type.",
          "example": "badge"
        },
        "label": {
          "type": "string",
          "description": "Label for the badge component.",
          "example": "Sample Badge"
        },
        "items": {
          "type": "array",
          "description": "Items for the badge component.",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Value of the badge item.",
                "example": "Badge 1"
              },
              "color": {
                "type": "string",
                "description": "Color of the badge item.",
                "example": "blue",
                "enum": ["blue", "green", "red", "orange", "gray"]
              }
            },
            "required": ["value", "color"]
          }
        }
      },
      "required": ["type", "label", "items"]
    },
    "buttonComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["button"],
          "description": "Component type.",
          "example": "button"
        },
        "label": {
          "type": "string",
          "description": "Label for the button component.",
          "example": "Click Me"
        },
        "button_type": {
          "type": "string",
          "description": "Type of button.",
          "example": "link",
          "enum": ["link"]
        },
        "url": {
          "type": "string",
          "description": "URL for the button.",
          "format": "uri",
          "example": "http://example.com"
        }
      },
      "required": ["type", "label", "button_type", "url"]
    },
    "linkComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["link"],
          "description": "Component type.",
          "example": "link"
        },
        "label": {
          "type": "string",
          "description": "Label for the link component.",
          "example": "Example Link"
        },
        "url": {
          "type": "string",
          "description": "URL for the link.",
          "format": "uri",
          "example": "http://example.com"
        }
      },
      "required": ["type", "label", "url"]
    },
    "dividerComponent": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["divider"],
          "description": "Component type.",
          "example": "divider"
        }
      },
      "required": ["type"]
    }
  }
}
```

</details>

<details>

<summary>Example</summary>

```json
{
  "version": "1.0.0",
  "header": {
    "title": "Sample Header",
    "icon_url": "http://example.com/icon.png"
  },
  "components": [
    {
      "type": "card",
      "components": [
        {
          "type": "text",
          "label": "Nested Text",
          "value": "This is nested text",
          "icon_url": "http://example.com/nested-text-icon.png"
        }
      ]
    },
    {
      "type": "text",
      "label": "Sample Text",
      "value": "This is a sample text",
      "icon_url": "http://example.com/text-icon.png"
    },
    {
      "type": "date_time",
      "label": "Sample Date",
      "value": "2024-07-15T12:34:56Z",
      "icon_url": "http://example.com/date-icon.png"
    },
    {
      "type": "badge",
      "label": "Sample Badge",
      "items": [
        {
          "value": "Badge 1",
          "color": "blue"
        },
        {
          "value": "Badge 2",
          "color": "green"
        }
      ]
    },
    {
      "type": "button",
      "label": "Click Me",
      "button_type": "link",
      "url": "http://example.com"
    },
    {
      "type": "link",
      "label": "Example Link",
      "url": "http://example.com"
    },
    {
      "type": "divider"
    }
  ]
}
```

</details>

## Usage

Embed your custom app within the issue view.

<figure><img src="https://982123570-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9hXBoCTTNw5flARlUVJf%2Fuploads%2Fme1Axi0i9ydJgesCaP8Z%2FCleanShot%202024-10-05%20at%2011.57.26.png?alt=media&amp;token=11f25886-40ea-4cc7-b2e1-b4f25d3dc089" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.usepylon.com/pylon-docs/developer/custom-apps.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
