ProposalsProposals

API reference (v1)

Beta. The live, machine-readable description of this API is at https://api.proposals.co/api/public/v1/openapi.json. If this page and the OpenAPI ever disagree, the OpenAPI is right.

  • Base URL: https://api.proposals.co/api/public/v1
  • Authentication: Authorization: Bearer prp_live_… on every request (details)
  • Content type: application/json
  • Every POST needs an Idempotency-Key header (details)
  • Language of message fields and questions: Accept-Language: pl gives Polish, anything else English
  • Errors: {"detail": {"code", "message", "params"}, "request_id"} (codes)
Method and path Scope Returns
GET /brands brands:read Your brands
POST /proposals proposals:write 202 Proposal
GET /proposals/{id} proposals:read Proposal
GET /proposals proposals:read Page of proposals
POST /proposals/{id}/answers proposals:write 202 Proposal
POST /proposals/{id}/revise proposals:write 202 Proposal
PATCH /proposals/{id} proposals:write Proposal
POST /proposals/{id}/publish proposals:publish Proposal with public_url
POST /proposals/{id}/unpublish proposals:publish Proposal
POST /proposals/{id}/mark-sent proposals:write Proposal

The Proposal object

Every proposal endpoint returns this object (lists return a shorter version).

{
  "id": 123,
  "status": "ready_for_review",
  "questions": [],
  "error": null,
  "preflight": {
    "blockers": [],
    "warnings": [
      { "code": "price_without_tax_basis", "message": "The price doesn't say whether it's net or gross." }
    ]
  },
  "revision": 7,
  "language": "en",
  "currency": "USD",
  "client": {
    "name": "Lila Flowers",
    "website": "https://lila-flowers.example",
    "contact": { "name": "Anna Novak", "email": "anna@lila-flowers.example" }
  },
  "brand": { "id": 42, "name": "Northwind Studio" },
  "facts": {
    "title": "New online store for Lila Flowers",
    "scope": [
      "Six-page store with delivery-date picker",
      "Product photography for 40 bouquets",
      "Launch support for the first 30 days"
    ],
    "price": "$9,000 net, 50% upfront, 50% on delivery",
    "valid_until": "2026-10-31"
  },
  "panel_url": "https://app.proposals.co/offers/123",
  "public_url": null,
  "engagement": {
    "sent_at": null,
    "first_opened_at": null,
    "last_opened_at": null,
    "opens": 0,
    "questions_asked": 0,
    "accepted_at": null
  },
  "deal_status": "open",
  "source": "api",
  "source_client": "Zapier – CRM",
  "external_reference": "crm-deal-731",
  "created_at": "2026-09-26T09:12:03Z",
  "updated_at": "2026-09-26T09:13:41Z",
  "next_poll_after_ms": null
}
Field Notes
status generating, needs_input, ready_for_review, blocked, failed, draft, published, sent, opened, accepted. See statuses.
questions Filled when status is needs_input. Each has kind (client, budget or currency), a question to show the user, and options with what was found in the notes.
error Filled when status is failed: code and a human message.
preflight Result of the pre-publish check. Any item in blockers prevents publishing; warnings don't.
revision The draft's revision number. Send it back as expected_revision to make sure you're changing what you last saw.
facts Computed from the proposal itself, without AI. Use them when writing the email; don't promise anything beyond them.
panel_url The proposal in the Proposals panel. Requires logging in; for your team, not for the client.
public_url The link for the client. null until the proposal is published; use it exactly as returned.
engagement What the client did: when the proposal was sent and opened, how many times, how many questions they asked, and when they accepted.
deal_status open, won or lost as set in the panel, or null. Read-only through the API.
source, source_client Where the proposal was created (panel, api, mcp) and by which key or app.
next_poll_after_ms Only while status is generating: how long to wait before reading again.

List brands

GET /brands · scope brands:read

curl https://api.proposals.co/api/public/v1/brands \
  -H "Authorization: Bearer $PROPOSALS_API_KEY"
{
  "items": [
    { "id": 42, "name": "Northwind Studio", "language": "en", "currency": "USD", "name_confirmed": true },
    { "id": 43, "name": "Northwind Polska", "language": "pl", "currency": "PLN", "name_confirmed": false }
  ]
}

