Skip to content

Updated Oct 12, 2026

Signed webhooks

Send your request events to your systems and verify the HMAC-SHA256 signature on every delivery.

A webhook tells another system, such as Zapier or your production sheet, when something happens on a request. Each delivery is signed with a secret so you can check it comes from Promptusphere. Only the Owner and Admin roles can set them up.

Add an endpoint

  1. 1. Go to Settings, Webhooks and choose Add endpoint.
  2. 2. Type the Endpoint URL. It must start with https:// and be public.
  3. 3. Pick the Events: request.created, request.status_changed, request.ready, request.completed, approval.recorded or quote.decided.
  4. 4. Save. We show you the signing secret only once: choose Copy secret, store it in your system and choose I saved it.
  5. 5. Choose Send test event to confirm it arrives.

Verify the signature

Every delivery includes this header:

Promptusphere-Signature: t=<unix seconds>,v1=<hex>
  1. 1. Split out t (seconds since 1970) and v1 (the signature in hexadecimal).
  2. 2. Reject the event if the difference between the current time and t is more than 300 seconds (5 minutes). This prevents replays.
  3. 3. Compute HMAC-SHA256 with your secret over the text t + . + the raw request body, before parsing it as JSON. Express the result in hexadecimal.
  4. 4. Compare your result with v1 in constant time. If they don't match, respond with an error and don't process the event.

This JavaScript example uses Web Crypto (crypto.subtle):

// Verifies the Promptusphere-Signature header with Web Crypto.
// Works in Cloudflare Workers, Deno and Node 20+ (in Node 18, use webcrypto from node:crypto).
const TOLERANCE_SECONDS = 300;
const encoder = new TextEncoder();

function toHex(buffer) {
  return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

function timingSafeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

export async function verifyPromptusphere(rawBody, header, secret) {
  if (!header) return false;
  const parts = {};
  for (const piece of header.split(",")) {
    const [key, value] = piece.trim().split("=");
    if (key && value) parts[key] = value;
  }
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - t) > TOLERANCE_SECONDS) return false;

  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(t + "." + rawBody));
  return timingSafeEqual(toHex(mac), parts.v1);
}

// Example: a Cloudflare Worker that receives the webhook.
export default {
  async fetch(request, env) {
    const rawBody = await request.text(); // raw body, before JSON.parse
    const header = request.headers.get("Promptusphere-Signature");
    const ok = await verifyPromptusphere(rawBody, header, env.PROMPTUSPHERE_WEBHOOK_SECRET);
    if (!ok) return new Response("invalid signature", { status: 400 });

    const event = JSON.parse(rawBody);
    // event.id, event.type (for example request.ready), event.createdAt, event.data
    return new Response("ok");
  },
};

Deliveries and retries

  • Respond with a 2xx code within 10 seconds. Any other result counts as a failure.
  • We retry failed deliveries several times, with longer waits each time.
  • View deliveries opens the Delivery log with the latest 50, their result and attempts. Use Redeliver to send one again.
  • Each delivery also carries the Promptusphere-Event and Promptusphere-Delivery headers. Use the event id to ignore duplicates.

soporte@promptusphere.com

Still have questions?

Write to us with the request name or the screen you were on and we'll help you look into it.

Include the request name or the page where it happened. Between 10 and 4,000 characters.

Abuse protection check

Loading the check…