Your endpoint must respond with a
2xx status code to acknowledge receipt. If Portal.io does not receive a 2xx response, it retries the delivery on this schedule: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours.Verifying Webhook Signatures
Every delivery includes anX-Webhook-Signature header you should use to verify the request came from Portal.io:
t— Unix timestamp when the webhook was generated.v1— HMAC-SHA256 hex digest.
- Read the raw request body exactly as received.
- Extract
tandv1from theX-Webhook-Signatureheader. - Rebuild the signed message:
t + "." + rawBody. - Compute
HMAC_SHA256(secretKey, signedMessage). - Compare your result to
v1. Reject the request if they do not match. - Reject the request if
tis more than 5 minutes old.
secretKey is the value returned in the secretKey field when you created the subscription.
Payload Envelope
All webhook events use a standard envelope format:envelope (data trimmed)
Event Types
Use these values in the repeatedEvents body parameter when creating a webhook subscription.
proposal.build.status_update
AI proposal builder finishes or fails.
proposal.outline.status_update
AI outline generation completes or fails.
proposal.status_changed
A proposal’s status changes (e.g. Draft → Submitted).
catalog.items_updated
Prices change on catalog items your account holds.
proposal.build.status_update
Fired when the Portal.io AI builder finishes generating a proposal or encounters an error during generation. Subscribe to this event if you need to react when a build completes asynchronously.Payload Fields
Thedata object contains:
proposal.outline.status_update
Fired when Portal.io’s AI outline generation completes or fails. Useful for workflows that wait for an AI-generated scope before proceeding.Payload Fields
Thedata object contains:
proposal.status_changed
Fired whenever a proposal’s status changes. Common transitions include Draft → Submitted, Submitted → Accepted, and Accepted → Completed.Payload Fields
Thedata object contains the full proposal detail. See Proposal Object Fields for the complete structure.
catalog.items_updated
Fired when prices change on catalog items your account holds — through a supplier catalog import, an edit you make to your own catalog, or a future price reaching its effective date. Subscribe to this event to keep a local copy of your catalog pricing current without polling. One event carries every item repriced in the same update. A large update is split across several events, so a single import can deliver more than one.Payload Fields
Thedata object contains:
A percentage and its basis arrive as a single entry, never two. If only the percentage changed,
fromBasis and toBasis match; if only the basis changed, from and to match.Cost entry — and a SellPrice entry derived from one — reflects the supplier your account buys the item from, the same cost the Catalog API returns. A cost change at any other supplier you hold does not produce an entry, and no supplier is named in the payload.
Proposal Object Fields
The proposal object included inproposal.build.status_update and proposal.status_changed payloads shares the same structure.