Pular para o conteúdo

Estimates

Este conteúdo não está disponível em sua língua ainda.

Estimates let you send a priced quote to a customer before work begins. Each estimate belongs to a ticket (one is created automatically if you don’t supply one), tracks its own line items and totals, and moves through a status lifecycle: draft → sent → approved / declined → converted (when turned into an invoice).

Money values in the API are objects, { "amount_cents": 1999, "amount": "19.99", "currency": "USD" }. Input fields (request body) use *_cents integers (e.g. unit_price_cents: 1999). Tax rates are percent, tax_rate: 8.25 means 8.25%.

{
"object": "estimate",
"id": "42",
"public_url": "https://app.benchkey.com/estimate/42/tok_5a7e33b9",
"ticket_id": "17",
"converted_invoice_id": null,
"status": "sent",
"currency": "USD",
"customer": {
"name": "Jane Smith",
"email": "jane.smith@example.com",
"phone": "555-867-5309"
},
"line_items": [
{
"object": "estimate_line_item",
"name": "Screen replacement",
"quantity": 1,
"unit_price": { "amount_cents": 12999, "amount": "129.99", "currency": "USD" },
"amount": { "amount_cents": 12999, "amount": "129.99", "currency": "USD" }
},
{
"object": "estimate_line_item",
"name": "Labor",
"quantity": 1,
"unit_price": { "amount_cents": 4500, "amount": "45.00", "currency": "USD" },
"amount": { "amount_cents": 4500, "amount": "45.00", "currency": "USD" }
}
],
"subtotal": { "amount_cents": 17499, "amount": "174.99", "currency": "USD" },
"discount": { "amount_cents": 0, "amount": "0.00", "currency": "USD" },
"tax_rate": 8.25,
"tax_amount": { "amount_cents": 1444, "amount": "14.44", "currency": "USD" },
"total": { "amount_cents": 18943, "amount": "189.43", "currency": "USD" },
"deposit": { "amount_cents": 0, "amount": "0.00", "currency": "USD" },
"approved_via": null,
"approved_by": null,
"valid_days": 30,
"sent_via": "email",
"approved_at": null,
"declined_at": null,
"created_at": "2025-11-15T09:10:00.000Z"
}
FieldTypeDescription
idstringUnique estimate ID
public_urlstring | nullCustomer share link, the same tokenized page BenchKey emails/texts. Anyone with the URL can view and approve/decline the estimate; treat it like the emailed link. null until the estimate has a share token
ticket_idstring|nullID of the attached ticket (null for billing-only tickets)
converted_invoice_idstring|nullInvoice ID if the estimate was converted, otherwise null
statusstringdraft, sent, pending, viewed, approved, declined, or converted
currencystringISO-4217 currency code from tenant settings (e.g. "USD")
customerobjectSnapshot of the customer’s name, email, and phone at estimate time
line_itemsarrayOrdered list of line items
subtotalmoneySum of line items before discount and tax
discountmoneyEstimate-level discount amount
tax_ratenumber|nullTax rate as a percent (e.g. 8.25 = 8.25%), or null if no tax
tax_amountmoneyComputed tax amount
totalmoneyFinal amount due (subtotal − discount + tax)
depositmoneyDeposit recorded against the estimate
approved_viastring|nullHow the estimate was approved (staff, customer portal, api), or null
approved_bystring|nullWho approved it, or null
valid_daysinteger|nullHow many days the estimate is valid, or null
sent_viastring|nullDelivery channel used when sent: "sms", "email", "both", or null
approved_atstring|nullISO-8601 timestamp of approval, or null
declined_atstring|nullISO-8601 timestamp of decline, or null
created_atstringISO-8601 creation timestamp

GET /api/v1/estimates

Returns a cursor-paginated list of estimates, newest first.

Scope required: estimates.read

