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 your
Template on the same JSON level as steps:
{
"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, 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.
Customize the Notification payload
By default, webhook requests include the full
Assembly Status JSON. If your Assemblies produce many
results, you can reduce payload size with notification_payload.
Add it alongside notify_url:
{
"steps": {
// …
},
"notify_url": "https://example.com/transloadit_pingback",
"notification_payload": ["without_results", "without_upload_meta_data"]
}
notification_payloadArray<without_params | without_result_meta_data | without_results | without_upload_meta_data | without_uploads>Controls the size of the payload sent in Assembly notifications (both the initial notification and any notification replays). An empty array (default) sends the complete Assembly status. Add
"without_params"to strip raw Assembly instruction JSON fields (params,template, andmerged_params). Add"without_upload_meta_data"to stripmetafrom files inuploads. Add"without_result_meta_data"to stripmetafrom files inresults. Add"without_uploads"to stripuploadsentirely. Add"without_results"to stripresultsentirely. Filtering is applied before serialization, so using these options reduces memory use and helps avoid oversized-payload or OOM failures on large assemblies.
You can combine multiple values in the same array. This also applies when you replay notifications from the Assembly page.
What does this POST request look like?
This multipart POST request contains a field called transloadit, which contains the full
Assembly Status JSON. You can find an example of this in our
API Response docs. It will also contain a
signature field. Verify it with the Auth Secret belonging
to the Auth Key used for the original Assembly before trusting the payload. Verification must use
the exact transloadit field string, before parsing or reserializing its JSON.
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.
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 instead. If you'd like to see how signature verification works under the hood, you can view the signature utilities source code.
This example uses Node.js 24 or newer, its built-in multipart parser, and the official signature verifier. Install its dependencies:
yarn add @transloadit/utils@4.7.1 zod@3.25.76
Use an ES module project ("type": "module" in package.json) and save the following as
notification-backend-node.ts:
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
import { verifyWebhookSignature } from '@transloadit/utils'
import { z } from 'zod'
const authSecret = process.env.AUTH_SECRET
if (!authSecret) throw new Error('Set AUTH_SECRET to the secret used for the original Assembly')
const port = Number(process.argv[2] ?? '3020')
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid port')
const maxBodyBytes = 1024 * 1024
const notificationSchema = z.object({ assembly_id: z.string().regex(/^[a-f0-9]{32}$/u) })
function respond(res: ServerResponse, status: number, message: string): void {
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8', Connection: 'close' })
res.end(message)
}
async function receive(req: IncomingMessage, res: ServerResponse, secret: string): Promise<void> {
if (req.url !== '/transloadit_pingback' || req.method !== 'POST') {
return respond(res, 404, 'Not found')
}
const contentType = req.headers['content-type'] ?? ''
if (contentType.split(';')[0].trim().toLowerCase() !== 'multipart/form-data') {
return respond(res, 415, 'Expected multipart form data')
}
const chunks: Buffer[] = []
let bytes = 0
// Keep the socket open long enough to send 413 when rejecting a streaming request.
for await (const chunk of req.iterator({ destroyOnReturn: false })) {
if (!Buffer.isBuffer(chunk)) throw new Error('Expected a binary request stream')
bytes += chunk.length
if (bytes > maxBodyBytes) return respond(res, 413, 'Notification too large')
chunks.push(chunk)
}
let assemblyId: string
try {
const form = await new Request('http://localhost/transloadit_pingback', {
method: 'POST',
headers: { 'Content-Type': contentType },
body: Buffer.concat(chunks),
}).formData()
const payload = form.get('transloadit')
const signature = form.get('signature')
if (
form.getAll('transloadit').length !== 1 ||
form.getAll('signature').length !== 1 ||
typeof payload !== 'string' ||
typeof signature !== 'string'
) {
return respond(res, 400, 'Expected one payload and one signature')
}
if (!(await verifyWebhookSignature({ rawBody: payload, signatureHeader: signature, authSecret: secret }))) {
return respond(res, 403, 'Invalid signature')
}
assemblyId = notificationSchema.parse(JSON.parse(payload)).assembly_id
} catch {
return respond(res, 400, 'Invalid notification')
}
console.log(`Verified notification for Assembly ${assemblyId}`)
respond(res, 200, 'Accepted')
}
const server = createServer({ requestTimeout: 30_000 }, (req, res) => {
receive(req, res, authSecret).catch(() => respond(res, 500, 'Unable to accept notification'))
})
server.listen(port, '127.0.0.1', () => {
const address = server.address()
if (address && typeof address !== 'string') {
console.log(`Server started, listening on http://127.0.0.1:${address.port}`)
}
})
This verification demo logs only the Assembly ID. It accepts payloads that omit uploads or
results, and caps the entire request at 1 MiB. Adjust that limit for your expected payloads or use
notification_payload to reduce their size.
Before using this in production, check that the Assembly belongs to the operation and Workspace you
expect, validate the fields your application consumes, and durably store or enqueue the verified
notification before returning 200. Handle repeated notifications idempotently. A valid signature
does not make a notification new, nor does this logging demo persist results. Keep the Auth Secret
server-side and put the endpoint behind your HTTPS reverse proxy.
Run the server with the Auth Secret provided through your environment:
$ env AUTH_SECRET=******** node notification-backend-node.ts 3020
Server started, listening on http://127.0.0.1:3020
Trying the Code Example locally
While testing locally behind a NAT, use Cloudflare Tunnel, ngrok, or the first-party @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:
$ 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 and paste the following Instructions:
{
"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 or ngrok.
At the time of writing ngrok appears to have issues with AWS ranges. If this is the case for you, an alternative to try is Cloudflare Tunnel or the first-party @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 the Assembly completes:
Verified notification for Assembly 0123456789abcdef0123456789abcdef
The Assembly ID in your output will match the Assembly you created.
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.