Quotes API
Every Quotes action (13) and data model (5): endpoints, scopes, input, preconditions, events and examples.
Price quotes with a PDF and a link the customer can accept or decline.
Module code quotes · 13 actions · 5 data models · 12 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: quotes (everything), quotes:read (read only) or quotes:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
quote_events | quote_event | /api/v1/quote_events | |||
quote_lines | quote_line | /api/v1/quote_lines | |||
quote_templates | quote_template | /api/v1/quote_templates | |||
quote_versions | quote_version | /api/v1/quote_versions | |||
quotes | quote | /api/v1/quotes | quotes.create |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
quotes.accept | quotes:write | Mark accepted |
quotes.add_note | quotes:write | Add a note |
quotes.create | quotes:write | Start a quote |
quotes.create_template | quotes:manage | Save a quote template |
quotes.delete_template | quotes:manage | Delete a quote template |
quotes.duplicate | quotes:write | Duplicate |
quotes.expire_due | quotes:write | Lapse overdue quotes |
quotes.reject | quotes:write | Mark declined |
quotes.revise | quotes:write | Revise into a new version |
quotes.send | quotes:write | Send by email |
quotes.update_draft | quotes:write | Edit a draft quote |
quotes.update_template | quotes:manage | Edit a quote template |
quotes.withdraw | quotes:write | Withdraw |
quotes.accept
Mark accepted — Records the customer’s yes on the current version; finance drafts the agreement from it.
Needs quotes:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/quotes/{id}/actions/accept- Canonical address:
POST /api/v1/actions/quotes.accept
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
name | string | Answered by · max 200 | |
note | string | Note · max 4000 | |
via | string (enum) | How · Allowed values: manual, link | |
signatureName | string | Typed signature · max 200 | |
ip | string | From address · max 64 | |
userAgent | string | Browser · max 400 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_open | Only a sent quote is answered. |
quote_validity_passed | A quote past its valid-until day is not accepted. |
quote_signature_required | Accepting through the link needs the customer’s typed name. |
Events it publishes: quotes.accepted
What it writes: quote (update) — Marks it accepted, freezes the accepted version and keeps the typed signature; quote_event (insert) — Records the answer
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/accept \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Answered by"}'{
"ok": true,
"result": "…",
"events": [
"quotes.accepted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.accept("QUOTE_ID", {
name: "Answered by"
})era quotes accept QUOTE_ID \
--name "Answered by"{
"name": "era__quotes__accept",
"arguments": {
"id": "QUOTE_ID",
"name": "Answered by"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_open, quote_validity_passed, quote_signature_required · 429 slow down
quotes.add_note
Add a note — A line in the quote’s own history.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quote_events/actions/add_note- Canonical address:
POST /api/v1/actions/quotes.add_note
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Quote · id of quote |
message | string | yes | Note · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
Events it publishes: quotes.note_added
What it writes: quote_event (insert) — Records the note
curl -X POST https://app.getera.io/api/v1/quote_events/actions/add_note \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"id":"QUOTE_ID","message":"Note"}'{
"ok": true,
"result": "…",
"events": [
"quotes.note_added"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quoteEvents.addNote({
id: "QUOTE_ID",
message: "Note"
})era quote_events add_note \
--id "QUOTE_ID" \
--message "Note"{
"name": "era__quotes__add_note",
"arguments": {
"id": "QUOTE_ID",
"message": "Note"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found · 429 slow down
quotes.create
Start a quote — A numbered draft quote with its first version and lines, each with a frozen tax snapshot.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quotes/actions/create- REST alias:
POST /api/v1/quotes - Canonical address:
POST /api/v1/actions/quotes.create
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | For · id of customer | |
leadId | uuid | Lead | |
templateId | uuid | From template · id of quote_template | |
title | string | Title · max 200 | |
currency | string | Currency · max 3 | |
taxCountry | string | Tax country · max 2 | |
validUntil | date | Valid until | |
notes | string | Notes · max 4000 | |
lines | object / array | Lines |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
customer_not_found | The person must exist. |
quote_template_not_found | A named template must exist. |
customer_archived | An archived person is not quoted. |
invalid_currency | The currency must be an ISO-4217 code. |
quote_needs_lines | A quote needs at least one line. |
invalid_quote_lines | Every line needs a description, a positive quantity and a price; a catalog item must exist. |
Events it publishes: quotes.created
What it writes: quote (insert) — Opens the quote with its number; quote_version (insert) — Writes version 1; quote_line (insert) — Writes its lines with their tax snapshot; quote_event (insert) — Records that it was created
curl -X POST https://app.getera.io/api/v1/quotes/actions/create \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"quotes.created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.create({
customerId: "CUSTOMER_ID"
})era quotes create \
--customer-id "CUSTOMER_ID"{
"name": "era__quotes__create",
"arguments": {
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 customer_not_found, quote_template_not_found, customer_archived, invalid_currency, quote_needs_lines, invalid_quote_lines · 429 slow down
quotes.create_template
Save a quote template — A reusable title, notes and lines — typed in, or saved from a quote.
Needs quotes:manage · changes data
https://app.getera.io/api/v1/quote_templates/actions/create_template- Canonical address:
POST /api/v1/actions/quotes.create_template
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Template name · max 120 |
title | string | Quote title · max 200 | |
notes | string | Notes for the customer · max 4000 | |
currency | string | Currency · max 3 | |
lines | object / array | Lines | |
fromQuoteId | uuid | From quote · id of quote |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_template_name_taken | Template names are unique in a workspace. |
quote_not_found | The quote must exist. |
invalid_currency | The currency must be an ISO-4217 code. |
quote_needs_lines | A quote needs at least one line. |
invalid_quote_lines | Every line needs a description, a positive quantity and a price; a catalog item must exist. |
Events it publishes: quotes.template_changed
What it writes: quote_template (insert) — Saves the template; quote_event (insert) — Notes on the quote that it was saved as a template
curl -X POST https://app.getera.io/api/v1/quote_templates/actions/create_template \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Template name"}'{
"ok": true,
"result": "…",
"events": [
"quotes.template_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quoteTemplates.createTemplate({
name: "Template name"
})era quote_templates create_template \
--name "Template name"{
"name": "era__quotes__create_template",
"arguments": {
"name": "Template name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_template_name_taken, quote_not_found, invalid_currency, quote_needs_lines, invalid_quote_lines · 429 slow down
quotes.delete_template
Delete a quote template — Removes a template; quotes made from it are untouched.
Needs quotes:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/quote_templates/{id}/actions/delete_template- Canonical address:
POST /api/v1/actions/quotes.delete_template
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Template · id of quote_template |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_template_not_found | The template must exist. |
Events it publishes: quotes.template_changed
What it writes: quote_template (delete) — Deletes the template
curl -X POST https://app.getera.io/api/v1/quote_templates/QUOTE_TEMPLATE_ID/actions/delete_template \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"quotes.template_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quoteTemplates.deleteTemplate("QUOTE_TEMPLATE_ID")era quote_templates delete_template QUOTE_TEMPLATE_ID{
"name": "era__quotes__delete_template",
"arguments": {
"id": "QUOTE_TEMPLATE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_template_not_found · 429 slow down
quotes.duplicate
Duplicate — A new draft quote with the same lines — for the same person or another one.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quotes/{id}/actions/duplicate- Canonical address:
POST /api/v1/actions/quotes.duplicate
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
customerId | uuid | For · id of customer |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
customer_not_found | The person must exist. |
customer_archived | An archived person is not quoted. |
quote_needs_lines | A quote needs at least one line. |
invalid_quote_lines | Every line needs a description, a positive quantity and a price; a catalog item must exist. |
Events it publishes: quotes.created, quotes.duplicated
What it writes: quote (insert) — Opens the copy with its own number; quote_version (insert) — Writes version 1; quote_line (insert) — Copies the lines, re-priced; quote_event (insert) — Records where it came from; quote (update) — Remembers the original
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/duplicate \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"quotes.created",
"quotes.duplicated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.duplicate("QUOTE_ID", {
customerId: "CUSTOMER_ID"
})era quotes duplicate QUOTE_ID \
--customer-id "CUSTOMER_ID"{
"name": "era__quotes__duplicate",
"arguments": {
"id": "QUOTE_ID",
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, customer_not_found, customer_archived, quote_needs_lines, invalid_quote_lines · 429 slow down
quotes.expire_due
Lapse overdue quotes — Machine-only daily routine: a sent quote past its valid-until day becomes expired.
Needs quotes:write · changes data
403 machine_only).https://app.getera.io/api/v1/quote_events/actions/expire_due- Canonical address:
POST /api/v1/actions/quotes.expire_due
Input
This action takes no input.
Events it publishes: quotes.expired
What it writes: quote (update) — Marks overdue quotes expired; quote_event (insert) — Records the lapse on each
Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
quotes.reject
Mark declined — Records the customer’s no, with their reason when they gave one.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quotes/{id}/actions/reject- Canonical address:
POST /api/v1/actions/quotes.reject
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
name | string | Answered by · max 200 | |
note | string | Note · max 4000 | |
via | string (enum) | How · Allowed values: manual, link | |
signatureName | string | Typed signature · max 200 | |
ip | string | From address · max 64 | |
userAgent | string | Browser · max 400 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_open | Only a sent quote is answered. |
Events it publishes: quotes.rejected
What it writes: quote (update) — Marks it declined; quote_event (insert) — Records the answer
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/reject \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Answered by"}'{
"ok": true,
"result": "…",
"events": [
"quotes.rejected"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.reject("QUOTE_ID", {
name: "Answered by"
})era quotes reject QUOTE_ID \
--name "Answered by"{
"name": "era__quotes__reject",
"arguments": {
"id": "QUOTE_ID",
"name": "Answered by"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_open · 429 slow down
quotes.revise
Revise into a new version — Copies the sent version into a new draft version; the old link stops answering.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quotes/{id}/actions/revise- Canonical address:
POST /api/v1/actions/quotes.revise
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_revisable | Only a sent, declined or lapsed quote is revised. |
Events it publishes: quotes.revised
What it writes: quote_version (insert) — Writes the new version; quote_line (insert) — Copies the lines; quote (update) — Points at the new version, back to draft; quote_event (insert) — Records the revision
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/revise \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"quotes.revised"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.revise("QUOTE_ID")era quotes revise QUOTE_ID{
"name": "era__quotes__revise",
"arguments": {
"id": "QUOTE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_revisable · 429 slow down
quotes.send
Send by email — Emails the current version with a link to accept or decline it; the record shows whether it went.
Needs quotes:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/quotes/{id}/actions/send- Canonical address:
POST /api/v1/actions/quotes.send
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
to | string | Email · max 320 | |
message | string | Message · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_sendable | Only a draft or an open quote is sent. |
quote_validity_passed | A quote past its valid-until day is not sent. |
quote_needs_lines | A quote needs at least one line. |
quote_recipient_missing | There must be an address to send to. |
Events it publishes: quotes.send_requested, quotes.sent
What it writes: quote (update) — Marks it sent and mints its answer link; quote_version (update) — Stamps when this version was sent; quote_event (insert) — Records who it was sent to
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/send \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"to":"Email"}'{
"ok": true,
"result": "…",
"events": [
"quotes.send_requested",
"quotes.sent"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.send("QUOTE_ID", {
to: "Email"
})era quotes send QUOTE_ID \
--to "Email"{
"name": "era__quotes__send",
"arguments": {
"id": "QUOTE_ID",
"to": "Email"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_sendable, quote_validity_passed, quote_needs_lines, quote_recipient_missing · 429 slow down
quotes.update_draft
Edit a draft quote — Changes the title, lines, validity or notes — only while the current version is a draft.
Needs quotes:write · changes data
https://app.getera.io/api/v1/quotes/{id}/actions/update_draft- Canonical address:
POST /api/v1/actions/quotes.update_draft
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
title | string | Title · max 200 | |
validUntil | date | Valid until | |
notes | string | Notes · max 4000 | |
taxCountry | string | Tax country · max 2 | |
lines | object / array | Lines |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_draft | Only a draft is edited. |
quote_needs_lines | A quote needs at least one line. |
invalid_quote_lines | Every line needs a description, a positive quantity and a price; a catalog item must exist. |
Events it publishes: quotes.updated
What it writes: quote (update) — Updates the title; quote_version (update) — Updates validity, notes and totals; quote_line (upsert) — Rewrites the lines; quote_event (insert) — Records the edit
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/update_draft \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Title"}'{
"ok": true,
"result": "…",
"events": [
"quotes.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.updateDraft("QUOTE_ID", {
title: "Title"
})era quotes update_draft QUOTE_ID \
--title "Title"{
"name": "era__quotes__update_draft",
"arguments": {
"id": "QUOTE_ID",
"title": "Title"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_draft, quote_needs_lines, invalid_quote_lines · 429 slow down
quotes.update_template
Edit a quote template — Changes a template; quotes already made from it keep their own copy.
Needs quotes:manage · changes data
https://app.getera.io/api/v1/quote_templates/{id}/actions/update_template- Canonical address:
POST /api/v1/actions/quotes.update_template
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Template · id of quote_template |
name | string | Template name · max 120 | |
title | string | Quote title · max 200 | |
notes | string | Notes for the customer · max 4000 | |
currency | string | Currency · max 3 | |
lines | object / array | Lines |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_template_not_found | The template must exist. |
quote_template_name_taken | Template names are unique in a workspace. |
quote_needs_lines | A quote needs at least one line. |
invalid_quote_lines | Every line needs a description, a positive quantity and a price; a catalog item must exist. |
Events it publishes: quotes.template_changed
What it writes: quote_template (update) — Rewrites the template
curl -X POST https://app.getera.io/api/v1/quote_templates/QUOTE_TEMPLATE_ID/actions/update_template \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Template name"}'{
"ok": true,
"result": "…",
"events": [
"quotes.template_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quoteTemplates.updateTemplate("QUOTE_TEMPLATE_ID", {
name: "Template name"
})era quote_templates update_template QUOTE_TEMPLATE_ID \
--name "Template name"{
"name": "era__quotes__update_template",
"arguments": {
"id": "QUOTE_TEMPLATE_ID",
"name": "Template name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_template_not_found, quote_template_name_taken, quote_needs_lines, invalid_quote_lines · 429 slow down
quotes.withdraw
Withdraw — Takes a draft or an open quote off the table; its link stops answering.
Needs quotes:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/quotes/{id}/actions/withdraw- Canonical address:
POST /api/v1/actions/quotes.withdraw
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Quote · id of quote |
reason | string | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
quote_not_found | The quote must exist. |
quote_not_withdrawable | Only a draft or an open quote is withdrawn. |
Events it publishes: quotes.withdrawn
What it writes: quote (update) — Marks it withdrawn; quote_event (insert) — Records why
curl -X POST https://app.getera.io/api/v1/quotes/QUOTE_ID/actions/withdraw \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"quotes.withdrawn"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.quotes.withdraw("QUOTE_ID", {
reason: "Reason"
})era quotes withdraw QUOTE_ID \
--reason "Reason"{
"name": "era__quotes__withdraw",
"arguments": {
"id": "QUOTE_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold quotes:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 quote_not_found, quote_not_withdrawable · 429 slow down
Data models
QuoteEvent
Table quote_event · Resource quote_events
GET /api/v1/quote_eventsGET /api/v1/quote_events/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
quote_id | string (uuid) · uuid | quote | ||
type | string · text | created, updated, revised, send_requested, accepted, rejected, expired, withdrawn, note_added, duplicated, template_saved | ||
message | string · text | yes | ||
parameters | object · jsonb | |||
actor_user_id | string (uuid) · uuid | yes | app_user | |
occurred_at | string (date-time) · timestamptz |
Primary key: id
QuoteLine
Table quote_line · Resource quote_lines
GET /api/v1/quote_linesGET /api/v1/quote_lines/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
quote_id | string (uuid) · uuid | quote | ||
version_id | string (uuid) · uuid | quote_version | ||
description | string · text | |||
quantity | string (decimal) · numeric | |||
item_variant_id | string (uuid) · uuid | yes | item_variant | |
currency | string · bpchar | max 3 | ||
unit_amount | string (int64) · int8 | |||
discount_amount | string (int64) · int8 | |||
tax_class_id | string (uuid) · uuid | yes | tax_class | |
tax_class_name | string · text | yes | ||
tax_rate | string (decimal) · numeric | |||
prices_entered_with_tax | boolean · bool | |||
net_amount | string (int64) · int8 | |||
tax_amount | string (int64) · int8 | |||
total_amount | string (int64) · int8 | |||
display_order | integer · int4 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((discount_amount >= 0))CHECK ((net_amount >= 0))CHECK ((quantity > (0)::numeric))CHECK ((tax_amount >= 0))CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))CHECK ((total_amount >= 0))CHECK ((unit_amount >= 0))
QuoteTemplate
Table quote_template · Resource quote_templates
GET /api/v1/quote_templatesGET /api/v1/quote_templates/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
title | string · text | |||
notes | string · text | yes | ||
currency | string · bpchar | yes | max 3 | |
lines | object · jsonb | |||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((currency IS NULL) OR (currency ~ '^[A-Z]{3}$'::text)))CHECK ((jsonb_typeof(lines) = 'array'::text))CHECK ((length(TRIM(BOTH FROM name)) > 0))CHECK ((length(TRIM(BOTH FROM title)) > 0))
QuoteVersion
Table quote_version · Resource quote_versions
GET /api/v1/quote_versionsGET /api/v1/quote_versions/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
quote_id | string (uuid) · uuid | quote | ||
version_no | integer · int4 | |||
currency | string · bpchar | max 3 | ||
tax_country | string · bpchar | yes | max 2 | |
valid_until | string (date) · date | yes | ||
notes | string · text | yes | ||
subtotal_amount | string (int64) · int8 | |||
discount_total_amount | string (int64) · int8 | |||
tax_total_amount | string (int64) · int8 | |||
total_amount | string (int64) · int8 | |||
sent_at | string (date-time) · timestamptz | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: quote_id, version_no
Checks:
CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((discount_total_amount >= 0))CHECK ((subtotal_amount >= 0))CHECK (((tax_country IS NULL) OR (tax_country ~ '^[A-Z]{2}$'::text)))CHECK ((tax_total_amount >= 0))CHECK ((total_amount >= 0))CHECK ((version_no >= 1))
Quote
Table quote · Resource quotes
GET /api/v1/quotesGET /api/v1/quotes/{id}POST /api/v1/quotes→quotes.create
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
number | string · text | |||
title | string · text | |||
status | string · text | draft, sent, accepted, rejected, expired, withdrawn | ||
customer_id | string (uuid) · uuid | customer | ||
lead_id | string (uuid) · uuid | yes | ||
owner_user_id | string (uuid) · uuid | yes | app_user | |
current_version_id | string (uuid) · uuid | yes | quote_version | |
accepted_version_id | string (uuid) · uuid | yes | quote_version | |
link_expires_at | string (date-time) · timestamptz | yes | ||
last_viewed_at | string (date-time) · timestamptz | yes | ||
sent_at | string (date-time) · timestamptz | yes | ||
responded_at | string (date-time) · timestamptz | yes | ||
response_name | string · text | yes | ||
response_note | string · text | yes | ||
responded_via | string · text | yes | ||
withdrawn_at | string (date-time) · timestamptz | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
signature_name | string · text | yes | ||
signed_at | string (date-time) · timestamptz | yes | ||
signature_ip | string · text | yes | ||
signature_user_agent | string · text | yes | ||
duplicated_from_id | string (uuid) · uuid | yes | quote | |
template_id | string (uuid) · uuid | yes | quote_template |
Primary key: id
Checks:
CHECK (((responded_via IS NULL) OR (responded_via = ANY (ARRAY['link'::text, 'manual'::text]))))CHECK ((length(TRIM(BOTH FROM title)) > 0))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
quotes.accepted | The customer accepted a quote. | id, number, customerId, leadId, ownerUserId, versionNo, totalAmount, currency, description, lines, via |
quotes.created | A quote was started. | id, number, customerId, leadId, ownerUserId, totalAmount, currency |
quotes.duplicated | A quote was copied into a new draft. | id, fromId, ownerUserId |
quotes.expired | A sent quote lapsed unanswered. | id, number, ownerUserId |
quotes.note_added | A note was added to a quote. | id |
quotes.rejected | The customer declined a quote. | id, number, customerId, ownerUserId, reason, via |
quotes.revised | A quote was revised into a new version. | id, versionNo |
quotes.send_requested | A quote was sent by email; notifications delivers it. | id, eventId, to |
quotes.sent | A quote went out to the customer. | id, number, customerId, ownerUserId, versionNo |
quotes.template_changed | A quote template was saved, edited or deleted. | id |
quotes.updated | A draft quote was edited. | id |
quotes.withdrawn | A quote was withdrawn. | id, ownerUserId |