Webhook event

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…

Sodtrack sends POST with Content-Type: application/json to the endpoint configured for your tenant. See webhooks overview for delivery and security guidance.

Payload

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

FieldTypeDescription
quote.type`stringnull`
quote.description`stringnull`
quote.itemsarrayPriced materials, products and components. Empty when the budget is not itemized.
quote.activitiesarrayPriced labor activities. Empty when the budget is not itemized.
quote.discountsarrayDiscounts applied to the budget. Empty when none.
quote.surchargesarraySurcharges applied to the budget. Empty when none.
quote.attachmentsarrayAttachments explicitly shared with the customer. Empty when none.
quote.totalsobjectFull amount breakdown. Always present.

📦 Quote itemsquote.items[]

FieldTypeDescription
idnumberUnique Sodtrack identifier for the line.
name`stringnull`
description`stringnull`
sku`stringnull`
typestringOne of: "material", "product", "component".
costTypestringHow the line is priced. One of: "metric" (unit price × quantity), "total" (flat amount).
quantity`numbernull`
metric`stringnull`
metricCost`numbernull`
totalCost`numbernull`
lineTotalnumberAmount contributed by this line, already resolved for both pricing models.
requiredbooleanWhether 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 activitiesquote.activities[]

Labor performed as part of the budget.

FieldTypeDescription
idnumberUnique Sodtrack identifier for the activity.
namestringActivity name.
description`stringnull`
order`numbernull`
quantity`numbernull`
metric`stringnull`
costPerMetric`numbernull`
typeCost`stringnull`
globalCost`numbernull`
lineTotalnumberAmount contributed by this activity.
required`booleannull`
productsarrayMaterials consumed by the activity. Empty when none.

🧰 Activity productsquote.activities[].products[]

FieldTypeDescription
idnumberUnique Sodtrack identifier.
namestringProduct name.
description`stringnull`
quantity`numbernull`
productMetric`stringnull`
productMetricCost`numbernull`
lineTotalnumberquantity × productMetricCost.

⚠️ Activity products are informational. They do not contribute to quote.totals in itemized or global budgets. See Totals calculation.


🏷️ Discounts and surchargesquote.discounts[], quote.surcharges[]

Both arrays share the same shape.

FieldTypeDescription
idnumberUnique Sodtrack identifier.
namestringName shown to the customer.
description`stringnull`
typestringHow the amount is expressed. One of: "percent", "global" (fixed amount).
entityApplicatedstringWhat the adjustment applies to. One of: "item" (materials), "labor_force" (activities), "sub_total" (whole budget).
percent`numbernull`
globalValue`numbernull`
required`booleannull`

📎 Attachmentsquote.attachments[]

FieldTypeDescription
idnumberUnique Sodtrack identifier.
name`stringnull`
urlstringDownload URL.

Only attachments explicitly marked to be shared with the customer are included. Internal attachments are never sent.


💰 Totalsquote.totals

FieldTypeDescription
subTotalQuotenumberSum of items and activities before any discount or surcharge.
baseItemsCostnumberItems subtotal before adjustments.
baseActivitiesCostnumberActivities subtotal before adjustments.
itemsCostnumberItems subtotal after item-level discounts and surcharges.
activitiesCostnumberActivities subtotal after labor-level discounts and surcharges.
totalItemDiscountnumberTotal discounted from items.
totalActivityDiscountnumberTotal discounted from activities.
totalSubTotalDiscountnumberTotal discounted from the subtotal.
totalDiscountnumberSum of all discounts.
totalSurchargenumberSum of all surcharges.
totalQuotenumberFinal quoted amount, before commission.
totalPrice`numbernull`
totalCost`numbernull`

🧭 Context

FieldTypeDescription
context.hasOriginBookingbooleantrue when the budget originated from an existing booking.
context.originBookingId`numbernull`
context.projectId`numbernull`
context.customer`objectnull`
context.provider`objectnull`
context.address`objectnull`
context.serviceVariantArea`objectnull`

🧍 Customercontext.customer

FieldTypeDescription
idnumberUnique Sodtrack customer identifier.
name`stringnull`
lastname`stringnull`
email`stringnull`
phone`stringnull`
identification_number`stringnull`

🛠️ Providercontext.provider

FieldTypeDescription
idnumberUnique Sodtrack provider identifier.
isCompanybooleantrue when the provider is a company, false for an individual.
user.idnumberIdentifier of the provider's user account.
user.name`stringnull`
user.lastname`stringnull`
user.email`stringnull`
user.phone`stringnull`
user.identification_number`stringnull`

🏠 Addresscontext.address

FieldTypeDescription
idnumberUnique Sodtrack address identifier.
addressstringAddress as entered.
formatted_addressstringNormalized address.
coordinate`objectnull`
coordinate.lngnumberLongitude.
coordinate.latnumberLatitude.

🧩 Servicecontext.serviceVariantArea

FieldTypeDescription
idnumberUnique Sodtrack identifier of the service variant area.
name`stringnull`
description`stringnull`
serviceVariant.idnumberService variant identifier.
serviceVariant.internal_name`stringnull`
serviceVariant.skusarraySKUs associated with the variant, as { "id": number, "sku": string }. Empty when none.
serviceVariant.service.idnumberService identifier.
serviceVariant.service.name`stringnull`
serviceVariant.service.matchingTypeobjectProvider 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

json
{
  "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

ScenarioBehavior
hasOriginBooking is trueThe budget was created from an existing booking. originBookingId identifies it, and projectId is populated when the booking belongs to a project.
hasOriginBooking is falseThe 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:

  1. Items and activities are added up separately, producing baseItemsCost and baseActivitiesCost.

  2. Discounts and surcharges targeting "item" are applied to the items subtotal, and those targeting "labor_force" to the activities subtotal, producing itemsCost and activitiesCost.

  3. The two adjusted subtotals are added together.

  4. Discounts and surcharges targeting "sub_total" are applied to that result, producing totalQuote.

  5. The configured commission is applied to totalQuote, producing totalPrice and totalCost.

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.typeBehavior
"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.
nullLegacy 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.