BudgetExecutionStatusChangedToWaitingForCustomerApprovalEvent
Sodtrack sends this payload when a budget execution changes to waiting for customer approval, meaning the budget has been prepared and is now published to the customer for review, approval or rejecti…
POST with Content-Type: application/json to the endpoint configured for your tenant. See webhooks overview for delivery and security guidance.Payload
{
"quote": {
"type": "itemized",
"description": "Excepteur aute elit consectetur sit veniam enim amet excepteur eu. Aute cillum reprehenderit cillum aliqua incididunt duis ex aute aliquip commodo aliquip pariatur quis amet excepteur. Adipisicing sunt magna est anim est amet aliquip Lorem officia dolore non qui exercitation cillum dolor. Lorem do non labore tempor. Veniam nostrud adipisicing est mollit.",
"items": [
{
"id": 366,
"name": "Material costo total",
"description": "test",
"sku": null,
"type": "material",
"costType": "total",
"quantity": null,
"metric": null,
"metricCost": null,
"totalCost": 100,
"lineTotal": 100,
"required": false
},
{
"id": 364,
"name": "Componente 1",
"description": "Esta es la descripción",
"sku": "1",
"type": "component",
"costType": "metric",
"quantity": 1,
"metric": "u",
"metricCost": 0,
"totalCost": null,
"lineTotal": 0,
"required": false
},
{
"id": 367,
"name": "Material costo por métrica",
"description": "Test",
"sku": null,
"type": "material",
"costType": "metric",
"quantity": 200,
"metric": "cm2",
"metricCost": 1,
"totalCost": null,
"lineTotal": 200,
"required": false
},
{
"id": 365,
"name": "Producto 6",
"description": null,
"sku": "6",
"type": "product",
"costType": "metric",
"quantity": 1,
"metric": "u",
"metricCost": 0,
"totalCost": null,
"lineTotal": 0,
"required": false
}
],
"activities": [
{
"id": 818,
"name": "Actividad con costo total",
"description": "Test",
"order": null,
"quantity": null,
"metric": null,
"costPerMetric": null,
"typeCost": "global",
"globalCost": 100,
"lineTotal": 100,
"required": null,
"products": []
},
{
"id": 819,
"name": "Actrivida con costo por métrica",
"description": "Test",
"order": null,
"quantity": 100,
"metric": "m",
"costPerMetric": 1,
"typeCost": "metric",
"globalCost": null,
"lineTotal": 100,
"required": null,
"products": []
}
],
"discounts": [
{
"id": 176,
"name": "Descuento fijo",
"description": "test",
"type": "global",
"entityApplicated": "labor_force",
"percent": null,
"globalValue": 100,
"required": null
},
{
"id": 177,
"name": "Descuento procentual",
"description": "Test",
"type": "percent",
"entityApplicated": "labor_force",
"percent": 10,
"globalValue": null,
"required": null
}
],
"surcharges": [
{
"id": 120,
"name": "Recargo fijo",
"description": "Test",
"type": "global",
"entityApplicated": "labor_force",
"percent": null,
"globalValue": 100,
"required": null
},
{
"id": 121,
"name": "Recargo procentual",
"description": "Test",
"type": "percent",
"entityApplicated": "labor_force",
"percent": 10,
"globalValue": null,
"required": null
}
],
"attachments": [],
"totals": {
"subTotalQuote": 500,
"baseItemsCost": 300,
"baseActivitiesCost": 200,
"itemsCost": 300,
"activitiesCost": 200,
"totalItemDiscount": 0,
"totalActivityDiscount": 120,
"totalSubTotalDiscount": 0,
"totalDiscount": 120,
"totalSurcharge": 120,
"totalQuote": 500,
"totalPrice": 500,
"totalCost": -500
}
},
"context": {
"hasOriginBooking": true,
"originBookingId": 2686,
"projectId": null,
"customer": {
"id": 7178,
"name": "Stiven",
"lastname": null,
"email": "stiven.jimenez+cliente@sodtrack.com",
"phone": "3107406842",
"identification_number": "3107406842"
},
"provider": {
"id": 11,
"isCompany": false,
"user": {
"id": 137,
"name": "Stiven's",
"lastname": "Jimenez",
"email": "stiven.jimenez+proveedor@sodtrack.com",
"phone": "+573107406842",
"identification_number": "1233696062"
}
},
"address": {
"id": 6472,
"address": "Comandante Chacón 5650, 8500048 Quinta Normal, Región Metropolitana, Chile",
"formatted_address": "Comandante Chacón 5650, 8500048 Quinta Normal, Región Metropolitana, Chile",
"coordinate": {
"lng": -70.7134606,
"lat": -33.4286826
}
},
"serviceVariantArea": {
"id": 4790,
"name": "",
"description": null,
"serviceVariant": {
"id": 1764,
"internal_name": "Presupuesto puerta",
"service": {
"id": 999,
"name": "Presupuestos puertas",
"matchingType": {
"id": 2,
"internalName": "all_eligible_at_once"
}
},
"skus": []
}
}
}
}Purpose
Sodtrack sends this payload when a budget execution changes to waiting for customer approval, meaning the budget has been prepared and is now published to the customer for review, approval or rejection.
The payload describes what is being quoted (quote) and who the budget is for (context). It is a complete snapshot at the moment of submission: every line item, labor activity, discount, surcharge, customer-facing attachment and the full amount breakdown are included, so the receiving system can reproduce the quote without any additional call to Sodtrack.
The payload has the same shape for budgets created from an existing booking and for standalone budgets. Use context.hasOriginBooking to tell the two apart.
Field Definitions
🧾 Quote
| Field | Type | Description |
|---|---|---|
quote.type | `string | null` |
quote.description | `string | null` |
quote.items | array | Priced materials, products and components. Empty when the budget is not itemized. |
quote.activities | array | Priced labor activities. Empty when the budget is not itemized. |
quote.discounts | array | Discounts applied to the budget. Empty when none. |
quote.surcharges | array | Surcharges applied to the budget. Empty when none. |
quote.attachments | array | Attachments explicitly shared with the customer. Empty when none. |
quote.totals | object | Full amount breakdown. Always present. |
📦 Quote items — quote.items[]
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier for the line. |
name | `string | null` |
description | `string | null` |
sku | `string | null` |
type | string | One of: "material", "product", "component". |
costType | string | How the line is priced. One of: "metric" (unit price × quantity), "total" (flat amount). |
quantity | `number | null` |
metric | `string | null` |
metricCost | `number | null` |
totalCost | `number | null` |
lineTotal | number | Amount contributed by this line, already resolved for both pricing models. |
required | boolean | Whether the line is mandatory in the budget. |
Use lineTotal rather than recomputing from quantity and metricCost: it is calculated by Sodtrack with the same rules used for the totals.
👷 Quote activities — quote.activities[]
Labor performed as part of the budget.
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier for the activity. |
name | string | Activity name. |
description | `string | null` |
order | `number | null` |
quantity | `number | null` |
metric | `string | null` |
costPerMetric | `number | null` |
typeCost | `string | null` |
globalCost | `number | null` |
lineTotal | number | Amount contributed by this activity. |
required | `boolean | null` |
products | array | Materials consumed by the activity. Empty when none. |
🧰 Activity products — quote.activities[].products[]
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier. |
name | string | Product name. |
description | `string | null` |
quantity | `number | null` |
productMetric | `string | null` |
productMetricCost | `number | null` |
lineTotal | number | quantity × productMetricCost. |
⚠️ Activity products are informational. They do not contribute to quote.totals in itemized or global budgets. See Totals calculation.
🏷️ Discounts and surcharges — quote.discounts[], quote.surcharges[]
Both arrays share the same shape.
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier. |
name | string | Name shown to the customer. |
description | `string | null` |
type | string | How the amount is expressed. One of: "percent", "global" (fixed amount). |
entityApplicated | string | What the adjustment applies to. One of: "item" (materials), "labor_force" (activities), "sub_total" (whole budget). |
percent | `number | null` |
globalValue | `number | null` |
required | `boolean | null` |
📎 Attachments — quote.attachments[]
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier. |
name | `string | null` |
url | string | Download URL. |
Only attachments explicitly marked to be shared with the customer are included. Internal attachments are never sent.
💰 Totals — quote.totals
| Field | Type | Description |
|---|---|---|
subTotalQuote | number | Sum of items and activities before any discount or surcharge. |
baseItemsCost | number | Items subtotal before adjustments. |
baseActivitiesCost | number | Activities subtotal before adjustments. |
itemsCost | number | Items subtotal after item-level discounts and surcharges. |
activitiesCost | number | Activities subtotal after labor-level discounts and surcharges. |
totalItemDiscount | number | Total discounted from items. |
totalActivityDiscount | number | Total discounted from activities. |
totalSubTotalDiscount | number | Total discounted from the subtotal. |
totalDiscount | number | Sum of all discounts. |
totalSurcharge | number | Sum of all surcharges. |
totalQuote | number | Final quoted amount, before commission. |
totalPrice | `number | null` |
totalCost | `number | null` |
🧭 Context
| Field | Type | Description |
|---|---|---|
context.hasOriginBooking | boolean | true when the budget originated from an existing booking. |
context.originBookingId | `number | null` |
context.projectId | `number | null` |
context.customer | `object | null` |
context.provider | `object | null` |
context.address | `object | null` |
context.serviceVariantArea | `object | null` |
🧍 Customer — context.customer
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack customer identifier. |
name | `string | null` |
lastname | `string | null` |
email | `string | null` |
phone | `string | null` |
identification_number | `string | null` |
🛠️ Provider — context.provider
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack provider identifier. |
isCompany | boolean | true when the provider is a company, false for an individual. |
user.id | number | Identifier of the provider's user account. |
user.name | `string | null` |
user.lastname | `string | null` |
user.email | `string | null` |
user.phone | `string | null` |
user.identification_number | `string | null` |
🏠 Address — context.address
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack address identifier. |
address | string | Address as entered. |
formatted_address | string | Normalized address. |
coordinate | `object | null` |
coordinate.lng | number | Longitude. |
coordinate.lat | number | Latitude. |
🧩 Service — context.serviceVariantArea
| Field | Type | Description |
|---|---|---|
id | number | Unique Sodtrack identifier of the service variant area. |
name | `string | null` |
description | `string | null` |
serviceVariant.id | number | Service variant identifier. |
serviceVariant.internal_name | `string | null` |
serviceVariant.skus | array | SKUs associated with the variant, as { "id": number, "sku": string }. Empty when none. |
serviceVariant.service.id | number | Service identifier. |
serviceVariant.service.name | `string | null` |
serviceVariant.service.matchingType | object | Provider matching strategy, as { "id": number, "internalName": string }. |
Units of measure
metric, productMetric and related fields use one of the following values:
cm, mm, m, cm2, mm2, m2, cm3, mm3, m3, in, ft, yds, in2, ft2, yds2, in3, ft3, yds3, g, kg, t, oz, lbs, mlts, lts, gal, min, hrs, day, month, u, box
Payload Example
{
"quote": {
"type": "itemized",
"description": "Replacement of the damaged section of the potable water main, floors 3 to 5.",
"items": [
{
"id": 366,
"name": "Sealing kit",
"description": "Includes clamps, seals and structural adhesive",
"sku": null,
"type": "material",
"costType": "total",
"quantity": null,
"metric": null,
"metricCost": null,
"totalCost": 100,
"lineTotal": 100,
"required": false
},
{
"id": 367,
"name": "PVC pipe 50mm",
"description": "Sanitary PVC pipe, class 10",
"sku": "PVC-50-C10",
"type": "material",
"costType": "metric",
"quantity": 200,
"metric": "cm2",
"metricCost": 1,
"totalCost": null,
"lineTotal": 200,
"required": false
}
],
"activities": [
{
"id": 818,
"name": "Debris removal",
"description": "Removal and final disposal of demolished material",
"order": null,
"quantity": null,
"metric": null,
"costPerMetric": null,
"typeCost": "global",
"globalCost": 100,
"lineTotal": 100,
"required": null,
"products": []
},
{
"id": 819,
"name": "Water network installation",
"description": "Cut, replacement and pressure test of the affected section",
"order": null,
"quantity": 100,
"metric": "m",
"costPerMetric": 1,
"typeCost": "metric",
"globalCost": null,
"lineTotal": 100,
"required": null,
"products": []
}
],
"discounts": [
{
"id": 176,
"name": "Fixed discount",
"description": "Agreed with the account executive",
"type": "global",
"entityApplicated": "labor_force",
"percent": null,
"globalValue": 100,
"required": null
},
{
"id": 177,
"name": "Percentage discount",
"description": "Frequent customer",
"type": "percent",
"entityApplicated": "labor_force",
"percent": 10,
"globalValue": null,
"required": null
}
],
"surcharges": [
{
"id": 120,
"name": "Fixed surcharge",
"description": "Crew transfer outside the urban radius",
"type": "global",
"entityApplicated": "labor_force",
"percent": null,
"globalValue": 100,
"required": null
},
{
"id": 121,
"name": "Percentage surcharge",
"description": "Work at height",
"type": "percent",
"entityApplicated": "labor_force",
"percent": 10,
"globalValue": null,
"required": null
}
],
"attachments": [],
"totals": {
"subTotalQuote": 500,
"baseItemsCost": 300,
"baseActivitiesCost": 200,
"itemsCost": 300,
"activitiesCost": 200,
"totalItemDiscount": 0,
"totalActivityDiscount": 120,
"totalSubTotalDiscount": 0,
"totalDiscount": 120,
"totalSurcharge": 120,
"totalQuote": 500,
"totalPrice": 500,
"totalCost": 400
}
},
"context": {
"hasOriginBooking": true,
"originBookingId": 2686,
"projectId": null,
"customer": {
"id": 7178,
"name": "Alex",
"lastname": null,
"email": "alex.customer@example.com",
"phone": "3107406842",
"identification_number": "3107406842"
},
"provider": {
"id": 11,
"isCompany": false,
"user": {
"id": 137,
"name": "Sam",
"lastname": "Rivera",
"email": "sam.provider@example.com",
"phone": "+573107406842",
"identification_number": "1233696062"
}
},
"address": {
"id": 6472,
"address": "Comandante Chacón 5650, 8500048 Quinta Normal, Región Metropolitana, Chile",
"formatted_address": "Comandante Chacón 5650, 8500048 Quinta Normal, Región Metropolitana, Chile",
"coordinate": {
"lng": -70.7134606,
"lat": -33.4286826
}
},
"serviceVariantArea": {
"id": 4790,
"name": "",
"description": null,
"serviceVariant": {
"id": 1764,
"internal_name": "Door budget",
"service": {
"id": 999,
"name": "Door budgets",
"matchingType": {
"id": 2,
"internalName": "all_eligible_at_once"
}
},
"skus": []
}
}
}
}
Business Rules & Constraints
When the webhook is sent
The payload is delivered once, at the moment the budget is submitted to the customer for review. It is not resent when the customer approves or rejects the budget, and it is not resent if the budget is edited after submission.
If a budget is sent to the customer more than once, a new payload is delivered on each submission, reflecting the budget as it stands at that time.
Booking-backed and standalone budgets
| Scenario | Behavior |
|---|---|
hasOriginBooking is true | The budget was created from an existing booking. originBookingId identifies it, and projectId is populated when the booking belongs to a project. |
hasOriginBooking is false | The budget was created on its own. originBookingId and projectId are null. |
quote is identical in both scenarios: the priced content of a budget does not depend on how the budget was created. context.customer, context.provider, context.address and context.serviceVariantArea are always resolved and populated regardless of the scenario.
Totals calculation
Amounts are resolved in this order:
-
Items and activities are added up separately, producing
baseItemsCostandbaseActivitiesCost. -
Discounts and surcharges targeting
"item"are applied to the items subtotal, and those targeting"labor_force"to the activities subtotal, producingitemsCostandactivitiesCost. -
The two adjusted subtotals are added together.
-
Discounts and surcharges targeting
"sub_total"are applied to that result, producingtotalQuote. -
The configured commission is applied to
totalQuote, producingtotalPriceandtotalCost.
Because subtotal-level adjustments are applied after item and labor adjustments, a percentage discount on "sub_total" is calculated over the already-adjusted amount, not over subTotalQuote.
Activity products are not included in this calculation for budgets where quote.type is "itemized" or "global". They are delivered as a detail of the work, not as a priced line. Summing products[].lineTotal and adding it to the totals will not match totalQuote.
Pricing models
quote.type | Behavior |
|---|---|
"itemized" | items and activities carry the priced content. totalQuote is derived from them. |
"global" | The budget is quoted as a single amount. items and activities may be empty and the amount is carried in subTotalQuote. |
null | Legacy budget. totals is still complete, but discounts and surcharges are reported as 0. |
Commission and provider amount
totalPrice is what the customer pays and totalCost is what the provider receives. Depending on the commission configured for the service, the commission may be charged to the customer (totalPrice is higher than totalQuote), deducted from the provider (totalCost is lower than totalQuote), or both amounts may equal totalQuote when no commission applies to that side.
When a fixed provider commission is configured with a value greater than the quoted amount, totalCost can be negative. Treat this as a configuration issue on the budget rather than a valid payable amount.
Both fields are null when the budget has no commission configured.
Amount format
All amounts are numeric values in the currency of the budget's operation. The payload does not carry a currency code; agree on the expected currency with Sodtrack during integration setup.