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 · uuid

    Unique identifier for the proposal.

  • agentstring

    What proposed the change: assistant for Carom’s chat assistant, or the background agent that read the email or meeting, such as auto_deal or auto_task.

  • action_typestring

    The kind of change.

    deal.advance_stagedeal.createdeal.closedeal.fill_fieldtask.createtask.completeemail.sendtag.applytag.removecomment.create
  • statusstring

    A proposal leaves pending exactly once. superseded means the same agent later proposed a different change to the same record, which replaced this one.

    pendingacceptedrejectedexpiredsupersededauto_applied
  • record_typestring · nullable

    The type of record the change applies to. Null when the proposal would create a record, as deal.create and task.create do.

    dealtaskdraftcontactcalendar_eventorganizationfilethreadspace
  • record_idstring · uuid · nullable

    The record the change applies to.

  • payloadobject

    What to do. Which fields appear depends on action_type; email.send has an empty payload.

    Show child attributesHide child attributes
    • to_stage_idstring · uuid

      deal.advance_stage: the stage to move the deal to, in the deal’s own pipeline.

    • name, pipeline_id, pipeline_stage_id, owner_user_id

      deal.create: the new deal’s name, where it starts, and who owns it. Also takes exactly one of contact_id or organization_id as the counterparty, and optionally description, amount, currency, and expected_close_date.

    • outcomestring

      deal.close: won or lost, with an optional lost_reason on a loss.

    • field, value

      deal.fill_field: amount or expected_close_date, and the value to write. Only fills a field that is empty.

    • assignmentstring

      task.create: what the task asks for. May also carry description, due, assigned_user_ids, and the records it links to: contact_id, organization_id, deal_id, thread_id, or calendar_event_id.

    • tag_id, tag_name

      tag.apply: an existing tag’s id, or a name to find or create. tag.remove takes tag_id.

    • bodystring

      comment.create: the comment’s text.

  • evidenceobject

    What 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 · uuid

      From an email: the thread the agent read.

    • calendar_event_idstring · uuid

      From a meeting: the calendar event whose transcript the agent read.

    • entry_idstring · uuid · nullable

      From a meeting: the transcript entry the excerpt is quoted from. Null when the excerpt spans more than one entry.

    • excerptstring

      The passage the proposal is based on, quoted verbatim.

    • justificationstring

      The agent’s one-line reason for the change.

    • fromstring · nullable

      From 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 · nullable

      When that message was sent, or when the quoted entry was spoken. Null when the entry has no time.

    • meeting_started_atstring · date-time · nullable

      From a meeting: when the transcript starts. The difference between at and this is the excerpt’s offset into the recording. Null when the transcript has no times.

  • sourceobject

    The agent again, and for proposals from the chat assistant, the conversation_id and message_id of the message that made it. Both ids are null otherwise.

  • actionsarray of strings

    What you can do with the proposal now: accept and reject while it is pending and unexpired, undo after it has been applied, and nothing otherwise. Show exactly these controls.

    acceptrejectundo
  • displayobject · nullable

    Names for the records the proposal involves, resolved for the approver, such as deal_name, pipeline_name, to_stage_name, or assignment and due_on. Keys depend on action_type. Every proposal also has thread_subject, the subject of its evidence thread, which is null when there is no evidence thread. Tag and comment proposals also have record_type and record_name for the record they apply to. task.create proposals list assigned_users, each an id and name, and linked_records, each a type, id, and name. 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 · nullable

    The reason given when the proposal was rejected, if any.

    wrong_dealtoo_earlynot_a_dealother
  • resolution_reasonstring · nullable

    Set 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 · nullable

    Who accepted or rejected it. Null for auto_applied proposals, which nobody answered.

  • applied_record_typestring · nullable

    The type of record that applying the proposal changed or created, such as task or tagging.

  • applied_record_idstring · uuid · nullable

    That record’s id. For a proposal that created a record, this is the only link to it.

  • undone_by_user_idstring · uuid · nullable

    Who undid the change.

  • undone_atstring · date-time · nullable

    When it was undone. Undoing leaves status as it was.

  • expires_atstring · date-time

    When a pending proposal stops being answerable.

  • resolved_atstring · date-time · nullable

    When the proposal left pending. For auto_applied proposals, when the change was applied.

  • created_atstring · date-time
The proposal object
{
  "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"
  }
}
get/proposals Read key

#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

  • statusstringdefault pending

    One status or a comma-separated list. pending omits proposals past their expiry time. resolved stands for accepted, rejected, and auto_applied. Expired and superseded proposals appear only when you name those statuses.

    pendingacceptedrejectedexpiredsupersededauto_appliedresolved
  • sortstring

    Sort newest first by when the proposal was made or when it was resolved. Defaults to created_at when status includes pending, and resolved_at otherwise.

    created_atresolved_at
  • agentstring

    Only proposals from this agent.

    assistantauto_dealauto_meetingauto_task
  • exclude_agentstring

    Leave out this agent’s proposals. Pass assistant to build a review queue without the chat assistant’s pending proposals, which the user answers in chat.

    assistantauto_dealauto_meetingauto_task
  • record_type, record_id

    Only proposals about this record. Give both or neither. record_type takes the snake-case values listed on the proposal object; a capitalized value such as Deal returns 400 invalid_request.

  • pipeline_idstring · uuid

    Only proposals about deals in this pipeline, including proposals to create a deal in it.

  • conversation_idstring · uuid

    Only proposals the assistant made in this conversation.

  • pageintegerdefault 1
  • per_pageintegerdefault 50

    Values above 100 are treated as 100.