ParameterTypeDescription
statusstringFilter by status: draft, sent, pending, viewed, approved, declined, converted
ticket_idstringOnly estimates attached to this ticket
customer_emailstringOnly estimates for this customer email (case-insensitive)
created_afterstringISO-8601, only estimates created at or after this time
created_beforestringISO-8601, only estimates created at or before this time
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/estimates?status=sent&limit=10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch(
"https://app.benchkey.com/api/v1/estimates?status=sent&limit=10",
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const { data, has_more, next_cursor } = await res.json();
{
"object": "list",
"data": [ { "object": "estimate", "id": "42", "status": "sent" } ],
"has_more": false,
"next_cursor": null
}

Additional resource fields are omitted from this example.

See Pagination for how to page through results.

Visibility: estimates linked to hidden or soft-deleted tickets are automatically excluded from list results (and return 404 on single-GET).


GET /api/v1/estimates/:id

Returns a single estimate with all line items and computed totals.

Scope required: estimates.read

Terminal window
curl "https://app.benchkey.com/api/v1/estimates/42" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch(
"https://app.benchkey.com/api/v1/estimates/42",
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const estimate = await res.json();

Returns the estimate object. Returns 404 if no estimate with that ID exists, or if the estimate’s linked ticket is hidden or soft-deleted (matching the list’s visibility filter).


POST /api/v1/estimates

Creates a new estimate. Supply line_items to price it on creation, or omit them to create an empty draft. Pass ticket_id to attach it to an existing ticket, or omit it to anchor the estimate to a new billing-only ticket automatically.

Scope required: estimates.write

FieldRequiredTypeDescription
customer_idyesstringThe customer this estimate is for
ticket_idnostringExisting ticket to attach the estimate to
line_itemsnoarraySee line item fields below
discount_centsnointegerEstimate-level discount in integer cents. Requires at least one line item
tax_ratenonumberTax rate as a percent (e.g. 8.25). Requires at least one line item
FieldRequiredTypeDescription
descriptionyesstringLine item label (alias: name). Must be a string
unit_price_centsyesintegerUnit price in integer cents (positive safe integer)
quantitynonumberQuantity, defaults to 1. Alias qty is also accepted. unit_price_cents × quantity must produce a whole number of cents
cost_centsnointegerUnit cost in integer cents for margin reporting

The server enforces that each line’s extended price, the aggregate line total (sum of unit_price_cents × quantity across all items), and, when tax_rate is supplied, the resulting tax amount and final total all fit within JavaScript’s safe-integer range. Exceeding any of these limits returns 400 invalid_field on the offending field (unit_price_cents for a single line, tax_rate when the tax-pushed total overflows).

Terminal window
curl -X POST https://app.benchkey.com/api/v1/estimates \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: est-create-ticket17-$(date +%s)" \
-d '{
"customer_id": "jane.smith@example.com",
"ticket_id": "17",
"line_items": [
{ "description": "Screen replacement", "unit_price_cents": 12999 },
{ "description": "Labor", "unit_price_cents": 4500 }
],
"tax_rate": 8.25
}'
const res = await fetch("https://app.benchkey.com/api/v1/estimates", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": `est-create-ticket17-${Date.now()}`,
},
body: JSON.stringify({
customer_id: "jane.smith@example.com",
ticket_id: "17",
line_items: [
{ description: "Screen replacement", unit_price_cents: 12999 },
{ description: "Labor", unit_price_cents: 4500 },
],
tax_rate: 8.25,
}),
});
const estimate = await res.json(); // HTTP 201

Returns the estimate object with HTTP 201. The total, tax, and line items reflect exactly what was supplied.

If ticket_id refers to a hidden or soft-deleted ticket, the request returns 404 not_found (ticket). Supplying an unknown customer_id or ticket_id returns 422 create_failed.

Use an Idempotency-Key header to make retries safe.


PATCH /api/v1/estimates/:id

Reprices an estimate. This is a full replacement of the line items, send the complete set you want the estimate to have; discount, tax, and total are recomputed by the same internal edit flow staff use.

Editing an approved estimate materially resets it: status returns to sent, the signature block and approval provenance (approved_at, approved_via, approved_by) are cleared, and an approval_reset event is recorded, the customer must approve the new pricing. A converted estimate can no longer be edited (409 estimate_converted); it belongs to its invoice.

Scope required: estimates.write

FieldRequiredTypeDescription
line_itemsyesarrayComplete replacement set, same line item fields as create, at least one item
discount_centsnointegerEstimate-level discount in integer cents. Must be less than the line-item subtotal
tax_ratenonumberTax rate as a percent (0–100). Omit to keep the stored rate
deposit_centsnointegerDeposit collected against the estimate, in integer cents. Omit to keep the stored deposit
Terminal window
curl -X PATCH https://app.benchkey.com/api/v1/estimates/42 \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "description": "Screen replacement", "unit_price_cents": 13999 },
{ "description": "Labor", "unit_price_cents": 4500 }
],
"discount_cents": 1000,
"tax_rate": 8.25
}'

Returns the updated estimate object with HTTP 200, totals, line items, and (after an approval reset) status/approved_at reflect the persisted result.

StatusMeaning
400 invalid_fieldMalformed line items, discount at/above the subtotal, or out-of-range tax rate
404 not_foundUnknown estimate, or its ticket is hidden/soft-deleted
409 estimate_convertedThe estimate was converted to an invoice

