All integrations

Better Proposals

BUSINESS · CRM & SUPPORT

Proposals, quotes, templates, and their sent, signed and paid states.

Acts as the person, not as itself

Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.

Credentials never touch the agent

Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.

Every call on the record

Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.

What an agent can do

Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.

betterproposals_get_companyREAD

List the account's companies via GET /company, with `page` and `per_page`. A company is the client a proposal is addressed to; each row carries its id, name, CRM id, demo flag and its created, edited and deleted stamps.

api
betterproposals_get_company_company_idREAD

Fetch one company by id via GET /company/{COMPANY_ID}. AN ID THAT MATCHES NOTHING IS A 200, NOT A 404: measured 2026-09-19 on 0 and on 999999999, both answer HTTP 200 {"status":"success","data":[]} -- `data` is an empty ARRAY where a hit gives an OBJECT. Check the shape of `data`, not the status. Quotes and templates behave the same way; proposals 404 instead and currencies answer 200 with no `data` key at all.

api
betterproposals_get_currencyREAD

List the currencies Better Proposals supports via GET /currency, with `page` and `per_page`. Each row carries the 3-letter code that `Currency` on `betterproposals_post_proposal_create` takes, its symbol and name, whether it is zero-decimal, and whether Stripe and PayPal accept it. This is a platform catalogue rather than account data -- it reads the same for every account.

api
betterproposals_get_currency_currency_idREAD

Fetch one currency by id via GET /currency/{CURRENCY_ID}. The id is the catalogue's own numeric id from `betterproposals_get_currency`, not the 3-letter code. A THIRD absence shape, measured 2026-09-19: an id that matches nothing answers HTTP 200 {"status":"success","message":"Not found"} -- status "success", no `data` key at all. A caller that reads `data` unconditionally raises a KeyError on a miss here.

api
betterproposals_get_doctypeREAD

List the account's document types via GET /doctype, with `page` and `per_page`. A document type is the category a document belongs to -- Proposals, Quotes, Contracts and any type the account adds -- and each row carries its id, name, singular name, colour, icon and how many outstanding documents and templates use it. These ids are what the `type` filter on the proposal lists takes.

api
betterproposals_get_proposalREAD

List the account's proposals via GET /proposal. Returns every document the account holds, newest first, each with its company, contacts, amount, currency, status flags and its view and preview URLs. `type` narrows the list to ONE document type by its numeric id -- the ids come from `betterproposals_get_doctype` (measured 2026-09-19: 1 is Proposals and 2 is Quotes on a fresh account) -- and `page` with `per_page` walk it. This is also the endpoint the connect-time probe calls.

api
betterproposals_get_proposal_countREAD

Count the account's proposals via GET /proposal/count. Answers {"status":"success","count":<n>} and takes no parameters at all -- it is the whole-account total, not a filtered one. Cheaper than paging `betterproposals_get_proposal` when only the number is wanted.

api
betterproposals_get_proposal_newREAD

List proposals still in the NEW state via GET /proposal/new -- created and not yet sent. Same record shape and same `page`, `per_page` and `type` narrowing as `betterproposals_get_proposal`; this is the state filter rather than a different resource.

api
betterproposals_get_proposal_openedREAD

List proposals the recipient has OPENED via GET /proposal/opened. Same record shape and same `page`, `per_page` and `type` narrowing as `betterproposals_get_proposal`; this is the state filter rather than a different resource.

api
betterproposals_get_proposal_paidREAD

List proposals that have been PAID via GET /proposal/paid. Same record shape and same `page`, `per_page` and `type` narrowing as `betterproposals_get_proposal`; this is the state filter rather than a different resource.

api
betterproposals_get_proposal_proposal_idREAD

Fetch one proposal by id via GET /proposal/{PROPOSAL_ID}. Returns the fuller record the list form does not carry -- brand, cover, template, document type, quote id, customer journey, tax, the signature and payment timestamps and the preview link. THE ONLY BY-ID ROUTE ON THIS API THAT 404s: measured 2026-09-19, an id that matches nothing answers HTTP 404 {"status":"error","message":"Document not found"} -- a JSON envelope, unlike the HTML 404 a wrong PATH produces. Its three siblings do the opposite and answer 200 (see `betterproposals_get_company_company_id`), so absence is spelled differently on every one of them.

api
betterproposals_get_proposal_sentREAD

List proposals that have been SENT via GET /proposal/sent. Same record shape and same `page`, `per_page` and `type` narrowing as `betterproposals_get_proposal`; this is the state filter rather than a different resource.

api
betterproposals_get_proposal_signedREAD

List proposals the recipient has SIGNED via GET /proposal/signed. Same record shape and same `page`, `per_page` and `type` narrowing as `betterproposals_get_proposal`; this is the state filter rather than a different resource.

api
betterproposals_get_quoteREAD

List the account's quotes via GET /quote, with `page` and `per_page`. A quote is the priced half of a proposal: each row carries the company it belongs to, its one-off, monthly, quarterly and annual amounts, its VAT and total, and its accepted, archived and dead flags. Creating a proposal creates its quote alongside it.

