Notion button send webhook: put the video URL back on the page
A Notion button can send a webhook that starts a Sume video job. When Sume calls back, PATCH the page's URL property with the finished video.

A Notion button can run a Send webhook action, which sends an HTTP POST to your URL. Getting a finished Sume video back onto the page is your endpoint's job, done through Notion's API. So have your endpoint submit the job with a callback_url, then update the page yourself from Sume's job.completed callback with Notion's PATCH /v1/pages/{page_id}, writing the video link into a URL property.
Notion facts come from its help pages for database buttons and database automations and its developer pages for Update page, the property value object and request limits, read 2026-09-29. Sume facts come from Webhooks and Videos. This post starts after the click.
How does a Notion button start the job?
Database buttons are a property whose actions run when someone clicks: Notion lists Send webhook among the button actions, available on paid plans, and it sends a POST to the URL you enter. Two limits shape the design. Notion's webhook page says database button properties can't be selected as webhook content, and it doesn't document the body a button sends. So inspect one real delivery before you depend on a field.
The automations page gives a second route: button actions can trigger database automations, and a database automation can send selected page properties. Both routes end at the same place, an endpoint of your own that submits the Sume job and remembers which page it was for.
How do I write the video URL back to the page?
When the job finishes, Sume POSTs a job.completed event. Its payload.artifacts entries carry a url. Verify the signature on the raw body, find the page you saved at submit time, and PATCH one URL property. Notion's property page shows a URL write as { "url": "https://..." } under the property's name.
import { verifyWebhook } from "@sume-com/sdk";
export async function POST(request: Request) {
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET;
if (!secret) return new Response("not configured", { status: 500 });
const body = await request.text(); // raw, before any JSON.parse
if (!(await verifyWebhook({ body, headers: request.headers, secret }))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(body);
const url = event.payload?.artifacts?.[0]?.url;
if (event.event !== "job.completed" || !url) return new Response(null, { status: 204 });
const pageId = await pageIdForJob(event.job_id); // saved when you submitted
const res = await fetch(`https://api.notion.com/v1/pages/${pageId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
"Notion-Version": process.env.NOTION_VERSION!,
"Content-Type": "application/json",
},
body: JSON.stringify({ properties: { "Video URL": { url } } }),
});
// 429/529: answer 503 so Sume redelivers later; the same PATCH is safe to repeat.
return new Response(null, { status: res.ok ? 204 : 503 });
}Which Notion property should hold the video?
A URL property is the simplest. If you want a playable attachment, the Files & media property takes an external URL.
| Property | Write shape | Notes |
|---|---|---|
| URL | { "url": "https://example.com/v.mp4" } | A URL string or null |
| Files & media | { "files": [ { "name": "...", "external": { "url": "..." } } ] } | A file update replaces the full list, so include files you want to keep |
What about Notion's rate limit and Sume's retries?
Notion limits each connection to 180 requests per minute, an average of 3 per second, or 600 per minute on Business and Enterprise workspaces. Over the limit you get HTTP 429 with a Retry-After header in seconds; Notion also documents 529 for temporary overload. Sume retries a callback that gets a non-2xx response, up to 10 attempts 30 seconds apart by default, with 10 seconds allowed per attempt, so answering 503 on a Notion 429 is a workable backoff. Dedupe on job_id.
Sources
Related posts
More in Integrations
- OpenAI Agents SDK human in the loop for paid tools
Set needs_approval on a function tool that starts a paid video job. The run pauses with result.interruptions until you approve or reject the call.
- p-retry npm: retry a paid API POST and stop on errors
p-retry reruns an async function with exponential backoff. Throw AbortError on answers a resend can't fix, and keep one Idempotency-Key per job.
- PHP cURL POST JSON with a Bearer token
json_encode the body, pass the string to CURLOPT_POSTFIELDS, set Content-Type and Authorization headers, then check the status: cURL won't fail on a 4xx.
- Polly retry policy for an HttpClient POST to a paid API
A Polly retry for a paid POST: handle only transient failures, back off exponentially with jitter, honor Retry-After, and resend one idempotency key.
Written by Sume