Returns

  • collectionobject

    A page of proposals.

    Show 7 child attributesHide child attributes
    • recordsarray of proposals
    • pageinteger
    • per_pageinteger
    • total_resultsinteger

      Proposals matching the filters, across all pages.

    • pagesinteger
    • sortstring

      The sort that was applied.

    • orderstring

      Always desc.

Errors

  • 400invalid_requestA filter has a value outside its list, only one of record_type and record_id is given, or an id isn’t a UUID.
get/proposals
curl "https://api.carom.io/proposals?exclude_agent=assistant&per_page=20" \
  -H "Authorization: Bearer $CAROM_API_KEY"
Response200
{
  "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",
        …
      }
    ]
  }
}
get/proposals/{id} Read key

#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 · uuidrequired

    The proposal’s id.

Returns

The proposal object, wrapped in proposal.

Errors

  • 404record_not_foundNo proposal with that id has your key’s user as its approver.
get/proposals/{id}
curl https://api.carom.io/proposals/c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61 \
  -H "Authorization: Bearer $CAROM_API_KEY"
Response200
{
  "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"]
  }
}
get/proposals/unseen_count Read key

#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 · nullable

    When proposals were last marked as seen. Null if they never have been, in which case every automatically applied proposal counts.

get/proposals/unseen_count
curl https://api.carom.io/proposals/unseen_count \
  -H "Authorization: Bearer $CAROM_API_KEY"
Response200
{
  "unseen_count": 3,
  "last_viewed_at": "2026-09-16T21:04:55.336Z"
}
post/proposals/{id}/accept Write key

#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

  • displayobject

    Corrections, keyed by the display name they change. Only the keys listed for the proposal’s action type are accepted.

    Show 6 child attributesHide child attributes
    • deal_namestring

      deal.create: the new deal’s name.

    • lost_reasonstring

      deal.close: why the deal was lost.

    • assignmentstring

      task.create: what the task asks for.

    • due_onstring · date

      task.create: when the task is due.

    • tag_namestring

      tag.apply: the tag to apply, found or created by name.

    • bodystring

      comment.create: the comment’s text.

  • payloadobject

    Corrections to payload fields. Allowed only for these action types.

    Show 3 child attributesHide child attributes
    • value

      deal.fill_field: the value to write.

    • descriptionstring

      task.create: the task’s description.

    • assigned_user_idsarray of strings

      task.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

  • 400invalid_requestA correction isn’t allowed for this action type or isn’t valid. error.fields names it by its full path, such as display.due_on.
  • 403api_key_insufficientThe proposal is email.send, which API keys can’t accept.
  • 404record_not_foundNo proposal with that id has your key’s user as its approver.
  • 409proposal_already_resolvedThe proposal isn’t pending; someone already answered it.
  • 409proposal_expiredThe proposal passed its expires_at before anyone answered.
  • 409proposal_staleThe record changed or disappeared since the proposal was made. The proposal is now expired.
post/proposals/{id}/accept
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"
    }
  }'
Response200
{
  "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"]
  }
}
post/proposals/{id}/reject Write key

#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

  • reasonstring

    Why the proposal was wrong. wrong_deal, too_early, and not_a_deal apply to deal proposals; use other for the rest.

    wrong_dealtoo_earlynot_a_dealother

Returns

The proposal object, wrapped in proposal, with status rejected.

Errors

  • 400invalid_requestreason isn’t one of the listed values.
  • 404record_not_foundNo proposal with that id has your key’s user as its approver.
  • 409proposal_already_resolvedThe proposal isn’t pending; someone already answered it.
  • 409proposal_expiredThe proposal passed its expires_at before anyone answered.
post/proposals/{id}/reject
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" }'
Response200
{
  "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": []
  }
}
post/proposals/{id}/undo Write key

#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

  • 404record_not_foundNo proposal with that id has your key’s user as its approver.
  • 409proposal_not_appliedThe proposal is pending, rejected, expired, or superseded, so there is nothing to undo.
  • 409proposal_already_undoneThe change was already undone.
  • 409proposal_not_undoableThis action type can’t be undone.
  • 409proposal_staleThe record has changed since the proposal was applied.
post/proposals/{id}/undo
curl -X POST https://api.carom.io/proposals/c3e5a7b9-1d3f-4a5b-9c7e-2f4a6b8d0e61/undo \
  -H "Authorization: Bearer $CAROM_API_KEY"
Response200
{
  "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": []
  }
}
post/proposals/seen Write key

#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.

post/proposals/seen
curl -X POST https://api.carom.io/proposals/seen \
  -H "Authorization: Bearer $CAROM_API_KEY"
Response204
No content