api
betterproposals_get_quote_quote_idREAD

Fetch one quote by id via GET /quote/{QUOTE_ID}. An id that matches nothing answers HTTP 200 with `data` as an empty ARRAY rather than 404 (measured 2026-09-19 on 0 and on 999999999), so check the shape of `data` and not the status.

api
betterproposals_get_settingsREAD

Read the account's settings via GET /settings. Returns the default currency id, whether tax is on with its label and amount, the account time zone and the customer-journey defaults. Takes no parameters. Measured 2026-09-19: the response carries no part of the API token.

api
betterproposals_get_settings_brandREAD

Read the account's DEFAULT brand via GET /settings/brand. A brand carries the company name, logo, colours, currency, tax defaults and the badge setting that new proposals inherit when their own fields are left empty. Takes no parameters, and answers the default brand rather than a list.

api
betterproposals_get_settings_merge_tagREAD

List the account's custom merge tags via GET /settings/merge_tag, with `page` and `per_page`. A merge tag is a named placeholder a template substitutes at render time, and only tags listed here may be passed as `MergeTags` on `betterproposals_post_proposal_create` -- measured 2026-09-19, an undefined tag answers HTTP 500 {"status":"error","message":"Invalid MergeTag: <tag>"}. Call this first when a create is going to carry merge tags.

api
betterproposals_get_templateREAD

List the account's proposal templates via GET /template, with `page` and `per_page`. A template id is what `Template` on `betterproposals_post_proposal_create` copies from. Templates are authored in the Better Proposals web app: the API publishes no route that creates one (measured 2026-09-19, POST /template/create answers 405 "Invalid method").

api
betterproposals_get_template_template_idREAD

Fetch one template by id via GET /template/{TEMPLATE_ID}. An id that matches nothing answers HTTP 200 with `data` as an empty ARRAY rather than 404 (measured 2026-09-19 on 0, 1 and 999999999), so check the shape of `data` and not the status -- and note that an account with no templates cannot tell those two cases apart, because every id answers the same empty 200.

api
betterproposals_post_company_createWRITE

Create a company via POST /company/create. The reference marks `CompanyName` required and the server does NOT enforce it: measured 2026-09-19, an empty body answers HTTP 200 and creates a company row with `CompanyName: null`, which is a real record that then appears in every picker. It is required HERE, at the tool boundary, because it is the only thing a company has. Note also that `Company` on `betterproposals_post_proposal_create` creates a company from a name on its own, so this route is for creating one ahead of the proposal that will use it.

api
betterproposals_post_doctype_createWRITE

Create a document type via POST /doctype/create. `TypeName` is required and enforced -- measured 2026-09-19, omitting it answers HTTP 400 {"status":"error","message":"TypeName is required."} -- and `TypeColour` defaults to #01A3EF. The new type's id is usable immediately as `DocumentType` on `betterproposals_post_proposal_create`. THE ONE OPERATION ON THIS API THAT ANSWERS 3xx: a `TypeName` the account already holds answers HTTP 303 See Other, with NO Location header and a JSON body {"status":"error","message":"Document type existing. ID: <n>"} -- a status code used as a duplicate error, not a redirect. Use the id it names rather than retrying. Document types are the only resource here that dedupes: companies with the same name are created as separate records (measured the same day).

api
betterproposals_post_proposal_cover_createWRITE

Create a proposal cover page via POST /proposal/cover/create. A cover is the title card a proposal opens with -- headline, subheader, background and button styling -- and its id is what `Cover` on `betterproposals_post_proposal_create` takes. Every field is optional and each has a vendor default; `BrandID` left empty uses the account's default brand.

api
betterproposals_post_proposal_createWRITE

Create a proposal via POST /proposal/create. `Company` is the only required field and takes EITHER an existing company id OR a name, which creates the company; `DocumentType` and `Cover` behave the same way, and `Template` copies an existing template. The response carries the new proposal's id, its ProposalView URL and a preview link. Everything left empty falls back to the default brand's settings. Measured 2026-09-19: omitting `Company` answers HTTP 500 {"status":"error","message":"Please provide company"} -- this provider spells some VALIDATION failures 500, so read the message rather than the status. `MergeTags` accepts only tags the account has already defined (see `betterproposals_get_settings_merge_tag`); an undefined one answers 500 "Invalid MergeTag: <tag>".

api
betterproposals_post_quote_createWRITE

Create a quote via POST /quote/create. `CompanyID` names the company it belongs to and `TemplateID`, when supplied, copies that template's amounts onto it. The reference marks `CompanyID` required and the server does NOT enforce it: measured 2026-09-19, an empty body answers HTTP 200 and creates a quote with `CompanyID: null`, and an id belonging to no company is accepted as given rather than refused. Supply a real id from `betterproposals_get_company` -- the provider will not catch a wrong one for you.

api

Put Better Proposals behind one governed endpoint.

Same permissions, same audit trail, whatever else you connect next.