> Documentation index: [Saleor](/llms.txt) · [This section](/developer/llms.txt)
> Source: https://docs.saleor.io/developer/extending/apps/updating-app-webhooks

# How to Update App Webhooks

<a id="problem"></a>

## Problem

Imagine you have an app that notifies an external service whenever a new order is created. The app registers the `ORDER_CREATED` webhook based on the [subscription webhook payload](/developer/extending/webhooks/subscription-webhook-payloads.md) from the `OrderCreatedSubscription.graphql` file:

```graphql
# OrderCreatedSubscription.graphql
subscription OrderCreated {
  event {
    ... on OrderCreated {
      order {
        id
        created
        number
      }
    }
  }
}
```

The subscription defines what data will be sent to the app along with the webhook. The app can then execute logic utilizing the subscription payload. In our case, it will be notifying the external service about the new order:

```js
// pseudo-code for order-created webhook handler

const payload = {
  id: order.id,
  created: order.created,
  number: order.number,
};

service.notifyOrderCreated(payload);
```

After some time, you realize you need an additional order field: `status`. In the next app release, you add it to the subscription webhook payload:

```graphql
# OrderCreatedSubscription.graphql
subscription OrderCreated {
  event {
    ... on OrderCreated {
      order {
        id
        created
        number
        status
      }
    }
  }
}
```

You modify the code to use the new field:

```js
// pseudo-code for order-created webhook handler

const payload = {
  id: order.id,
  created: order.created,
  number: order.number,
  status: order.status,
};

service.notifyOrderCreated(payload);
```

You deploy the app and trigger the `ORDER_CREATED` webhook. Perhaps surprised, you see a type error: `order.status is not defined`. What happened?

Although you did modify both the `OrderCreatedSubscription.graphql` file, and the code of your application, **Saleor still uses the original subscription from the app installation manifest.**

caution

Whenever your app starts using a new field from the subscription, you must update the app's [webhook `query` field](/api-reference/webhooks/inputs/webhook-update-input.md#query).

If your app hasn't yet been released or does not require zero downtime, the easiest solution would be to simply reinstall it. The query used in the subscription will be regenerated during the installation.

However, if your app has to stay functional at all times, you have to programmatically update its webhooks.

<a id="webhook-migration-script"></a>

## Webhook migration script

To update webhooks without disrupting service you could use a webhook migration script. Those scripts should, ideally, execute before your app deployment (e.g., in CI). This way, you can ensure the app has all the required subscription fields without downtime.

If you are using [`@saleor/app-sdk`](/developer/extending/apps/developing-apps/app-sdk/overview.md) for app development, you can use some of its helpers in your migration script.

Here is what a webhook migration script may look like:

<a id="1-authenticate-app"></a>

### 1\. Authenticate app

To authenticate app-related API calls, you need to get the app token from its [`authData`](/developer/extending/apps/developing-apps/app-sdk/apl.md#authdata).

Where you get `authData` will depend on your [APL](/developer/extending/apps/developing-apps/app-sdk/apl.md) implementation.

Let's assume you are using the [`UpstashAPL`](https://github.com/saleor/app-sdk/blob/6527b0e308cc66fce11956be827464f2ff12d185/src/APL/upstash-apl.ts) provided in app-sdk:

```ts
import { UpstashAPL } from "@saleor/app-sdk/APL";

export const getAppAuthData = async () => {
  // Requires `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables
  // Initialize UpstashAPL
  const apl = new UpstashAPL();

  // Get authData of all registered apps
  const apps = await apl.getAll();

  // Assuming there is only one app, return its authData
  return apps[0];
};
```

<a id="2-get-a-webhook-manifest"></a>

### 2\. Get a webhook manifest

We will update our webhook with [`webhookUpdate`](/api-reference/webhooks/mutations/webhook-update.md) mutation. As its input, we only want to pass the new value of the `query` field.

We can get a stringified `query` field from the webhook manifest. Webhook manifest is the result of executing `getWebhookManifest` method on the instance of `SaleorAsyncWebhook` or `SaleorSyncWebhook` classes. These classes are provided by the [`@saleor/app-sdk`](/developer/extending/apps/developing-apps/app-sdk/overview.md) to help you build your webhooks for the [app manifest](/developer/extending/apps/extending-dashboard-with-apps.md#manifest).

Here is what webhook handler may look like for an `ORDER_CREATED` webhook:

```ts
// api/webhooks/order-created.ts

