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. Go to Settings, Webhooks and choose Add endpoint.
- 2. Type the Endpoint URL. It must start with
https://and be public. - 3. Pick the Events:
request.created,request.status_changed,request.ready,request.completed,approval.recordedorquote.decided. - 4. Save. We show you the signing secret only once: choose Copy secret, store it in your system and choose I saved it.
- 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. Split out
t(seconds since 1970) andv1(the signature in hexadecimal). - 2. Reject the event if the difference between the current time and
tis more than 300 seconds (5 minutes). This prevents replays. - 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. Compare your result with
v1in 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-EventandPromptusphere-Deliveryheaders. Use the eventidto 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.