# Webhooks

Instead of waiting for an Assembly to finish and its API request to respond, you can also configure a Webhook, also known as an Assembly Notification. The system will send a POST request to a URL of your choosing containing a full report once an Assembly ends.

## Why use Webhooks?

By choosing to use Webhooks you enable your end-users to have a smoother experience, as they only need to wait for file uploads to finish before they can close the browser window. Without file uploads, they could even close the browser window right away.

## How to activate Webhooks?

You activate webhooks by adding `notify_url` to your Assembly Instructions in yourTemplate on the same JSON level as `steps`:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```jsonc
{
  "steps": {
    // …
  },
  "notify_url": "https://example.com/transloadit_pingback"
}

```

When you then run your Template Transloadit is going to inform your back-end once all processing has happened by sending a `POST` request to that defined URL containing all the Assembly status json.

If you don't want your users/program to wait for encoding, this often also involves setting a flag. In the case of [Uppy](/docs/sdks/uppy.md), set the `waitForEncoding` parameter to `false`. In many back-end SDKs, waiting for encoding involves explicitly polling the Assembly Status, so just refraining from that will do the trick.

Your back-end needs to respond with a `200` header, otherwise Transloadit assumes the Notification has failed and retries it a few times with exponential backoff.

## What does this POST request look like?

This multipart POST request contains a field called `transloadit`, which contains the fullAssembly Status JSON. You can find an example of this in our[API Response docs](/docs/api/assemblies-assembly-id-get.md#response). It will also contain a`signature` field so that you could optionally verify that the request indeed came from us and was not tampered with. How to calculate that signature with your Auth Secret to match the one we sent is shown in the code example below.

## Code Example

Let's assume you had indeed specified `"notify_url": "https://example.com/transloadit_pingback"` and that the back-end server that would accept incoming POSTs there was written in Node.js.

###### Note

This example shows how to **verify** incoming webhook signatures from Transloadit, which is different from **generating** signatures for API requests. For creating Assemblies with automatic signature generation, use our [SDKs](/docs/sdks.md) instead. If you'd like to see how signature verification works under the hood, you can view the[Node SDK source code](https://github.com/transloadit/node-sdk/blob/main/src/Transloadit.ts).

The verification server could look like this:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```js
import crypto from 'node:crypto'
import http from 'node:http'

import formidable from 'formidable'

const PORT = process.argv[2] || 3020

if (!/^[A-Za-z0-9]{40}$/.test(process.env.AUTH_SECRET)) {
  throw new Error(`Please pass the secret from https://transloadit.com/c/template-credentials
    via the AUTH_SECRET environment var. It must be the auth secret that belongs to the auth key you used for the original Assembly.`)
}

const checkSignature = (fields, authSecret) => {
  const receivedSignature = fields.signature
  const payload = fields.transloadit

  if (!receivedSignature || !payload) {
    return false
  }

  // If the signature contains a colon, we expect it to be of format `algo:actual_signature`.
  // If there are no colons, we assume it's a legacy signature using SHA-1.
  const algoSeparatorIndex = receivedSignature.indexOf(':')
  const algo = algoSeparatorIndex === -1 ? 'sha1' : receivedSignature.slice(0, algoSeparatorIndex)

  try {
    const calculatedSignature = crypto
      .createHmac(algo, authSecret)
      .update(Buffer.from(payload, 'utf-8'))
      .digest('hex')

    // If we are in legacy signature mode, algoSeparatorIndex is -1 and we are
    // comparing the whole string. Otherwise we slice out the prefixed algo.
    return calculatedSignature === receivedSignature.slice(algoSeparatorIndex + 1)
  } catch {
    // We can assume the signature string was ill-formed.
    return false
  }
}

const respond = (res, code, messages) => {
  if (code !== 200) {
    console.error({ messages, code })
  }

  res.writeHead(code, {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
  })

  if (messages) {
    res.write(JSON.stringify({ messages }))
  }

  res.end()
}

http
  .createServer((req, res) => {
    if (req.method === 'OPTIONS') {
      return respond(res, 204)
    }
    if (req.url === '/transloadit_pingback' && req.method === 'POST') {
      const form = new formidable.IncomingForm()
      form.parse(req, (err, fields) => {
        if (err) {
          return respond(res, 500, [`Error while parsing multipart form`, err])
        }

        if (!checkSignature(fields, process.env.AUTH_SECRET)) {
          return respond(res, 403, [
            `Error while checking signatures`,
            `No match so payload was tampered with, or an invalid Auth Secret was used`,
          ])
        }

        let assembly = {}
        try {
          assembly = JSON.parse(fields.transloadit)
        } catch (err) {
          return respond(res, 500, [`Error while parsing transloadit field`, err])
        }

        console.log(`--> ${assembly.ok || assembly.error} ${assembly.assembly_ssl_url}`)

        for (const upload of assembly.uploads) {
          // save upload.ssl_url and metadata to your db here
          console.log(`    ^- uploaded '${upload.name}' ready at ${upload.ssl_url}`)
        }

        for (const stepName in assembly.results) {
          for (const result of assembly.results[stepName]) {
            // save result.ssl_url and metadata to your db here
            console.log(`    ^- ${stepName} '${result.name}' ready at ${result.ssl_url}`)
          }
        }

        return respond(res, 200, [`Success!`])
      })
    } else {
      return respond(res, 500, [
        `Welcome! I only know how to handle POSTs to /transloadit_pingback`,
        `No handler for req.url=${req.url}, req.method=${req.method}`,
      ])
    }
  })
  .listen(PORT, () => {
    console.log(`Server started, listening on http://0.0.0.0:${PORT}`)
  })