A brand with name_confirmed: false can hold drafts, but its proposals end up blocked with brand_name_unconfirmed until someone confirms the name in the panel.

Create a proposal

POST /proposals · scope proposals:write · optional query wait_seconds (0–50)

curl -X POST https://api.proposals.co/api/public/v1/proposals \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-deal-731-create" \
  -d '{
    "client": {
      "name": "Lila Flowers",
      "website": "https://lila-flowers.example",
      "contact": { "name": "Anna Novak", "email": "anna@lila-flowers.example" }
    },
    "context": "Call notes, 26 Sep: they want to sell bouquets online with a delivery-date picker...",
    "brand_id": 42,
    "language": "en",
    "pricing": {
      "amount": 9000,
      "currency": "USD",
      "tax_basis": "net",
      "billing": "one_off",
      "schedule": "50% upfront, 50% on delivery"
    },
    "valid_until": "2026-10-31",
    "external_reference": "crm-deal-731"
  }'
Field Required Notes
client.name yes 1–255 characters
client.website no Public https:// address. Proposals reads it to tailor the draft.
client.contact no name and email of the person the proposal is for
context yes Notes or a call transcript, 1–50,000 characters. Treated as information about the deal, never as instructions.
brand_id no From GET /brands. Without it, your default brand is used.
language no en or pl
pricing no The price shown on the proposal, see below. Without it, a price from the notes is used only after it's confirmed, the same as in the panel.
valid_until no Date the terms are valid until, YYYY-MM-DD
external_reference no Your ID for this deal, up to 128 characters, unique in your organization

pricing fields:

Field Notes
amount Greater than 0, or null for "price to be agreed". Never guessed and never turned into 0.
amount_max Optional upper end of a range (must be greater than amount)
currency PLN, USD, EUR or GBP
tax_basis net, gross or unknown (default)
billing one_off, monthly, quarterly, yearly, hourly, per_unit or unknown (default)
schedule Payment schedule in words, up to 200 characters

A price in pricing wins over amounts in the notes. If the notes mention a different amount, you get a question or a warning about it.

Response: 202 with the Proposal in status generating and next_poll_after_ms. With wait_seconds, the call waits up to that many seconds and returns the state at that moment.

Errors specific to this call: 409 external_reference_exists (with params.proposal_id), 404 for a brand_id that isn't yours, 429 too_many_generations, 402 when the plan doesn't allow another proposal.

Get a proposal

GET /proposals/{id} · scope proposals:read

curl https://api.proposals.co/api/public/v1/proposals/123 \
  -H "Authorization: Bearer $PROPOSALS_API_KEY"

Returns the Proposal. While it's being written:

{ "id": 123, "status": "generating", "next_poll_after_ms": 4000, "...": "..." }

When the notes need clarifying:

{
  "id": 123,
  "status": "needs_input",
  "questions": [
    {
      "kind": "client",
      "question": "Your notes are about Lila Garden Center, but the client is Lila Flowers. Who is this proposal for?",
      "options": { "client": "Lila Flowers", "found": "Lila Garden Center" }
    }
  ],
  "...": "..."
}

The exact content of options depends on kind; the OpenAPI lists it for each kind.

List proposals

GET /proposals · scope proposals:read

Query parameter Notes
status One of the statuses
updated_since ISO 8601 date-time
external_reference Exact match
source panel, api or mcp
limit Up to 50
cursor next_cursor from the previous page
curl "https://api.proposals.co/api/public/v1/proposals?source=api&limit=20" \
  -H "Authorization: Bearer $PROPOSALS_API_KEY"
{
  "items": [
    {
      "id": 123,
      "status": "opened",
      "client": { "name": "Lila Flowers" },
      "source": "api",
      "external_reference": "crm-deal-731",
      "updated_at": "2026-09-27T14:02:11Z"
    }
  ],
  "next_cursor": "eyJpZCI6MTIzfQ"
}

