# App Use Case: Send an Issue to an External Service

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com.cn/en-us/help/youtrack/devportal/llms.txt).

When part of your team's process takes place outside YouTrack, users might need to send selected issues to an external service without copying their details manually.

Building on this idea, create an app that adds an action to the issue toolbar. The action sends the current issue to an external service and records the identifier returned by the service. This app is most useful for solution developers and system administrators who build and maintain integrations for one or more projects.

The app includes:

* An [ISSUE_OPTIONS_MENU_ITEM widget](apps-reference-extension-points.html#places-issue-options-menu-item) that starts the integration from the current issue.

* A project-level [HTTP handler](apps-reference-http-handlers.html) that prepares the issue data and sends it to the external service.

* [App settings](app-settings.html) for the service URL and credentials.

* An [issue extension property](apps-extension-properties.html) that stores the identifier returned by the external service.

> **Note:**
> This tutorial describes a reusable integration pattern rather than an API for a specific external service. Adapt the request URL, payload, authentication method, and response handling to the service you want to connect.

Follow these steps to build and test the integration.

## Prerequisites

To create an app like the one described in this tutorial, you need:

* Basic knowledge of JavaScript.

* Basic-level proficiency in using terminal commands.

* Node.js and npm installed on your machine.

* Access at the project admin level or higher to a YouTrack instance and a project where you can test the app.

* A test endpoint for the external service and credentials that can access it.

* Information about the request payload and response format expected by the external service.

## Step 1 — Create an App Package

Prepare an empty directory for your app. Open the terminal, navigate to the app directory, and run the YouTrack app generator using the following command:

```SHELL
npm create @jetbrains/youtrack-app@latest
```

Create an app named `external-service-integration` with the title `External Service Integration`.

When the generator prompts you to create a widget, add a widget named `Send to External Service` with the key `send-to-external-service`. Select `ISSUE_OPTIONS_MENU_ITEM` as the extension point. This extension point adds an item to the issue toolbar that opens the widget.

For more information about creating an app package, see [Create an App Package](apps-quick-start-guide.html#create-app-package).

## Step 2 — Update the Manifest

Open the `manifest.json` file and replace the widget declaration created by the generator with the following declaration:

```JSON
{
    "key": "send-to-external-service",
    "name": "Send to External Service",
    "indexPath": "send-to-external-service/index.html",
    "extensionPoint": "ISSUE_OPTIONS_MENU_ITEM",
    "description": "Sends the current issue to an external service.",
    "permissions": ["UPDATE_ISSUE"]
}
```

The `UPDATE_ISSUE` permission limits the action to users who can update the current issue. The HTTP handler uses the same permission and updates the issue extension property only after the external service accepts the request.

For details about widget declarations and permission restrictions, see [Widgets](app-manifest.html#app-manifest-widgets) and [App Permissions](app-permissions.html).

## Step 3 — Configure the External Service

Create a `settings.json` file in the root directory of the app package. This file lets an administrator configure the service URL and credentials. Use project-level settings when each project connects to a different endpoint or account.

```JSON
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "title": "External Service",
    "properties": {
        "serviceUrl": {
            "title": "Service URL",
            "type": "string",
            "x-scope": "PROJECT"
        },
        "apiToken": {
            "title": "API Token",
            "type": "string",
            "format": "secret",
            "x-scope": "PROJECT"
        }
    },
    "required": ["serviceUrl", "apiToken"]
}
```

Enter the base URL of the external service in the Service URL setting. For example, if the service accepts requests at `https://api.example.com/items`, enter `https://api.example.com`. The HTTP handler adds the `/items` path.

The secret format keeps the token from being exposed to the widget or displayed after it is saved. The HTTP handler can use the value to authenticate requests sent with the YouTrack HTTP module.

For details, see [Working with Settings for Secrets](app-settings.html#working-with-secrets).

## Step 4 — Add App Storage

Use an extension property to remember the identifier returned by the external service. This lets the app recognize issues that have already been sent and avoid creating duplicate external items.

Create an `entity-extensions.json` file in the root directory of the app package and declare a string property for issues:

```JSON
{
    "entityTypeExtensions": [
        {
            "entityType": "Issue",
            "properties": {
                "externalId": {
                    "type": "string",
                    "description": "Identifier returned by the external service."
                }
            }
        }
    ]
}
```

Use a regular custom field instead when users need to search, sort, or report on the value in the YouTrack UI.

## Step 5 — Build the Widget Frontend

Open the `src/widgets/send-to-external-service` directory and replace the contents of `index.html` with the following markup:

```HTML
            
<!doctype html>
<html>
<head><link rel="canonical" href="https://www.jetbrains.com.cn/en-us/help/youtrack/devportal/app-uc-send-issue-to-external-service.html.md/" data-react-helmet="true"/>
    <meta charset="UTF-8">
    <title>Send to External Service</title>
</head>
<body class="plugin">
    <p>Send the current issue to the configured external service?</p>
    <button id="send-button" type="button">Send issue</button>
    <p id="status" role="status"></p>
    <script type="module" src="./index.js"></script>
</body>
</html>

```

Create an `index.js` file in the same directory and add the following code:

```JAVASCRIPT
const host = await YTApp.register();
const sendButton = document.getElementById('send-button');
const status = document.getElementById('status');

sendButton.addEventListener('click', async () => {
    sendButton.disabled = true;
    status.textContent = 'Sending issue...';

    try {
        const result = await host.fetchApp('backend/send', {
            method: 'POST',
            scope: true
        });

        if (!result.success) {
            throw new Error(result.message || 'The external service rejected the request.');
        }

        status.textContent = result.alreadySent
            ? 'This issue was already sent as ' + result.externalId + '.'
            : 'Issue sent as ' + result.externalId + '.';
    } catch (error) {
        status.textContent = error.message || 'Unable to send the issue.';
        sendButton.disabled = false;
    }
});
```

The call to `host.fetchApp()` uses the current issue as its scope. The widget does not need to send an issue identifier or any service credentials to the handler.

The widget must not receive or send the service credentials. Keep authentication in the backend handler.

For details about registering a widget and calling an app endpoint, see [Host API](apps-host-api.html).

## Step 6 — Add an HTTP Handler

The sample handler uses the following external API contract:

* Send a `POST` request to `<serviceUrl>/items`.

* Authenticate the request with the configured API token as a bearer token.

* Send the YouTrack issue ID, summary, and description as a JSON object.

* Read the external identifier from the `id` property in the JSON response.

For example, the external service should return a response in the following format:

```JSON
{
    "id": "EXT-123"
}
```

Open the `backend.js` file in the `src` directory and replace its contents with the following handler:

```JAVASCRIPT
const http = require('@jetbrains/youtrack-scripting-api/http');

exports.httpHandler = {
    endpoints: [
        {
            scope: 'issue',
            method: 'POST',
            path: 'send',
            permissions: ['UPDATE_ISSUE'],
            handle: function handle(ctx) {
                const issue = ctx.issue;
                const savedExternalId = issue.extensionProperties.externalId;

                if (savedExternalId) {
                    return ctx.response.json({
                        success: true,
                        alreadySent: true,
                        externalId: savedExternalId
                    });
                }

                const payload = {
                    id: issue.idReadable,
                    summary: issue.summary,
                    description: issue.description || ''
                };

                try {
                    const connection = new http.Connection(ctx.settings.serviceUrl);
                    connection.bearerAuth(ctx.settings.apiToken);
                    connection.addHeader('Content-Type', 'application/json');

                    const serviceResponse = connection.postSync(
                        '/items',
                        null,
                        JSON.stringify(payload)
                    );

                    if (!serviceResponse.isSuccess) {
                        return ctx.response.json({
                            success: false,
                            message: 'External service returned status ' + serviceResponse.code + '.'
                        });
                    }

                    const responseData = serviceResponse.json();
                    if (!responseData || responseData.id == null) {
                        return ctx.response.json({
                            success: false,
                            message: 'External service did not return an identifier.'
                        });
                    }

                    const externalId = String(responseData.id);
                    issue.extensionProperties.externalId = externalId;

                    return ctx.response.json({
                        success: true,
                        alreadySent: false,
                        externalId: externalId
                    });
                } catch (error) {
                    console.warn('Unable to send issue to external service: ' + error);
                    return ctx.response.json({
                        success: false,
                        message: 'Unable to contact the external service.'
                    });
                }
            }
        }
    ]
};
```

This example sends a `POST` request to the `/items` path and expects a JSON response with an `id` property. Replace the path, payload, and response property with values supported by your external service. Replace `bearerAuth()` when the service uses a different authentication method.

The handler checks the extension property before sending the request, uses the current issue from the endpoint scope, and stores the external identifier only after the service returns a successful response.

For the HTTP handler format and an example that calls an external service, see [Use Workflow API Modules in HTTP Handlers](apps-reference-http-handlers.html#workflow-modules-in-http-handlers).

## Step 7 — Build and Test the App

Build the app by running the following command in your terminal:

```SHELL
npm run build
```

Upload the app to a test YouTrack instance:

```
npm run upload -- --host <your YouTrack base URL> --token <permanent token>
```

Alternatively, upload the ZIP archive manually. For details, see [Upload the App to YouTrack](apps-quick-start-guide.html#add-app-to-youtrack).

Attach the app to a test project. On the project-level Settings tab for the app, configure the service URL and API token, then activate the widget and HTTP handler.

Open an issue in the test project and verify the following behavior:

* Sign in as a user with the Update Issue permission and confirm that the app action is available.

* Select the app action and click Send issue.

* Check that the external service receives the issue ID, summary, and description.

* Check that the widget displays the identifier returned by the external service.

* Select the action again and confirm that the app reports the existing identifier without creating a duplicate.

* Use an invalid service URL and confirm that the app reports an error without storing an identifier or exposing the API token.

## Summary

You've built an app that sends an issue to an external service from the issue toolbar. The app keeps the integration logic and credentials in a backend HTTP handler, while the widget handles confirmation and displays the result.

The app uses a secret setting for authentication and an issue extension property to remember the identifier returned by the external service.

## Next Steps

Now that you've built the basic integration, you can extend it in several ways:

* Add a link that opens the external item from the YouTrack issue.

* Let users update an existing external item instead of creating another one.

* Add project settings that map YouTrack fields to fields in the external service.

* Add a workflow rule that sends issues automatically when they meet specific conditions.

* Expose a separate webhook endpoint for updates initiated by the external service.

* Add asynchronous processing for integrations that take too long to complete in the initial request.

