space.hooks.verify()

Check that a webhook your server received was sent by us, unchanged and recently.

Checks a delivery your server received from a destination: that it carries a valid Standard Webhooks signature made with the destination’s signing secret, over the body exactly as it was sent, and that it was sent no more than 5 minutes ago. Anyone who knows your URL can send it a request, so check every delivery before you act on it.

verify() runs on your own server and makes no request. It needs no API key and no project, only the destination’s signing secret.

space.hooks.verify(
  request: Request | {
    headers: Headers | Record<string, string | undefined>
    body: string | Uint8Array
  },
  options: {
    secret: string
    toleranceSeconds?: number
    now?: number
  }
): Promise<VerifiedDelivery>

Parameters

requestRequest | { headers, body }Required

The delivery as your server received it. Pass a Request from any server that gives you one. From another server, pass its headers, as Headers or an object of names and values in any case, and its body as the raw bytes or text exactly as received.

verify() reads the webhook-id, webhook-timestamp, and webhook-signature headers.

options.secretstringRequired

The destination’s signing secret, which starts with whsec_. Read it with space.hooks.destinations.signingSecret() and keep it in your server’s settings, such as an environment variable.

options.toleranceSecondsinteger

How far the delivery’s webhook-timestamp may be from now, in either direction, 1 to 86,400 seconds. 300, which is 5 minutes, when you leave it out. It stops someone from sending an old delivery again.

options.nownumber

The time to check against, in milliseconds since 1 January 1970. The current time when you leave it out. Use it in tests.

Returns

A promise of the checked delivery.

idstring

The webhook-id header: the ID of the event. It is the same on every attempt and every time the delivery is sent again, so keep the IDs you have handled and skip a delivery you have already seen.

timestampDate

When this attempt was sent, from the webhook-timestamp header.

bodystring

The body, as UTF-8 text. When you pass a Request, verify() reads its body, so read the body from here.

json()<T = unknown>() => T

Parses body as JSON. It throws a SyntaxError when the body is not JSON.

Errors

The promise rejects with a SpaceError.

CodeWhen
invalid_signatureA signing header is missing, webhook-timestamp is not a whole number of seconds, the delivery was sent more than toleranceSeconds ago or that far in the future, or no signature matches the secret and the body. status is 401
validation_failedsecret does not start with whsec_ followed by base64, or toleranceSeconds is not a whole number from 1 to 86,400

Answer an invalid_signature with 401 and do nothing else. When a real delivery fails the check, look for a secret from another destination, a secret that was reset, a body changed before the check, or a server clock that is more than 5 minutes out.

Examples

Any server that hands you a Request and takes a Response.

payments-webhook.ts
import { SpaceError, space } from "digitospace"

type Payment = {
  reference: string
  amount: number
  msisdn: string
  status: "COMPLETED" | "FAILED"
}

const handled = new Set<string>()

export async function POST(request: Request): Promise<Response> {
  try {
    const delivery = await space.hooks.verify(request, {
      secret: process.env.WEBHOOK_SECRET ?? "",
    })
    if (handled.has(delivery.id)) return new Response(null, { status: 200 })
    const payment = delivery.json<Payment>()
    if (payment.status === "COMPLETED") {
      console.log(`Order ${payment.reference} paid TZS ${payment.amount} from ${payment.msisdn}`)
    }
    handled.add(delivery.id)
    return new Response(null, { status: 200 })
  } catch (error) {
    if (error instanceof SpaceError && error.code === "invalid_signature") {
      return new Response(null, { status: 401 })
    }
    throw error
  }
}

Answer with a 2xx as soon as the delivery is checked and saved, and do slow work afterwards. An answer that takes longer than the destination’s timeoutSeconds, or any answer that is not 2xx, counts as a failed attempt and the delivery is tried again.

Last updated