Proposals API and integrations
Beta. Integrations are switched on per team while they are in beta. If you don't see Settings → Integrations in your Proposals account, write to support@proposals.co and we'll turn it on.
Proposals integrations let you create and manage proposals from the tools you already use: ChatGPT, Claude, or your CRM through Zapier, Make or n8n. A proposal made this way goes through the same engine as one made in the panel: your brand, your price, the same quality checks, the same tracking. You see it in the panel like any other proposal, marked with where it came from.
Two things integrations deliberately don't do:
- Proposals doesn't write or send emails through the API. You get the public link and the key facts of the proposal. The email is written by you, or by the assistant you're talking to, which knows the conversation and the client better than any template.
- One proposal per call. You can create many proposals, each with its own request and within the rate limits. There is no bulk import and no campaign endpoint.
Guides
| Guide | For |
|---|---|
| ChatGPT | Connecting Proposals to ChatGPT and making a proposal in a chat |
| Claude | Connecting Proposals to Claude and making a proposal in a chat |
| Zapier, Make and n8n | Creating proposals from a CRM deal with an API key |
| API reference | Every endpoint with request and response examples |
The live OpenAPI description is at https://api.proposals.co/api/public/v1/openapi.json.
Two ways in
| REST API | MCP server | |
|---|---|---|
| For | Automations and your own code | ChatGPT, Claude and other assistants that support MCP connectors |
| Address | https://api.proposals.co/api/public/v1 |
https://api.proposals.co/mcp |
| Sign-in | API key (Authorization: Bearer prp_live_…) |
OAuth: you log in to Proposals and approve the permissions |
| Who sets it up | A Proposals admin creates the key | Any team member connects their own assistant |
Both call the same service, so the rules below apply to both.
Authentication
API keys (REST)
An admin creates keys in Settings → Integrations → API keys. The secret starts with prp_live_ and is shown once, right after you create it. Store it in your automation tool's secret storage, never in a shared document.
Send it in the Authorization header on every request:
Authorization: Bearer prp_live_...The key is never accepted in the query string. Keys are valid for 365 days by default, and you can choose a shorter period when you create one. Every team member can see the list of keys with their prefix and when each was last used; only admins can create or revoke them.
A key acts on behalf of the admin who created it and never has more rights than that person. If they leave the organization, the key stops working.
Connected apps (MCP)
ChatGPT and Claude connect through OAuth. When you add the connector, you are sent to Proposals to log in and approve the permissions. Publishing is unticked by default. You can disconnect an app at any time in Settings → Integrations → Connected apps, and its access ends immediately.
Permissions (scopes)
| Scope | Lets the integration |
|---|---|
brands:read |
See your brands (name, language, currency), so it can pick the right sender |
proposals:read |
Read proposals: status, questions, key facts, link, whether the client opened or accepted |
proposals:write |
Create proposals, answer their questions, ask for revisions, change recipient and terms, mark as sent |
proposals:publish |
Publish a proposal (make its link live) and unpublish it |
A new API key gets brands:read, proposals:read and proposals:write. proposals:publish is only granted when you tick it. Without it, an integration can prepare a draft, but a person publishes it in the panel.
A call without the needed scope fails with 403 insufficient_scope, and detail.params.required names the missing scope.
What stays in your control
- Drafts are private. Creating a proposal never publishes it.
- Publishing needs three things at once: the
proposals:publishscope, no blockers in the pre-publish check, and an explicit"confirm": truein the call. There is no way to force past a blocker through the API. - You see everything. Every proposal shows its source in the panel (for example "Created in ChatGPT"), each proposal has a history of what integrations did with it, and publishing through an integration sends the proposal owner a notification.
- Your data stays yours. Other organizations' proposals and brands don't exist for your key: they answer
404, never403.
Proposal status
Every proposal has one status, computed from its current state:
| Status | Meaning | What to do |
|---|---|---|
generating |
The draft is being written (usually 30–120 seconds) | Poll again after next_poll_after_ms |
needs_input |
The notes are ambiguous, for example two different prices or two company names | Ask the user the questions, then send the answers |
ready_for_review |
The draft is ready and nothing blocks publishing | Review it (panel_url) or publish it |
blocked |
The draft is ready but the pre-publish check found a blocker | Read preflight.blockers and fix them, usually in the panel |
failed |
Generation didn't finish; error.code says why |
Fix the cause, then retry with an empty answers call |
draft |
The proposal exists but has no page yet (for example it was started in the panel) | Continue in the panel |
published |
The link is live | Send it to the client |
sent |
You marked it as sent, or it was sent from the panel | Wait for the client |
opened |
The client opened it | Follow up while it's fresh |
accepted |
The client accepted it | – |
Publishing is not sending. A proposal becomes sent only when you say so (mark-sent) or when it was sent from the panel.
Asynchronous work and polling
Writing a proposal takes time, so POST /proposals, POST /proposals/{id}/answers and POST /proposals/{id}/revise return 202 right away with the proposal in status generating. Then:
- Wait
next_poll_after_msmilliseconds (it's only present while the status isgenerating). - Call
GET /proposals/{id}. - Repeat until the status is anything other than
generating.
If your tool can't loop, add ?wait_seconds=50 to POST /proposals: the call waits up to 50 seconds and returns the state at that moment. It may still be generating, so keep a fallback poll.
Idempotency
Every POST needs an Idempotency-Key header. Use a value that is unique per intended action, for example your CRM deal ID plus the action name (deal-731-create).
- Same key, same body: you get the original response back, and nothing is done twice. This is safe after a timeout or a lost response.
- Same key, different body:
409 idempotency_conflict, nothing happens. - Same key while the first request is still running:
409 idempotency_in_progress; retry a moment later. - A
POSTwithout the header:422 idempotency_key_required.
Keys are remembered for 30 days and are scoped to your credential. Keys can be up to 255 characters long.
external_reference protects you a second time: if you set it (for example to your CRM deal ID), a second proposal with the same value fails with 409 external_reference_exists, and detail.params.proposal_id points to the existing one.
MCP clients don't send headers, so the MCP server deduplicates on its own: the same tool call with the same arguments within 10 minutes returns the first result.
Limits
Per API key or connected app:
- 60 reads per minute,
- 10 new proposals per minute,
- 20 revisions per 10 minutes,
- 30 other changes per minute.
Per organization, at most 3 proposals can be generating at the same time.
Over a limit, you get 429 with a Retry-After header (seconds). rate_limited means your credential sent too many requests; too_many_generations means three proposals are already being written.
Your plan applies exactly as in the panel: a proposal created through an integration counts as one created proposal, and AI actions follow the same fair use. When the plan blocks an action, you get 402 with the same codes as in the panel.
Errors
Every error has the same shape:
{
"detail": {
"code": "insufficient_scope",
"message": "This key can't publish proposals.",
"params": { "required": "proposals:publish" }
},
"request_id": "req_01J..."
}message is written for a person and follows Accept-Language (pl gives Polish, anything else English). Build your logic on code, not on message. Include request_id when you write to support.
| HTTP | Common codes | Meaning |
|---|---|---|
401 |
invalid_credentials |
The key or token is missing, wrong, expired or revoked |
402 |
trial_offer_cap, usage_limit_exceeded, fair_use_exceeded, subscription_inactive |
The plan doesn't allow this right now; resolve it in the panel's billing settings |
403 |
insufficient_scope, integrations_not_enabled |
Missing permission, or integrations aren't switched on for your organization |
404 |
not_found |
It doesn't exist, or it belongs to another organization |
409 |
idempotency_conflict, idempotency_in_progress, external_reference_exists, site_conflict, generation_in_progress, offer_accepted |
Conflict with the current state; see below |
422 |
idempotency_key_required, preflight_blockers, invalid input |
The request can't be carried out as sent |
429 |
rate_limited, too_many_generations |
Wait for Retry-After seconds |
A few codes worth handling explicitly:
site_conflict– someone edited the draft since the revision you sent inexpected_revision. Nothing was lost: read the proposal again and decide.generation_in_progress– the proposal is already being written; poll instead of starting again.offer_accepted– the client has accepted, so the proposal can no longer be changed.preflight_blockers– publishing was refused; the blockers are listed in the proposal'spreflight.brand_name_unconfirmed– the brand's name hasn't been confirmed yet; confirm it in the panel under brand settings.email_verification_required– the account behind the credential hasn't verified its email address yet.
Versioning
The API is versioned in the path (/api/public/v1). Within v1 we only add things: new fields, new optional parameters, new error codes. Write clients that ignore fields they don't know. Changes that would break a client go to /v2, and v1 keeps working for at least 12 months after v2 is announced.
While integrations are in beta we avoid breaking changes. If one becomes unavoidable, it is listed in the changelog below before it ships.
Changelog
- 2026-10-05 – MCP: direct editing of the draft (
get_proposal_content,edit_proposal_content,update_proposal_settings, version history, discard changes, unpublish). REST:PATCH /proposals/{id}also takesclient_name,client_website,service_type,next_stepandpricing. - 2026-09-26 –
v1beta: brands, proposals (create, read, list, answers, revise, update, publish, unpublish, mark as sent), MCP server with OAuth.