Collect files when a webhook arrives
Give this prompt to your AI agent. The agent does the task for you.
Read https://thirds.ai/docs/integrations/webhook-delivery and help me build a signed webhook receiver and collect finished files without creating duplicate renders
Let your server collect a finished PDF or image as soon as thirds.ai sends its result. The receiver saves each verified event to a private inbox. A separate worker reads the job and downloads the file. Neither step starts a render.
Set up the receiver
You need a server you control and an HTTPS proxy in front of it. Follow webhook registration to register your public https://YOUR_RECEIVER.example/thirds-events URL. Save the returned secret as THIRDS_WEBHOOK_SECRET in the receiver's secret store. The receiver doesn't need your API key.
Build the receiver with the Node or Python verifier from the webhook guide. Your receiver does four things, in this order:
- It reads the raw body and the
thirds-signatureheader. - It checks the signature before it parses the JSON. It rejects an event that fails.
- It saves the event to a private inbox, keyed by the event
id. A repeated ID adds no new work. - It returns a small
2xxresponse after the save.
Let the process listen on 127.0.0.1. Set your HTTPS proxy to forward only POST requests for /thirds-events to that local port. Preserve the raw body and thirds-signature header. Keep the proxy's request size and time limits enabled. Don't log request bodies, signing secrets, or authorization headers.
Use durable storage for the inbox, and keep the receiver's clock in sync. The webhook guide owns the signature and delivery rules.
Start an approved output
Complete the API workflow recipe with your template and data. Register the webhook before you create the output. Pressing your app's create button or running its explicit render command authorizes that render. Loading a page or receiving an event must never create one.
The event identifies a job. It is not the file itself. Your worker uses the job ID to read the authoritative result from thirds.ai. It must not send your API key to a URL supplied by an unchecked event.
Collect the inbox
Set THIRDS_API_KEY in the worker's secret store. Use a key from the same account as the webhook destination and render. Run the worker from your existing job runner after events arrive.
For each saved event, the worker takes data.job.id and data.job.output. It builds the job URL itself from https://thirds.ai, so it never trusts a URL from the event. Read a PDF job like this, or use /v1/image/{id} when output is image:
curl --fail-with-body --silent --show-error \
https://thirds.ai/v1/pdf/pdf_00000000000000000000000000000000 \
-H 'Authorization: Bearer YOUR_API_KEY'const response = await fetch("https://thirds.ai/v1/pdf/pdf_00000000000000000000000000000000", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY"
},
redirect: "error",
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}; Retry-After: ${response.headers.get("retry-after") ?? "none"}`);
console.log(await response.text());import json
from urllib.error import HTTPError
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
request = Request("https://thirds.ai/v1/pdf/pdf_00000000000000000000000000000000",
method="GET",
headers={
"Authorization": "Bearer YOUR_API_KEY"
},
)
try:
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(response.read().decode("utf-8"))
except HTTPError as error:
retry = error.headers.get("Retry-After", "none")
raise RuntimeError(f"HTTP {error.code}; Retry-After: {retry}") from NoneWhen status is succeeded, add https://thirds.ai before download.url and save the file. The download doesn't need your API key:
curl --fail --silent --show-error \
'https://thirds.ai/v1/downloads/YOUR_SIGNED_DOWNLOAD_TOKEN' \
--output report.pdfimport { writeFile } from "node:fs/promises";
const response = await fetch("https://thirds.ai/v1/downloads/YOUR_SIGNED_DOWNLOAD_TOKEN", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}; Retry-After: ${response.headers.get("retry-after") ?? "none"}`);
await writeFile("report.pdf", Buffer.from(await response.arrayBuffer()));import json
from urllib.error import HTTPError
from pathlib import Path
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
request = Request("https://thirds.ai/v1/downloads/YOUR_SIGNED_DOWNLOAD_TOKEN",
method="GET",
headers={
},
)
try:
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("report.pdf").write_bytes(response.read())
except HTTPError as error:
retry = error.headers.get("Retry-After", "none")
raise RuntimeError(f"HTTP {error.code}; Retry-After: {retry}") from NoneCompare the file's byte count and SHA-256 with artifact.byte_size and artifact.sha256. Mark the event done only after the check passes. A failed or cancelled job has no file, so record that result and mark the event done.
Open a sample collected file before you connect your own delivery step. Keep the job ID, byte size, and SHA-256 with the source record, then use the storage recipe. Email and customer delivery remain steps in your own app.
Recover without another render
| What happens | What to do |
|---|---|
| Signature fails | Check the secret and clock. Ensure the proxy does not change the body. |
| Receiver cannot save an event | Restore its disk access and space. Let thirds.ai retry delivery. |
| The same event arrives twice | Keep the inbox and worker state. The repeat refers to the same job. |
| Download or worker stops | Run the worker again. It reads the same job for a fresh link. |
| A result event is missed | Read the known job directly or replay delivery through the webhook API. |
| Job fails or its artifact expires | Follow the shared recovery guide before you authorize new work. |
Webhook recovery explains retries, replay, disabled destinations, and secret rotation. Download recovery explains fresh links and file expiry. Repairing delivery does not need another billed render.