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:
X-Webhook-Signature: t=1710000000,v1=5f2b3e7a9d1c4f8e6b2a3c9d7f4e1a6b5c3d2f7e9a1b4c6d8e0f2a3b5c7d9e1
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:{
"id": "evt_abc123",
"type": "proposal.build.status_update",
"created": "2026-04-06T00:22:19Z",
"data": { ... }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique webhook delivery identifier, prefixed with evt_. |
type | string | Event type identifier (dot-notation). |
created | string (ISO 8601) | UTC timestamp when the event was generated. |
data | object | Event-specific payload. |
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).
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:
| Field | Type | Description |
|---|---|---|
data.status | string | Build result status. One of Building, Completed, Failed. |
data.proposal | object | Full proposal detail snapshot (see proposal object fields below). |
{
"id": "evt_build_abc123",
"type": "proposal.build.status_update",
"created": "2026-04-06T00:22:19Z",
"data": {
"status": "Completed",
"proposal": {
"id": 4123,
"number": 1001,
"name": "Smith Residence AV",
"status": "Draft",
"createdDate": "2026-04-05T18:00:00Z",
"lastModifiedDate": "2026-04-06T00:22:19Z",
"financialSummary": {
"partsSubtotal": 5200.00,
"partsTotal": 5200.00,
"laborTotal": 800.00,
"feeTotal": 0,
"proposalSubtotal": 6000.00,
"proposalTotal": 6450.00
}
}
}
}
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:
| Field | Type | Description |
|---|---|---|
data.proposalId | integer | ID of the proposal whose outline changed. |
data.status | string | Outline generation status. One of Generating, Completed, Failed. |
data.outline | string or null | The generated outline text. Only populated when status is Completed. |
{
"id": "evt_outline_def456",
"type": "proposal.outline.status_update",
"created": "2026-04-06T00:20:00Z",
"data": {
"proposalId": 4123,
"status": "Completed",
"outline": "1. Living Room AV System\n2. Master Bedroom Audio\n3. Outdoor Speakers"
}
}
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.
{
"id": "evt_status_ghi789",
"type": "proposal.status_changed",
"created": "2026-04-06T00:22:19Z",
"data": {
"id": 4123,
"number": 1001,
"name": "Smith Residence AV",
"status": "Accepted",
"createdDate": "2026-04-05T18:00:00Z",
"lastSubmittedDate": "2026-04-05T20:00:00Z",
"clientLastOpenedDate": "2026-04-06T00:10:00Z",
"clientLastDecisionDate": "2026-04-06T00:22:00Z",
"lastCompletedDate": null,
"lastModifiedDate": "2026-04-06T00:22:19Z",
"lastModifiedByUserDate": "2026-04-06T00:22:19Z",
"coverpageImageUrl": "https://files.portal.io/covers/4123.jpg",
"aboutUs": "We specialize in custom AV installations.",
"projectDescription": "Full home AV system including living room, bedroom, and outdoor areas.",
"projectTerms": "Net 30 payment terms apply.",
"lastModifiedUser": {
"firstName": "John",
"lastName": "Doe"
},
"customer": {
"id": 88,
"partyType": "Person",
"contactType": "Client",
"firstName": "Jane",
"lastName": "Smith",
"companyName": "",
"contactEmail": "jane.smith@example.com",
"contactEmailCC": "",
"contactPhone": "(555) 123-4567",
"location": {
"id": 1,
"street": "123 Main St",
"suite": "Apt 4B",
"city": "Austin",
"postalCode": "78701",
"state": "Texas",
"stateAbbrev": "TX",
"country": "United States",
"phone": "(555) 123-4567"
}
},
"dealer": {
"companyName": "Premier AV Solutions",
"location": {
"id": 10,
"street": "500 Commerce Dr",
"suite": "Suite 200",
"city": "Dallas",
"postalCode": "75201",
"state": "Texas",
"stateAbbrev": "TX",
"country": "United States",
"phone": "(555) 987-6543"
},
"salesperson": {
"firstName": "John",
"lastName": "Doe",
"id": 5,
"email": "john@premierav.com"
},
"webSiteUrl": "https://premierav.com",
"companyPhone": "(555) 987-6543",
"companyLogoUrl": "https://files.portal.io/logos/premierav.png"
},
"financialSummary": {
"partsSubtotal": 5200.00,
"partsDiscountType": "Percentage",
"partsDiscountPercentage": 0,
"partsDiscount": 0,
"partsDiscountTaxable": 0,
"partsDiscountTaxExempt": 0,
"partsTotal": 5200.00,
"laborTotal": 800.00,
"feeTotal": 0,
"proposalSubtotal": 6000.00,
"salesTax": {
"taxStatus": "Ok",
"total": 450.00,
"calculation": {
"method": "FixedPercentage",
"applyTo": ["Parts", "Labor"],
"partsTax": 8.25,
"partsTaxName": "State Tax",
"laborTax": 8.25,
"laborTaxName": "State Tax",
"hasMultipleTaxSupport": false,
"partsTax2": null,
"partsTax2Name": null,
"laborTax2": null,
"laborTax2Name": null,
"feeTax": null,
"feeTaxName": null,
"feeTax2": null,
"feeTax2Name": null,
"taxLocation": null,
"isTaxJarAvailable": false,
"partsTotalTax": 429.00,
"laborTotalTax": 66.00,
"feeTotalTax": null
}
},
"proposalTotal": 6450.00,
"currency": {
"code": "USD",
"symbol": "$"
}
},
"areas": [
{
"id": 1,
"name": "Living Room",
"options": [
{
"id": 1,
"status": "Accepted",
"lastModifiedDate": "2026-04-06T00:22:19Z",
"clientDescription": "Full surround sound system",
"installerDescription": "Install 5.1 system with in-wall wiring",
"items": [
{
"id": 101,
"parentId": null,
"itemType": "Part",
"referencedItemId": 500,
"createdDate": "2026-04-05T18:00:00Z",
"lastModifiedDate": "2026-04-05T18:00:00Z",
"brand": "Sonos",
"model": "Arc",
"description": "Premium Smart Soundbar",
"name": "Sonos Arc",
"shortDescription": "Soundbar",
"clientNote": null,
"imageUrl": "https://files.portal.io/items/500.jpg",
"msrp": 999.00,
"sellPrice": 899.00,
"cost": 650.00,
"costUpdateDate": "2026-03-01T00:00:00Z",
"supplier": "Acme Audio Distributors",
"quantity": 1,
"total": {
"amount": 899.00,
"currency": {
"code": "USD",
"symbol": "$"
},
"isCombinedPrice": false
},
"isTaxExempt": false,
"isRecurringService": false,
"linkedOrders": [
{
"orderId": 7001,
"orderNumber": 301,
"orderNumberSuffix": "A",
"supplier": "Acme Audio Distributors",
"supplierRef": "PO-12345",
"orderName": "Smith AV Equipment Order",
"orderStatus": "Submitted"
}
]
}
],
"total": 5200.00,
"totalRecurringService": 0
}
]
}
],
"profit": {
"total": 1350.00,
"percentage": 22.5,
"partTotal": 1050.00,
"partPercentage": 20.19,
"laborTotal": 300.00,
"laborPercentage": 37.5,
"isProfitIncludeCos": false
},
"recurringServices": null,
"paymentSchedule": {
"customerDescription": "50% deposit, 50% on completion",
"payments": [
{
"calculation": "Percentage",
"amount": 3225.00,
"due": {
"date": null,
"milestone": "Upon acceptance"
}
},
{
"calculation": "Percentage",
"amount": 3225.00,
"due": {
"date": null,
"milestone": "Upon completion"
}
}
]
},
"paymentRequests": [
{
"id": 3042,
"status": "Paid",
"amount": 3225.00,
"dueDate": "2026-04-20T00:00:00Z",
"description": "50% deposit",
"paymentMethod": "Stripe"
}
],
"changeOrders": []
}
}
Proposal Object Fields
The proposal object included inproposal.build.status_update and proposal.status_changed payloads shares the same structure.
Top-Level Fields
| Field | Type | Description |
|---|---|---|
id | integer | Unique identifier for the proposal. |
number | integer | Human-readable proposal number, unique within the account. |
name | string | Display name of the proposal. |
status | string | Current proposal status (see proposal statuses). |
createdDate | string (ISO 8601) | When the proposal was created. |
lastSubmittedDate | string or null | When the proposal was last submitted to the client. |
clientLastOpenedDate | string or null | When the client last opened/viewed the proposal. |
clientLastDecisionDate | string or null | When the client last approved or declined. |
lastCompletedDate | string or null | When the proposal was last marked complete. |
lastModifiedDate | string (ISO 8601) | When the proposal was last modified by any actor. |
lastModifiedByUserDate | string (ISO 8601) | When the proposal was last modified by a human user (not system). |
coverpageImageUrl | string or null | URL of the proposal cover page image. |
aboutUs | string or null | ”About Us” text included in the proposal. |
projectDescription | string or null | Project description text. |
projectTerms | string or null | Terms and conditions text. |
lastModifiedUser | object | User who last modified the proposal (see user fields). |
customer | object or null | Customer contact (see customer fields). |
dealer | object | Dealer account (see dealer fields). |
financialSummary | object or null | Financial breakdown (see financial summary). |
areas | array or null | Proposal areas with options and line items (see area fields). |
profit | object or null | Profit breakdown (see profit fields). |
recurringServices | object or null | Recurring services summary (see recurring services). |
paymentSchedule | object or null | Payment schedule (see payment schedule). |
paymentRequests | array or null | Payment requests (see payment requests). |
changeOrders | array | Associated change orders (see change order fields). |
Proposal Statuses
| Value | Description |
|---|---|
Undefined | Unset/default status. |
Draft | Proposal is being edited, not yet sent to client. |
Submitted | Proposal has been sent to the client. |
ViewedByClient | Client has opened/viewed the proposal. |
Accepted | Client has accepted the proposal. |
Declined | Client has declined the proposal. |
Delayed | Proposal has been delayed. |
Completed | Proposal is marked complete. |
EmailFailed | Delivery email to client failed. |
Expired | Proposal has expired. |
Customer Fields
| Field | Type | Description |
|---|---|---|
id | integer | Contact ID. |
partyType | string | Person or Company. |
contactType | string | Contact classification (e.g. Client). |
firstName | string or null | First name. |
lastName | string or null | Last name. |
companyName | string or null | Company name. |
contactEmail | string or null | Primary email address. |
contactEmailCC | string or null | CC email address. |
contactPhone | string or null | Primary phone number. |
location | object or null | Primary location (see location fields). |
Location Fields
| Field | Type | Description |
|---|---|---|
id | integer or null | Location ID. |
street | string or null | Street address. |
suite | string or null | Suite or unit number. |
city | string or null | City. |
postalCode | string or null | ZIP or postal code. |
state | string or null | Full state name. |
stateAbbrev | string or null | Two-letter state abbreviation. |
country | string or null | Country name. |
phone | string or null | Location phone number. |
Dealer Fields
| Field | Type | Description |
|---|---|---|
companyName | string | Company name. |
location | object | Company location (see location fields). |
salesperson | object or null | Assigned salesperson (see salesperson fields). |
webSiteUrl | string or null | Company website URL. |
companyPhone | string or null | Company phone number. |
companyLogoUrl | string or null | URL of the company logo. |
Salesperson Fields
Extends user fields with:| Field | Type | Description |
|---|---|---|
firstName | string | First name. |
lastName | string or null | Last name. |
id | integer or null | User ID. |
email | string | Email address. |
User Fields
| Field | Type | Description |
|---|---|---|
firstName | string | First name. |
lastName | string or null | Last name. |
Financial Summary
| Field | Type | Description |
|---|---|---|
partsSubtotal | number | Subtotal for parts before discount. |
partsDiscountType | string or null | Discount type. One of Percentage, Fixed. |
partsDiscountPercentage | number or null | Discount percentage applied to parts. |
partsDiscount | number or null | Discount amount applied to parts. |
partsDiscountTaxable | number or null | Taxable portion of parts discount. |
partsDiscountTaxExempt | number or null | Tax-exempt portion of parts discount. |
partsTotal | number | Total for parts after discount. |
laborTotal | number | Total labor cost. |
feeTotal | number | Total fees. |
proposalSubtotal | number | Subtotal before tax. |
salesTax | object or null | Sales tax details (see sales tax). |
proposalTotal | number or null | Grand total including tax. |
currency | object or null | Currency with code (ISO 4217, e.g. USD) and symbol (e.g. $). |
Sales Tax
| Field | Type | Description |
|---|---|---|
taxStatus | string | One of Undefined, Ok, NoState, OutOfCountry, NoClient, NoClientAddress, IncompleteClientAddress, NoCompanyAddress, TaxableStateDeclined. |
total | number or null | Total sales tax amount. |
calculation | object or null | Tax calculation details (see tax calculation). |
Tax Calculation
| Field | Type | Description |
|---|---|---|
method | string | One of ClientLocation, CompanyLocation, FixedPercentage, None. |
applyTo | array<string> | Array of categories tax applies to. Each value is one of None, Parts, Labor, Fee. |
partsTax | number or null | Parts tax rate. |
partsTaxName | string or null | Parts tax label. |
laborTax | number or null | Labor tax rate. |
laborTaxName | string or null | Labor tax label. |
hasMultipleTaxSupport | boolean | Whether multiple tax rates are configured. |
partsTax2 | number or null | Second parts tax rate. |
partsTax2Name | string or null | Second parts tax label. |
laborTax2 | number or null | Second labor tax rate. |
laborTax2Name | string or null | Second labor tax label. |
feeTax | number or null | Fee tax rate. |
feeTaxName | string or null | Fee tax label. |
feeTax2 | number or null | Second fee tax rate. |
feeTax2Name | string or null | Second fee tax label. |
taxLocation | object or null | Tax jurisdiction location (see location fields). |
isTaxJarAvailable | boolean | Whether TaxJar integration is available. |
partsTotalTax | number or null | Calculated total tax on parts. |
laborTotalTax | number or null | Calculated total tax on labor. |
feeTotalTax | number or null | Calculated total tax on fees. |
Area Fields
| Field | Type | Description |
|---|---|---|
id | integer | Area ID. |
name | string | Area name. |
options | array | Area options (see area option fields). |
Area Option Fields
| Field | Type | Description |
|---|---|---|
id | integer | Option ID. |
status | string | Option status. One of Draft, Accepted, Declined. |
lastModifiedDate | string (ISO 8601) | When the option was last modified. |
clientDescription | string or null | Client-facing description. |
installerDescription | string or null | Installer-facing notes. |
items | array or null | Line items (see item fields). |
total | number | Option total amount. |
totalRecurringService | number | Recurring service total for this option. |
Item Fields
| Field | Type | Description |
|---|---|---|
id | integer | Item ID. |
parentId | integer or null | Parent item ID (for sub-items). |
itemType | string | One of Part, Labor, CustomItem, Fee. |
referencedItemId | integer | Catalog item ID reference. |
createdDate | string (ISO 8601) | When the item was added. |
lastModifiedDate | string (ISO 8601) | When the item was last modified. |
brand | string or null | Item brand. |
model | string or null | Item model. |
description | string or null | Full description. |
name | string or null | Item name. |
shortDescription | string or null | Short description. |
clientNote | string or null | Note visible to client. |
imageUrl | string or null | Item image URL. |
msrp | number or null | Manufacturer’s suggested retail price. |
sellPrice | number or null | Sell price per unit. |
cost | number or null | Cost per unit. |
costUpdateDate | string or null | When cost was last updated. |
supplier | string or null | Supplier name. |
quantity | number | Quantity. |
total | object | Item total with amount (number), currency (object or null), and isCombinedPrice (boolean or null). |
isTaxExempt | boolean or null | Whether the item is tax-exempt. |
isRecurringService | boolean or null | Whether this is a recurring service item. |
linkedOrders | array or null | Linked purchase orders (see linked order fields). |
Linked Order Fields
| Field | Type | Description |
|---|---|---|
orderId | integer | Order ID. |
orderNumber | integer | Order number. |
orderNumberSuffix | string | Order number suffix (e.g. A, B). |
supplier | string or null | Supplier name. |
supplierRef | string or null | Supplier reference/PO number. |
orderName | string | Order name. |
orderStatus | string | One of Undefined, Draft, Submitted, ViewedBySupplier, Accepted, Received, EmailFailed. |
Change Order Fields
| Field | Type | Description |
|---|---|---|
id | integer | Change order ID. |
number | integer | Change order number. |
name | string | Change order name. |
status | string | Status (see proposal statuses). |
total | object or null | Total with changeOrderTotal (number or null) and currency (object or null). |
customer | object or null | Customer contact (see customer fields). |
createdDate | string (ISO 8601) | When the change order was created. |
lastModifiedDate | string (ISO 8601) | When last modified. |
lastModifiedByUserDate | string or null | When last modified by a human user. |
Profit Fields
| Field | Type | Description |
|---|---|---|
total | number or null | Total profit amount. |
percentage | number or null | Overall profit percentage. |
partTotal | number or null | Profit on parts. |
partPercentage | number or null | Profit percentage on parts. |
laborTotal | number or null | Profit on labor. |
laborPercentage | number or null | Profit percentage on labor. |
isProfitIncludeCos | boolean or null | Whether profit calculation includes cost of sale. |
Recurring Services
| Field | Type | Description |
|---|---|---|
items | array | Recurring service items, each with name (string), sellPrice (number), quantity (number), totalSell (number). |
totalRecurringService | number | Total recurring service amount. |
Payment Schedule
| Field | Type | Description |
|---|---|---|
customerDescription | string or null | Customer-facing payment schedule description. |
payments | array or null | Scheduled payments, each with calculation (string or null), amount (number), and due object containing date (string or null) and milestone (string or null). |
Payment Requests
| Field | Type | Description |
|---|---|---|
id | integer | Payment request ID. |
status | string | Payment status. One of Undefined, Draft, Submitted, Viewed, Paid, Declined, Refunded, Pending, RequiresAction, Verifying, Cancelled. |
amount | number or null | Payment amount. |
dueDate | string (ISO 8601) | Payment due date. |
description | string or null | Payment description. |
paymentMethod | string or null | Payment method used. |