DELETE /api/v1/estimates/:id

Deletes a draft, sent, or declined estimate (its event history and public share token are cleaned up with it, and the deletion is recorded on the ticket’s activity log).

Two states are protected and answer 409:

  • Approved (409 estimate_delete_signed_record), an approved estimate is a signed legal record (signature, IP, typed name, pinned terms). Decline it or convert it instead.
  • Converted (409 estimate_converted), the estimate belongs to its invoice; delete the invoice first.

Scope required: estimates.write

Terminal window
curl -X DELETE https://app.benchkey.com/api/v1/estimates/42 \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
{ "object": "estimate", "id": "42", "deleted": true }

POST /api/v1/estimates/:id/send

Delivers the estimate to the customer by SMS, email, or both using the tenant’s estimate template. Sets status to sent and advances the attached ticket to “Quote Sent”.

Scope required: estimates.write

FieldRequiredTypeDescription
viayesstringDelivery channel: "sms", "email", or "both"
Terminal window
curl -X POST https://app.benchkey.com/api/v1/estimates/42/send \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: est-42-send-1" \
-d '{ "via": "email" }'
const res = await fetch("https://app.benchkey.com/api/v1/estimates/42/send", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": "est-42-send-1",
},
body: JSON.stringify({ via: "email" }),
});
const estimate = await res.json(); // status: "sent", sent_via: "email"

Returns the refreshed estimate object with status: "sent" and sent_via set. Returns 404 if the estimate does not exist or its linked ticket is hidden/soft-deleted.


POST /api/v1/estimates/:id/approve

Records staff-side approval of the estimate. Sets status to approved, advances the ticket to “Estimate approved”, and fires the usual notifications and automation. Only a non-approved, non-declined, non-converted estimate can be approved.

Scope required: estimates.write

Terminal window
curl -X POST https://app.benchkey.com/api/v1/estimates/42/approve \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Idempotency-Key: est-42-approve-1"
const res = await fetch("https://app.benchkey.com/api/v1/estimates/42/approve", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Idempotency-Key": "est-42-approve-1",
},
});
const estimate = await res.json(); // status: "approved", approved_at: "..."

Returns the refreshed estimate object with status: "approved" and approved_at set. Returns 404 if the estimate does not exist or its linked ticket is hidden/soft-deleted.


POST /api/v1/estimates/:id/decline

Declines the estimate. Sets status to declined, flips the ticket back to “Customer Reply”, and fires the usual notifications and automation. Only a sent estimate (one that has a share token) can be declined. An optional reason is recorded on the decline event.

Scope required: estimates.write

FieldRequiredTypeDescription
reasonnostringOptional reason recorded on the decline event (max 200 characters)
Terminal window
curl -X POST https://app.benchkey.com/api/v1/estimates/42/decline \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: est-42-decline-1" \
-d '{ "reason": "Customer found a cheaper repair shop." }'
const res = await fetch("https://app.benchkey.com/api/v1/estimates/42/decline", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": "est-42-decline-1",
},
body: JSON.stringify({ reason: "Customer found a cheaper repair shop." }),
});
const estimate = await res.json(); // status: "declined", declined_at: "..."

Returns the refreshed estimate object with status: "declined" and declined_at set. Returns 404 if the estimate does not exist or its linked ticket is hidden/soft-deleted.


HTTP statusCodeMeaning
400invalid_queryA list filter parameter was supplied as an array or object instead of a single scalar value
400invalid_idThe :id is not a valid positive integer
400missing_fieldA required field is absent (customer_id, via)
400invalid_fieldA field value is invalid, via not in allowed set, unit_price_cents not a positive safe integer, description not a string, unit_price_cents × quantity not a whole number of cents, a single line’s extended price, the aggregate line total, or the tax-pushed final total exceeds the safe-integer range, discount_cents ≥ subtotal, etc.
422send_failedThe estimate could not be delivered, no contact info, or the estimate has no linked ticket (empty/null ticket_id)
404not_foundNo estimate with that ID exists, the estimate’s linked ticket is hidden or soft-deleted, or (on create) the supplied ticket_id is hidden/soft-deleted
422create_failedThe customer or ticket could not be found
409estimate_declinedThe estimate has already been declined and cannot be approved; create or send a new estimate instead
422approve_failedThe estimate is already approved or converted
422decline_failedThe estimate has not been sent yet (no share token), or is already declined/approved/converted
403insufficient_scopeAPI key lacks estimates.read or estimates.write
429rate_limitedToo many requests; see Retry-After

See Errors for the full error envelope format.

Status do sistema