Verify calls to your app
Check that a scheduler call or a webhook delivery to your endpoint came from us, and refuse everything else.
Anyone who knows your endpoint’s address can call it. The scheduler and webhooks sign what they send, and the SDK checks the signature for you:
space.schedule.verifychecks a call the scheduler makes to one of your job’s targets.space.hooks.verifychecks a webhook delivery to one of your destinations.
Neither needs an API key or a project. The check happens in your app. Both take the incoming Request, or its headers and raw body from any framework.
Verify a scheduler call
Sign the job’s calls
A job made with
space.schedule.everysigns its calls unless you passsigned: false. Each call then carries a token in itsAuthorizationheader.create-job.ts import { space } from "digitospace" const job = await space.schedule.every( "@daily", "https://shop.example.com/tasks/reminders" ) console.log(job.id)The token is for the job’s audience: the target URL without its query, unless you set
audienceon the job.Check the call in your endpoint
Pass the request and the audience to
space.schedule.verify. Name the jobs that may call this endpoint injobs, so a token made for another job is refused even when its audience matches.app/tasks/reminders/route.ts import { isSpaceError, space } from "digitospace" const jobs = [process.env.REMINDERS_JOB_ID ?? ""] export async function POST(request: Request): Promise<Response> { try { const call = await space.schedule.verify(request, { audience: "https://shop.example.com/tasks/reminders", jobs, }) console.log(`Called by job ${call.job} in project ${call.project}`) return new Response(null, { status: 204 }) } catch (error) { if (!isSpaceError(error)) throw error if (error.code === "invalid_signature") return new Response(null, { status: 401 }) if (error.code === "unexpected_job") return new Response(null, { status: 403 }) if (error.code === "keys_unavailable") return new Response(null, { status: 503 }) throw error } }A token that is missing, badly signed, expired, from another issuer, or for another audience fails with
invalid_signatureand a status of401. A token from a job not injobsfails withunexpected_joband403.
The SDK fetches the scheduler’s public keys and keeps them for 5 minutes, so most calls are checked without a request. When it cannot fetch them, it fails with keys_unavailable. Answering 503 fails the attempt, so the scheduler tries again if the job’s retry policy allows it.
See verify() for every option and field, and Verify calls to your target for how the token is made.
Verify a webhook delivery
Get the destination’s signing secret
Every delivery to a destination is signed with that destination’s signing secret, which starts with
whsec_. Copy it from the destination in the console, or read it with the SDK:signing-secret.ts import { space } from "digitospace" const secret = await space.hooks.destinations.signingSecret( process.env.DESTINATION_ID ?? "" ) console.log(secret)Keep it in your app’s secret settings, such as
WEBHOOK_SECRET.Check the delivery in your endpoint
Pass the request and the secret to
space.hooks.verify. It reads the body, so read the delivery from what it returns rather than from the request.app/webhooks/payments/route.ts import { isSpaceError, space } from "digitospace" interface Payment { id: string amount: number } export async function POST(request: Request): Promise<Response> { try { const delivery = await space.hooks.verify(request, { secret: process.env.WEBHOOK_SECRET ?? "", }) const payment = delivery.json<Payment>() console.log(`Delivery ${delivery.id} for payment ${payment.id}`) return new Response(null, { status: 204 }) } catch (error) { if (isSpaceError(error) && error.code === "invalid_signature") return new Response(null, { status: 401 }) throw error } }A delivery with a missing or wrong signature, or a timestamp more than 5 minutes from your server’s clock, fails with
invalid_signatureand a status of401. Change the 5 minutes withtoleranceSeconds.
See verify() for every option and field.
Verify with the raw body
When your framework gives you headers and a body rather than a Request, pass them as an object. The body must be exactly the bytes that arrived, as a string or a Uint8Array, before any JSON parsing.
import { createServer } from "node:http"
import { isSpaceError, space } from "digitospace"
createServer(async (request, response) => {
const chunks: Buffer[] = []
for await (const chunk of request) chunks.push(chunk as Buffer)
try {
const delivery = await space.hooks.verify(
{
headers: request.headers as Record<string, string | undefined>,
body: new Uint8Array(Buffer.concat(chunks)),
},
{ secret: process.env.WEBHOOK_SECRET ?? "" }
)
console.log(delivery.body)
response.writeHead(204).end()
} catch (error) {
const refused = isSpaceError(error) && error.code === "invalid_signature"
response.writeHead(refused ? 401 : 500).end()
}
}).listen(3000)space.schedule.verify takes the same object form, and needs only headers.
Last updated