import { saleorApp } from "@/saleor-app";
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
  OrderCreatedDocument,
  OrderCreatedSubscriptionPayloadFragment,
} from "@/generated/graphql";

export const orderCreatedWebhook =
  new SaleorAsyncWebhook<OrderCreatedSubscriptionPayloadFragment>({
    name: "Order Created",
    webhookPath: "api/webhooks/order-created",
    event: "ORDER_CREATED",
    apl: saleorApp.apl,
    query: OrderCreatedDocument, // OrderCreatedDocument will be converted into a string query
  });

// Stable, app-defined identifier used to reference this webhook in the migration script
export const ORDER_CREATED_WEBHOOK_IDENTIFIER = "order-created";
```

You can see `getWebhookManifest` being used in the `webhooks` field of your `manifest.ts`, where the app manifest is created. Saleor registers the app based on this manifest.

Add an [`identifier`](/developer/extending/webhooks/creating.md#webhook-identifier) to the manifest entry so Saleor stores it when the app is installed. `getWebhookManifest` does not set one, so spread its result and add the field:

```ts
// pages/api/manifest.ts
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
import {
  orderCreatedWebhook,
  ORDER_CREATED_WEBHOOK_IDENTIFIER,
} from "./webhooks/order-created";

export default createManifestHandler({
  async manifestFactory({ appBaseUrl }) {
    const manifest = {
      // ...
      webhooks: [
        {
          ...orderCreatedWebhook.getWebhookManifest(appBaseUrl),
          identifier: ORDER_CREATED_WEBHOOK_IDENTIFIER,
        },
      ],
    };
    return manifest;
  },
});
```

note

Requires Saleor 3.23.23 or newer for the manifest and `Webhook.identifier` field, and `@saleor/app-sdk` with `identifier` on the `WebhookManifest` type. On older Saleor versions the field is ignored, and the migration script has to fall back to matching by `name` — see [Apps installed before identifiers](#apps-installed-before-identifiers).

We will repeat the same logic in our migration script to get the current state of our webhook:

```ts
import { orderCreatedWebhook } from "./pages/api/webhooks/order-created";

const runMigration = async () => {
  const authData = await getAppAuthData();

  // Regenerate orderCreated webhook manifest with updated state
  const webhookManifest = orderCreatedWebhook.getWebhookManifest(
    authData.saleorApiUrl
  );
};
```

<a id="3-update-the-webhook"></a>

### 3\. Update the webhook

Because the webhook declares an `identifier`, the script can address it directly — [`webhookUpdate`](/api-reference/webhooks/mutations/webhook-update.md) accepts `identifier` in place of `id` from Saleor 3.23.27, so there is no lookup step:

```graphql
mutation UpdateWebhookQuery($identifier: String!, $query: String!) {
  webhookUpdate(identifier: $identifier, input: { query: $query }) {
    webhook {
      id
      identifier
    }
    errors {
      field
      code
      message
    }
  }
}
```

note

In the next code example, we will assume the existence of:

-   `createGraphQLClient` - a function that returns a GraphQL client
-   `AppWebhookManager` - a class that takes in the GraphQL client and makes calls to the Saleor API. It has the following methods:
    -   `updateWebhookQuery` - runs the [`webhookUpdate`](/api-reference/webhooks/mutations/webhook-update.md) mutation above, addressing the webhook by `identifier`
    -   `getOwnWebhooks` - runs the [`app`](/api-reference/apps/queries/app.md) query and returns the app's own `webhooks`
    -   `setWebhookIdentifier` - runs `webhookUpdate` by `id`, writing the `identifier` input

Neither is provided by Saleor or `@saleor/app-sdk` — they stand in for whatever GraphQL client and wrapper your app already uses.

Once we have the up-to-date manifest, we can retrieve the stringified `query` from it and update the webhook:

```ts
import { ORDER_CREATED_WEBHOOK_IDENTIFIER } from "./pages/api/webhooks/order-created";