List items are a shorter version of the Proposal (abridged above; the OpenAPI lists every field). next_cursor is null on the last page.

Answer questions or retry

POST /proposals/{id}/answers · scope proposals:write

Send the user's answers to the proposal's questions. Every field is optional; send only what was asked.

Field Answers a question of kind
confirm_client: true client: the client in the request is right
client_name client: the proposal is for this company instead
pricing budget: the correct price, same shape as in create
currency currency: the correct currency
curl -X POST https://api.proposals.co/api/public/v1/proposals/123/answers \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-deal-731-answers-1" \
  -d '{"confirm_client": true}'

Response: 202 with the Proposal back in generating.

Retry after a failure: when the status is failed, send an empty body ({}) to try again once you've fixed the cause.

Ask for a revision

POST /proposals/{id}/revise · scope proposals:write

curl -X POST https://api.proposals.co/api/public/v1/proposals/123/revise \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-deal-731-revise-1" \
  -d '{"instruction": "Make the SEO section shorter and add a 5-week timeline.", "expected_revision": 7}'
Field Notes
instruction What to change, in plain words, 1–4,000 characters
expected_revision Optional. If the draft has changed since this revision, you get 409 site_conflict and nothing is overwritten.

Response: 202 with the Proposal in generating. When it finishes, revision is higher. Revisions change the draft only; if the proposal is already published, the client sees the change after it's published again.

Update recipient and terms

PATCH /proposals/{id} · scope proposals:write · no Idempotency-Key needed

curl -X PATCH https://api.proposals.co/api/public/v1/proposals/123 \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"recipient": {"name": "Anna Novak", "email": "anna@lila-flowers.example"}, "valid_until": "2026-11-15"}'
Field Notes
recipient name and email of the person the proposal is for
language en or pl
currency PLN, USD, EUR or GBP
valid_until YYYY-MM-DD
external_reference Up to 128 characters, unique in your organization
client_name The client company name
client_website The client's public website, http or https
service_type What you're selling, in a few words
next_step What the proposal asks the client to do: accept, call or variant
pricing Confirmed terms, same shape as in create; its currency must match currency when you send both

Response: the updated Proposal.

Publish

POST /proposals/{id}/publish · scope proposals:publish

curl -X POST https://api.proposals.co/api/public/v1/proposals/123/publish \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-deal-731-publish" \
  -d '{"confirm": true, "expected_revision": 7}'
Field Notes
confirm Must be true
expected_revision Optional; publish only if the draft is still at this revision

Response: the Proposal with status: "published" and public_url:

{ "id": 123, "status": "published", "public_url": "https://app.proposals.co/northwind-studio/lila-flowers?k=7Qm2xR", "...": "..." }

If the pre-publish check finds a blocker, you get 422 preflight_blockers and nothing is published. There is no force option in the API. Publishing through an integration sends the proposal owner a notification. Publishing doesn't send anything to the client.

Unpublish

POST /proposals/{id}/unpublish · scope proposals:publish · empty body

curl -X POST https://api.proposals.co/api/public/v1/proposals/123/unpublish \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Idempotency-Key: crm-deal-731-unpublish"

Response: the Proposal. The public link stops working for the client; the draft stays in the panel.

Mark as sent

POST /proposals/{id}/mark-sent · scope proposals:write

Call this after you (or your email tool) sent the link to the client. It starts the follow-up clock the same way as in the panel.

curl -X POST https://api.proposals.co/api/public/v1/proposals/123/mark-sent \
  -H "Authorization: Bearer $PROPOSALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-deal-731-mark-sent" \
  -d '{"sent_at": "2026-09-26T10:05:00Z"}'
Field Notes
sent_at Optional ISO 8601 date-time; defaults to now

Response: the Proposal with status: "sent" (or later) and engagement.sent_at set. The proposal is recorded as sent outside Proposals.

Start today

Send your next proposal as a page your client can say yes to

Paste the notes, review the draft, send one link.

Free for 14 days · 3 proposals included · No card needed