Proposals
A proposal is a change Carom’s AI suggests for your records, such as moving a deal to the next stage, creating a task an email asked for, or applying a tag. It records the change, the email excerpt or chat message that prompted it, and what happened to it. Use this resource to build an approval inbox, and to accept, reject, or undo changes.
Each proposal has a single approver, and only the approver can see or answer it. For everyone else in the account, admins included, it doesn’t exist: every endpoint here returns 404. Pending proposals expire after seven days.
Whether a change waits for approval depends on a level. At suggest it waits as a pending proposal. At auto Carom applies it immediately and still records a proposal, with status auto_applied, so it can be reviewed and undone. A background agent’s levels are account-wide settings that an admin sets on its agent configuration, where a level can also be off so the change is not proposed at all. The chat assistant’s levels belong to each user, in their assistant settings. email.send always waits for approval, because a sent email can’t be undone.
#The proposal object
Returned by every proposal endpoint, and embedded in daily briefings. What a proposal does is determined by its action_type and payload; display carries the names you need to describe it to a person.
Attributes
-
idstring · uuidUnique identifier for the proposal.
-
agentstringWhat proposed the change:
assistantfor Carom’s chat assistant, or the background agent that read the email or meeting, such asauto_dealorauto_task. -
action_typestringThe kind of change.
deal.advance_stagedeal.createdeal.closedeal.fill_fieldtask.createtask.completeemail.sendtag.applytag.removecomment.create -
statusstringA proposal leaves
pendingexactly once.supersededmeans the same agent later proposed a different change to the same record, which replaced this one.pendingacceptedrejectedexpiredsupersededauto_applied -
record_typestring · nullableThe type of record the change applies to. Null when the proposal would create a record, as
deal.createandtask.createdo.dealtaskdraftcontactcalendar_eventorganizationfilethreadspace -
record_idstring · uuid · nullableThe record the change applies to.
-
payloadobjectWhat to do. Which fields appear depends on
action_type;email.sendhas an empty payload.Show child attributesHide child attributes
to_stage_idstring · uuiddeal.advance_stage: the stage to move the deal to, in the deal’s own pipeline.name,pipeline_id,pipeline_stage_id,owner_user_iddeal.create: the new deal’s name, where it starts, and who owns it. Also takes exactly one ofcontact_idororganization_idas the counterparty, and optionallydescription,amount,currency, andexpected_close_date.outcomestringdeal.close:wonorlost, with an optionallost_reasonon a loss.field,valuedeal.fill_field:amountorexpected_close_date, and the value to write. Only fills a field that is empty.assignmentstringtask.create: what the task asks for. May also carrydescription,due,assigned_user_ids, and the records it links to:contact_id,organization_id,deal_id,thread_id, orcalendar_event_id.tag_id,tag_nametag.apply: an existing tag’s id, or a name to find or create.tag.removetakestag_id.bodystringcomment.create: the comment’s text.
-
evidenceobjectWhat prompted a background agent’s proposal: an email thread or a meeting transcript. Empty for proposals from the chat assistant; see
source.Show 8 child attributesHide child attributes
thread_idstring · uuidFrom an email: the thread the agent read.
calendar_event_idstring · uuidFrom a meeting: the calendar event whose transcript the agent read.
entry_idstring · uuid · nullableFrom a meeting: the transcript entry the excerpt is quoted from. Null when the excerpt spans more than one entry.
excerptstringThe passage the proposal is based on, quoted verbatim.
justificationstringThe agent’s one-line reason for the change.
fromstring · nullableFrom an email: display name, or email address, of the sender of the thread’s latest message when the agent read it. From a meeting: the speaker of the quoted entry. Null when the speaker isn’t known.
atstring · date-time · nullableWhen that message was sent, or when the quoted entry was spoken. Null when the entry has no time.
meeting_started_atstring · date-time · nullableFrom a meeting: when the transcript starts. The difference between
atand this is the excerpt’s offset into the recording. Null when the transcript has no times.
-
sourceobjectThe
agentagain, and for proposals from the chat assistant, theconversation_idandmessage_idof the message that made it. Both ids are null otherwise. -
actionsarray of stringsWhat you can do with the proposal now:
acceptandrejectwhile it is pending and unexpired,undoafter it has been applied, and nothing otherwise. Show exactly these controls.acceptrejectundo -
displayobject · nullableNames for the records the proposal involves, resolved for the approver, such as
deal_name,pipeline_name,to_stage_name, orassignmentanddue_on. Keys depend onaction_type. Every proposal also hasthread_subject, the subject of its evidence thread, which is null when there is no evidence thread. Tag and comment proposals also haverecord_typeandrecord_namefor the record they apply to.task.createproposals listassigned_users, each anidandname, andlinked_records, each atype,id, andname. A name the approver can no longer see is null, and a user or linked record they can’t see is left out. -
rejection_reasonstring · nullableThe reason given when the proposal was rejected, if any.
wrong_dealtoo_earlynot_a_dealother -
resolution_reasonstring · nullableSet when the proposal expired early because its record changed or was deleted. The value is human-readable text, not meant for parsing. Null otherwise, including for a proposal that ran out of time.
-
resolved_by_user_idstring · uuid · nullableWho accepted or rejected it. Null for
auto_appliedproposals, which nobody answered. -
applied_record_typestring · nullableThe type of record that applying the proposal changed or created, such as
taskortagging. -
applied_record_idstring · uuid · nullableThat record’s id. For a proposal that created a record, this is the only link to it.
-
undone_by_user_idstring · uuid · nullableWho undid the change.
-
undone_atstring · date-time · nullableWhen it was undone. Undoing leaves
statusas it was. -
expires_atstring · date-timeWhen a pending proposal stops being answerable.
-
resolved_atstring · date-time · nullableWhen the proposal left
pending. Forauto_appliedproposals, when the change was applied. -
created_atstring · date-time
{
"id": "c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61",
"agent": "auto_deal",
"action_type": "deal.advance_stage",
"status": "pending",
"record_type": "deal",
"record_id": "8f2b6d4a-1c9e-4a7b-b3d5-0e6f8a2c4d17",
"rejection_reason": null,
"resolution_reason": null,
"resolved_by_user_id": null,
"applied_record_type": null,
"applied_record_id": null,
"undone_by_user_id": null,
"expires_at": "2026-09-24T14:05:33.575Z",
"resolved_at": null,
"undone_at": null,
"created_at": "2026-09-17T14:05:33.504Z",
"payload": {
"to_stage_id": "4f6a8c0e-2b4d-4c6e-8f0a-3b5d7e9f1c28"
},
"evidence": {
"thread_id": "7a9c1e3f-5b7d-4f9a-a1c3-8e0b2d4f6a55",
"excerpt": "We're ready to talk terms. Can you send the 2027 numbers by Friday?",
"justification": "The customer asked for renewal pricing, which starts negotiation.",
"from": "Priya Natarajan",
"at": "2026-09-17T13:58:20Z"
},
"source": {
"agent": "auto_deal",
"conversation_id": null,
"message_id": null
},
"actions": ["accept", "reject"],
"display": {
"deal_name": "Wildgrove portfolio renewal",
"pipeline_name": "Renewals",
"from_stage_name": "Proposal sent",
"to_stage_name": "Negotiation",
"thread_subject": "Re: 2027 renewal terms"
}
}
#List proposals
Returns the proposals for which your key’s user is the approver, newest first. By default that is the pending ones still open to an answer, which makes this the user’s approval inbox. Ask for status=resolved for a history of what was decided or applied.
Once decided, the chat assistant’s proposals are left out unless you pass agent=assistant or a conversation_id. Its pending proposals are listed like any other agent’s.
This list is paged by page number, 50 per page by default, rather than by cursor. See Page-number pagination.
Query parameters
-
statusstringdefaultpendingOne status or a comma-separated list.
pendingomits proposals past their expiry time.resolvedstands foraccepted,rejected, andauto_applied. Expired and superseded proposals appear only when you name those statuses.pendingacceptedrejectedexpiredsupersededauto_appliedresolved -
sortstringSort newest first by when the proposal was made or when it was resolved. Defaults to
created_atwhenstatusincludespending, andresolved_atotherwise.created_atresolved_at -
agentstringOnly proposals from this agent.
assistantauto_dealauto_meetingauto_task -
exclude_agentstringLeave out this agent’s proposals. Pass
assistantto build a review queue without the chat assistant’s pending proposals, which the user answers in chat.assistantauto_dealauto_meetingauto_task -
record_type,record_idOnly proposals about this record. Give both or neither.
record_typetakes the snake-case values listed on the proposal object; a capitalized value such asDealreturns400 invalid_request. -
pipeline_idstring · uuidOnly proposals about deals in this pipeline, including proposals to create a deal in it.
-
conversation_idstring · uuidOnly proposals the assistant made in this conversation.
-
pageintegerdefault1 -
per_pageintegerdefault50Values above 100 are treated as 100.
Returns
-
collectionobjectA page of proposals.
Show 7 child attributesHide child attributes
-
recordsarray of proposals pageintegerper_pageintegertotal_resultsintegerProposals matching the filters, across all pages.
pagesintegersortstringThe sort that was applied.
orderstringAlways
desc.
-
Errors
- 400
invalid_requestA filter has a value outside its list, only one ofrecord_typeandrecord_idis given, or an id isn’t a UUID.
curl "https://api.carom.io/proposals?exclude_agent=assistant&per_page=20" \
-H "Authorization: Bearer $CAROM_API_KEY"
{
"collection": {
"sort": "created_at",
"order": "desc",
"per_page": 20,
"page": 1,
"total_results": 2,
"pages": 1,
"records": [
{
"id": "2a4c6e8a-0c2e-4a4c-9e6a-8c0e2a4c6e35",
"agent": "auto_task",
"action_type": "task.create",
"status": "pending",
"record_type": null,
"record_id": null,
…
"display": {
"assignment": "Send Priya the 2027 renewal numbers",
"description": "Priya asked for the 2027 pricing before Friday.",
"due_on": "2026-09-18",
"calendar_event_name": null,
"thread_subject": "Re: 2027 renewal terms",
"assigned_users": [
{ "id": "9c41d2e8-3f6a-4b7c-8d1e-5a2f7b9c0d3e", "name": "Dana Whitfield" }
],
"linked_records": [
{ "type": "thread", "id": "7a9c1e3f-5b7d-4f9a-a1c3-8e0b2d4f6a55", "name": "Re: 2027 renewal terms" }
]
}
},
{
"id": "c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61",
"agent": "auto_deal",
"action_type": "deal.advance_stage",
"status": "pending",
…
}
]
}
}
#Retrieve a proposal
Returns one proposal, in any status. Use it to follow a proposal_id from a timeline item, a deal’s stage history, or a chat message.
Path parameters
-
idstring · uuidrequiredThe proposal’s id.
Returns
The proposal object, wrapped in proposal.
Errors
- 404
record_not_foundNo proposal with that id has your key’s user as its approver.
curl https://api.carom.io/proposals/c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61 \
-H "Authorization: Bearer $CAROM_API_KEY"
{
"proposal": {
"id": "c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61",
"agent": "auto_deal",
"action_type": "deal.advance_stage",
"status": "pending",
"record_type": "deal",
"record_id": "8f2b6d4a-1c9e-4a7b-b3d5-0e6f8a2c4d17",
…
"actions": ["accept", "reject"]
}
}
#Count unseen proposals
Counts the proposals Carom’s background agents applied automatically for your key’s user since they last marked proposals seen. Proposals the user accepted or rejected are never counted, since they already know about them, and neither are changes the chat assistant made, since the user asked for them. The marker is stored on the server, so every client sees the same count.
Returns
-
unseen_countinteger -
last_viewed_atstring · date-time · nullableWhen proposals were last marked as seen. Null if they never have been, in which case every automatically applied proposal counts.
curl https://api.carom.io/proposals/unseen_count \
-H "Authorization: Bearer $CAROM_API_KEY"
{
"unseen_count": 3,
"last_viewed_at": "2026-09-16T21:04:55.336Z"
}
#Accept a proposal
Applies the change as your key’s user, the same way an edit in Carom would, and marks the proposal accepted. Before applying, Carom checks the record against what the agent saw. The check fails, for example, when the record was deleted or is no longer visible to your key’s user, a deal’s status or stage has changed, the deal field being filled has changed, a task has been completed or reopened, or a draft has already been sent. Then nothing is applied, the proposal expires, and you get proposal_stale.
You can correct some details before accepting, such as a task’s wording or assignees, or a deal’s name, by sending only the keys you are changing. email.send proposals can’t be accepted with an API key; the request fails with api_key_insufficient.
Path parameters
-
idstring · uuidrequired
Request body application/json
-
displayobjectCorrections, keyed by the
displayname they change. Only the keys listed for the proposal’s action type are accepted.Show 6 child attributesHide child attributes
deal_namestringdeal.create: the new deal’s name.lost_reasonstringdeal.close: why the deal was lost.assignmentstringtask.create: what the task asks for.due_onstring · datetask.create: when the task is due.tag_namestringtag.apply: the tag to apply, found or created by name.bodystringcomment.create: the comment’s text.
-
payloadobjectCorrections to
payloadfields. Allowed only for these action types.Show 3 child attributesHide child attributes
valuedeal.fill_field: the value to write.descriptionstringtask.create: the task’s description.assigned_user_idsarray of stringstask.create: who the task is assigned to. A non-empty array of ids of active users in the account.
Returns
The proposal object, wrapped in proposal, with status accepted. applied_record_type and applied_record_id name the record that was changed or created.
Errors
- 400
invalid_requestA correction isn’t allowed for this action type or isn’t valid.error.fieldsnames it by its full path, such asdisplay.due_on. - 403
api_key_insufficientThe proposal isemail.send, which API keys can’t accept. - 404
record_not_foundNo proposal with that id has your key’s user as its approver. - 409
proposal_already_resolvedThe proposal isn’t pending; someone already answered it. - 409
proposal_expiredThe proposal passed itsexpires_atbefore anyone answered. - 409
proposal_staleThe record changed or disappeared since the proposal was made. The proposal is now expired.
curl https://api.carom.io/proposals/2a4c6e8a-0c2e-4a4c-9e6a-8c0e2a4c6e35/accept \
-H "Authorization: Bearer $CAROM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display": {
"assignment": "Send Priya the 2027 renewal numbers and terms",
"due_on": "2026-09-21"
}
}'
{
"proposal": {
"id": "2a4c6e8a-0c2e-4a4c-9e6a-8c0e2a4c6e35",
"agent": "auto_task",
"action_type": "task.create",
"status": "accepted",
"resolved_by_user_id": "9c41d2e8-3f6a-4b7c-8d1e-5a2f7b9c0d3e",
"applied_record_type": "task",
"applied_record_id": "4e6a8c0e-2a4c-4e6a-8c0e-1a3c5e7a9c62",
"resolved_at": "2026-09-18T16:20:47.542Z",
"payload": {
"assignment": "Send Priya the 2027 renewal numbers and terms",
"due": "2026-09-21",
"thread_id": "7a9c1e3f-5b7d-4f9a-a1c3-8e0b2d4f6a55"
},
…
"actions": ["undo"]
}
}
#Reject a proposal
Declines the change and marks the proposal rejected. No record is modified. A reason is optional and is kept as feedback on the agent.
Path parameters
-
idstring · uuidrequired
Request body application/json
-
reasonstringWhy the proposal was wrong.
wrong_deal,too_early, andnot_a_dealapply to deal proposals; useotherfor the rest.wrong_dealtoo_earlynot_a_dealother
Returns
The proposal object, wrapped in proposal, with status rejected.
Errors
- 400
invalid_requestreasonisn’t one of the listed values. - 404
record_not_foundNo proposal with that id has your key’s user as its approver. - 409
proposal_already_resolvedThe proposal isn’t pending; someone already answered it. - 409
proposal_expiredThe proposal passed itsexpires_atbefore anyone answered.
curl https://api.carom.io/proposals/6f8a0c2e-4a6c-4e8a-8c0e-9a2c4e6a8c07/reject \
-H "Authorization: Bearer $CAROM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "not_a_deal" }'
{
"proposal": {
"id": "6f8a0c2e-4a6c-4e8a-8c0e-9a2c4e6a8c07",
"agent": "auto_deal",
"action_type": "deal.create",
"status": "rejected",
"rejection_reason": "not_a_deal",
"resolved_by_user_id": "9c41d2e8-3f6a-4b7c-8d1e-5a2f7b9c0d3e",
"resolved_at": "2026-09-18T16:22:03.910Z",
…
"actions": []
}
}
#Undo a proposal
Reverses an accepted or auto_applied proposal by making the opposite edit as your key’s user. For example, a deal moves back to the stage it came from, and a created deal is deleted. The record’s history shows both changes. The proposal keeps its status and gains undone_at and undone_by_user_id.
Undo works only while the record is still as the proposal left it. If someone has changed it since, Carom refuses with proposal_stale and leaves the proposal as it was. email.send proposals can’t be undone. There is no redo.
Path parameters
-
idstring · uuidrequired
Returns
The proposal object, wrapped in proposal.
Errors
- 404
record_not_foundNo proposal with that id has your key’s user as its approver. - 409
proposal_not_appliedThe proposal is pending, rejected, expired, or superseded, so there is nothing to undo. - 409
proposal_already_undoneThe change was already undone. - 409
proposal_not_undoableThis action type can’t be undone. - 409
proposal_staleThe record has changed since the proposal was applied.
curl -X POST https://api.carom.io/proposals/c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61/undo \
-H "Authorization: Bearer $CAROM_API_KEY"
{
"proposal": {
"id": "c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61",
"agent": "auto_deal",
"action_type": "deal.advance_stage",
"status": "accepted",
"undone_by_user_id": "9c41d2e8-3f6a-4b7c-8d1e-5a2f7b9c0d3e",
"resolved_at": "2026-09-17T15:42:10.038Z",
"undone_at": "2026-09-18T09:11:36.917Z",
…
"actions": []
}
}
#Mark proposals seen
Records that your key’s user has seen every proposal up to now, which resets the unseen count to zero. Call it when the user opens their list of applied changes. Takes no body.
Returns
An empty response with status 204.
curl -X POST https://api.carom.io/proposals/seen \
-H "Authorization: Bearer $CAROM_API_KEY"
No content