```

You could run this script like so:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```console
$ env AUTH_SECRET=******** node notification-backend-node.js 3020
Server started, listening on http://0.0.0.0:3020

```

## Trying the Code Example locally

While testing locally behind a NAT, use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/), [ngrok](https://ngrok.com/), or the first-party [@transloadit/notify-url-relay](https://www.npmjs.com/package/@transloadit/notify-url-relay) via `npx -y @transloadit/notify-url-relay`; unlike tunnels, the relay polls public Assembly Status and forwards terminal notifications to your local `notify_url` handler.

Recommended (first-party relay), in a new tab:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```console
$ TRANSLOADIT_SECRET=******** npx -y @transloadit/notify-url-relay \
    --notifyUrl "http://127.0.0.1:3020/transloadit_pingback" \
    --log-level info
notify-url-relay [ NOTICE] Listening on http://localhost:8888, forwarding to https://api2.transloadit.com, notifying http://127.0.0.1:3020/transloadit_pingback

```

When using the relay, point your app/SDK Transloadit endpoint to `http://127.0.0.1:8888`.

You can now [create a Template](/docs/topics/templates.md#how-to-create-a-template) and paste the following Instructions:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```json
{
  "notify_url": "http://127.0.0.1:3020/transloadit_pingback",
  "steps": {
    ":original": {
      "robot": "/upload/handle"
    },
    "faces_detected": {
      "use": ":original",
      "robot": "/image/facedetect",
      "crop": true,
      "faces": "max-confidence",
      "crop_padding": "10%",
      "format": "preserve"
    }
  }
}

```

If you prefer a tunnel instead, use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) or [ngrok](https://ngrok.com/).

###### Note

At the time of writing[ngrok appears to have issues with AWS ranges](https://github.com/inconshreveable/ngrok/issues/408#issuecomment-791161309). If this is the case for you, an alternative to try is[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/)or the first-party [@transloadit/notify-url-relay](https://www.npmjs.com/package/@transloadit/notify-url-relay).

Now you're ready to test right inside the browser. The Instructions we used detect a face, so for optimal results, upload a photo of someone using the Template Editor's testing area. You can use Uppy's Webcam feature if you don't have a picture available.

Your Node.js script should report it has successfully received the Assembly Notification when theAssembly completes:

![](/_next/static/media/copy.04p1cju9qekk_.svg?dpl=dpl_CtwzFbHWtqiCy9uvWb9fE7WvfP9N)

```txt
-- > ASSEMBLY_COMPLETED https://api2.transloadit.com/assemblies/b2b580bdc969427091a48f1f0d3d9d40
^- uploaded 'avatar.jpg' ready at https://s3.amazonaws.com/tmp.transloadit.com/ff89be82...
^- faces_detected 'avatar.jpg' ready at https://s3.amazonaws.com/tmp.transloadit.com/fd2f61b9...

```

In addition, you'll see a record of the notification on the Assembly page, where you can also manually retry it for further testing.

The example code higher up shows how to verify webhook signatures in Node.js. For creating Assemblies with automatic signature generation (rather than verifying incoming webhooks), see the SDK examples in the [Signature Authentication docs](/docs/api/authentication.md).