const runMigration = async () => {
  // ...

  // Imaginary function that creates a GraphQL client for your API calls. This can be Apollo Client, Urql Client, etc.
  const client = createGraphQLClient({
    saleorApiUrl: authData.saleorApiUrl,
    token: authData.token,
  });

  // Imaginary class that takes in GraphQL client and makes calls to the Saleor API
  const appWebhookManager = new AppWebhookManager({
    client,
  });

  // Update webhook with new query, addressing it by its stable identifier
  await appWebhookManager.updateWebhookQuery({
    identifier: ORDER_CREATED_WEBHOOK_IDENTIFIER,
    query: webhookManifest.query, // update webhook with fields from the new manifest
  });
};
```

The `identifier` argument is available exclusively to an app referencing its own webhook — the app token in the `Authorization` header is what scopes the lookup. If no webhook of that app matches, the mutation returns a `NOT_FOUND` error on the `identifier` field.

<a id="apps-installed-before-identifiers"></a>

### Apps installed before identifiers

`identifier` is only stored at install time, so webhooks of apps installed before the identifier was added to the manifest have none. Those apps need a one-time backfill: list the app's own webhooks, match them by whatever you used previously (usually `name`), and write the identifier once.

The app's own webhooks are read through the [`app`](/api-reference/apps/queries/app.md) query — with an app token in the `Authorization` header it resolves to the calling app, so no ID is needed:

```graphql
query AppWebhooks {
  app {
    webhooks {
      id
      name
      identifier
    }
  }
}
```

Writing the identifier is a normal `webhookUpdate` addressed by `id`, since there is no identifier to address it by yet:

```graphql
mutation SetWebhookIdentifier($id: ID!, $identifier: String!) {
  webhookUpdate(id: $id, input: { identifier: $identifier }) {
    webhook {
      id
      identifier
    }
    errors {
      field
      code
      message
    }
  }
}
```

Using the same `appWebhookManager` as in [step 3](#3-update-the-webhook):

```ts
const backfillIdentifier = async () => {
  const webhooks = await appWebhookManager.getOwnWebhooks();

  const webhook = webhooks.find(
    (w) => !w.identifier && w.name === webhookManifest.name
  );

  if (webhook) {
    await appWebhookManager.setWebhookIdentifier({
      id: webhook.id,
      identifier: ORDER_CREATED_WEBHOOK_IDENTIFIER,
    });
  }
};
```

Run this before the first identifier-based migration. Afterwards the `name` match is no longer needed, and renaming the webhook in the Dashboard stops breaking your migrations.

caution

Matching on `name` is exactly the fragile step `identifier` removes: `name` is a display label that a staff user can change in the Dashboard, and nothing stops two webhooks of the same app from sharing it. Treat the backfill as a one-off, not as a permanent fallback.

<a id="next-steps"></a>

## Next steps

For most cases, the above script should be enough to update the webhook. However, if you can't afford to have any downtime, you should consider a more complex migration process that factors in:

-   **Rollback** - If the migration fails, you should be able to roll back to the previous state. This can be achieved by deactivating the old webhook, creating a new webhook, testing it, and only then removing the old one.
-   **Queued events** - [Saleor puts events in a queue](/developer/extending/apps/architecture/communication-between-app-and-saleor.md#task-queue) and processes them asynchronously. This means that even if you update the webhook, the events that were put in the queue before will still be sent with the old query. Your app should be able to work with both old and new queries until the migration is complete.
-   **Safe query modification** - If you add a new field to the query, you should ensure that the app can handle the absence of this field in the old events. If you remove a field, you should ensure that the app can handle the presence of this field in the old events.

If you need the full-picture view of migration scripts, feel free to peak into [saleor/apps repository](https://github.com/saleor/apps/tree/main/apps/avatax/scripts/migrations).
