Skip to content
Pathbound DOCS

Deals

A deal is a revenue opportunity. It has a name, a stage, an amount, a currency, a close date and a status (open, won or lost), and it can be linked to a company, a contact, or both.

Deals exist only in Pathbound. No integration brings them in from a CRM: a deal is created through this API, by an AI agent through the MCP server, or by a person in the dashboard. To mirror deals from another system, create them here and put that system’s id in external_deal_id. A deal cannot be a member of a segment, and a change to a deal is not delivered to a stream.

The MCP tools list_deals, get_deal, create_deal, create_deals_bulk, update_deal, delete_deal, list_deal_notes, create_deal_note, update_deal_note, delete_deal_note, add_deal_tags and remove_deal_tags call the endpoints on this page. See the tools reference.

EndpointScopeWhat it does
POST /v1/dealsdeals:writeCreates a deal, or updates the one that has your external_deal_id.
POST /v1/deals/bulkdeals:writeCreates or updates up to 1,000 deals in one request.
GET /v1/dealsdeals:readLists and filters deals.
GET /v1/deals/:deal_iddeals:readReturns one deal with its company and contact.
PUT /v1/deals/:deal_iddeals:writeChanges a deal’s properties or links.
DELETE /v1/deals/:deal_iddeals:writeDeletes a deal.
GET /v1/deals/:deal_id/notesdeals:readLists a deal’s notes.
POST /v1/deals/:deal_id/notesdeals:writeAdds a note to a deal.
PATCH /v1/deals/:deal_id/notes/:note_iddeals:writeChanges a note’s text.
DELETE /v1/deals/:deal_id/notes/:note_iddeals:writeDeletes a note.
POST /v1/deals/:deal_id/tagsdeals:writeAdds tags to one deal. See Tags.
DELETE /v1/deals/:deal_id/tagsdeals:writeRemoves tags from one deal. See Tags.
POST /v1/deals/tags/bulkdeals:writeAdds and removes tags on up to 1,000 deals. See Tags.

Neither scope is opt-in. See Scopes.

Deals can also be read through POST /v1/batch/details (up to 25 by id) and summed or counted through POST /v1/aggregate. To total a pipeline by stage, use the aggregate endpoint instead of paging through this list.

Reading a deal counts as reading the customer it belongs to: the deal’s company or, failing that, its contact. A deal linked to neither counts as a customer of its own. GET /v1/deals/:deal_id always counts. GET /v1/deals counts for every deal on the page, unless you ask for fields=ids or count_only=true. A customer you have already read in the billing cycle is not counted again. The notes endpoints, the tag endpoints and every write are free. The deal endpoints have no dry_run: to learn what a read would cost, use dry_run on POST /v1/batch/details. See Usage and metering.

{
"deal_id": "6acd7a3e9b1c4d2e8f012345",
"_id": "6acd7a3e9b1c4d2e8f012345",
"properties": {
"name": "Northwind annual plan",
"stage": "negotiation",
"amount": 24000,
"currency": "USD",
"status": "open",
"close_date": "2026-10-30T00:00:00.000Z",
"createdate": "2026-10-13T09:14:02.118Z",
"lastmodifieddate": "2026-10-13T09:14:02.118Z"
},
"company_id": "665f1c2ab3d4e5f601234567",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"external_deal_id": "crm-48213",
"notes": [],
"tags": ["renewal", "q4"],
"creation_source": "api",
"first_seen_at": "2026-10-13T09:14:02.118Z",
"created_at": "2026-10-13T09:14:02.118Z",
"updated_at": "2026-10-13T09:14:02.118Z"
}
FieldDescription
deal_idThe deal’s id. _id repeats it; use deal_id.
properties.nameThe deal’s name.
properties.stageThe pipeline stage. Stages are your own names: Pathbound keeps no list of them.
properties.amountThe deal’s value, a number from 0 to 1e15.
properties.currencyA three-letter code, uppercase.
properties.statusopen, won or lost.
properties.close_dateThe expected or actual close date.
properties.createdate, properties.lastmodifieddateSet by Pathbound when the deal is created and when its properties change.
properties.*Any other key you stored.
company_id, contact_idThe linked company and contact. Absent when the deal is not linked.
external_deal_idYour own id for the deal, when you gave one.
notesEvery note on the deal. See List Deal Notes for a note’s fields.
tagsThe deal’s tags. Always an array.
creation_sourceapi for a deal created through this API or the MCP server, manual for one created in the dashboard.
first_seen_at, created_atWhen the deal was created.
updated_atWhen the deal last changed.
POST /v1/deals

Required scope: deals:write

Creates one deal. No field is required: an empty body creates a deal with status: "open", currency: "USD" and nothing else.

With an external_deal_id the call is an upsert on it: when a deal in your workspace already has that id, the call updates that deal and is answered 200 with "created": false. The same request sent twice therefore leaves one deal, unchanged by the second call. On an update:

  • a property you send replaces the stored value, and one you leave out, or send as null, keeps it;
  • status and currency are not reset to their defaults;
  • a company_id, contact_id, company or contact you send replaces the link, and one you leave out keeps it. A company inherited from the contact only fills a deal that has none;
  • tags are added to the deal’s own.
FieldTypeRequiredDescription
propertiesobjectnoThe deal’s fields. See the table below.
company_idstringnoThe company to link. It must exist in your workspace.
companyobjectnoThe company to link, by your own key instead of its Pathbound id: { "external_id": "…" } (the id you gave the account on Create a Company) or { "domain": "…" }. Not with company_id.
contact_idstringnoThe contact to link. It must exist in your workspace. When you send a contact and no company, the deal is also linked to that contact’s company, if the contact has one.
contactobjectnoThe contact to link, by your own key: { "external_id": "…" } (the id you gave the person on Create a Contact or identify) or { "email": "…" }. Not with contact_id.
external_deal_idstringnoYour own id for the deal, up to 255 characters, unique in the workspace. It cannot be changed afterwards. A deal that already has it is updated.
tagsstring[]noUp to 50 tags to put on the deal.

Fields of properties:

FieldTypeDescription
namestringThe deal’s name.
stagestringThe pipeline stage, in your own words (discovery, negotiation). Use the same spelling each time.
amountnumberFrom 0 to 1e15. A numeric string is converted to a number.
currencystringThree letters, stored uppercase. Defaults to USD. It is not checked against a list of currencies.
statusstringopen (the default), won or lost. Stored lowercase.
close_datestringAn ISO 8601 date. It may be in the future.
any other keyStored as a custom property. A string value can be at most 1,000 characters.

Pathbound sets createdate and lastmodifieddate itself, whatever you send for them.

A company or contact key only looks a record up: one that finds nothing is a 422 and no deal is created or changed, never a new company or contact. Write the account and the person first, then their deal.

Terminal window
curl -X POST https://api.pathbound.ai/v1/deals \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"name": "Northwind annual plan",
"stage": "negotiation",
"amount": 24000,
"close_date": "2026-10-30"
},
"company_id": "665f1c2ab3d4e5f601234567",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"external_deal_id": "crm-48213",
"tags": ["renewal", "q4"]
}'

A deal created. A deal updated through its external_deal_id is answered 200 OK in the same shape, with "created": false and "message": "Deal updated successfully".

{
"status": "success",
"message": "Deal created successfully",
"created": true,
"deal": {
"deal_id": "6acd7a3e9b1c4d2e8f012345",
"_id": "6acd7a3e9b1c4d2e8f012345",
"properties": {
"name": "Northwind annual plan",
"stage": "negotiation",
"amount": 24000,
"close_date": "2026-10-30T00:00:00.000Z",
"status": "open",
"currency": "USD",
"createdate": "2026-10-13T09:14:02.118Z",
"lastmodifieddate": "2026-10-13T09:14:02.118Z"
},
"company_id": "665f1c2ab3d4e5f601234567",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"external_deal_id": "crm-48213",
"notes": [],
"tags": ["renewal", "q4"],
"creation_source": "api",
"first_seen_at": "2026-10-13T09:14:02.118Z",
"created_at": "2026-10-13T09:14:02.118Z",
"updated_at": "2026-10-13T09:14:02.118Z"
},
"timestamp": "2026-10-13T09:14:02.201Z"
}
StatusMeaning
400A field has the wrong type or a property was refused. See Errors. code: "INVALID_LINK" when company or contact is not an object with exactly one of its two keys, or is sent beside company_id or contact_id.
404company_id or contact_id names no record in your workspace. The error text is Company not found or Contact not found.
422code: "CONTACT_NOT_FOUND" or "COMPANY_NOT_FOUND": the contact or company key finds no record.
429The workspace has reached its limit on stored deals (code: "QUOTA_EXCEEDED"). An update of an existing deal is never refused for it.
POST /v1/deals/bulk

Required scope: deals:write

Creates up to 1,000 deals in one request. Each item is handled on its own: a bad item is reported as that item’s error and the others are still created.

The same rules apply as for a single create. Sending the same request twice creates every deal twice, except the items that carry an external_deal_id: an item whose id a deal already has updates that deal, as Create a Deal does, and comes back as updated.

FieldTypeRequiredDescription
dealsobject[]yes1 to 1,000 items. Each takes the fields of Create a Deal: properties, company_id, company, contact_id, contact, external_deal_id, tags.

An item’s tags are put on the deal it creates or updates.

{
"status": "success",
"requested": 3,
"created": 1,
"updated": 1,
"errors": 1,
"results": [
{
"index": 0,
"external_deal_id": "crm-48213",
"name": "Northwind annual plan",
"status": "updated",
"id": "6acd7a3e9b1c4d2e8f012345"
},
{
"index": 1,
"external_deal_id": "crm-48214",
"name": "Northwind add-on seats",
"status": "created",
"id": "6acd7b109b1c4d2e8f012377"
},
{
"index": 2,
"name": "Contoso pilot",
"status": "error",
"error": "Invalid amount. Must be a number >= 0."
}
],
"timestamp": "2026-10-13T09:15:40.512Z"
}
FieldDescription
requestedThe number of items you sent. created, updated and errors add up to it.
results[].indexThe item’s position in deals, from 0.
results[].external_deal_id, results[].nameEchoed from the item when it had them.
results[].statuscreated, updated or error.
results[].idThe deal’s id, for created and updated.
results[].errorWhy the item was refused, for error.
results[].codeFor an item refused over its company or contact: INVALID_LINK, CONTACT_NOT_FOUND or COMPANY_NOT_FOUND, as on Create a Deal.

The request is answered with 200 even when every item failed, so read errors and results. A 400 means deals itself is missing, empty or longer than 1,000. A 429 with code: "QUOTA_EXCEEDED" means the new deals in the batch would take the workspace past its limit on stored deals, and nothing was created; an item that updates a deal does not count toward it.

GET /v1/deals

Required scope: deals:read

Returns deals, most recently changed first, one page at a time. All filters combine with AND.

ParameterTypeDefaultDescription
pagenumber1Page number. See Pagination.
limitnumber25Deals per page (1 to 100).
fieldsstringThe shape of each row: ids, summary, or omit it for the full deal. The table under the response describes each. A comma-separated list of field names is not a projection here, as it is for contacts.
count_onlybooleanfalseIf true, return only { "status": "success", "count": 62 }. Not metered.
statusstringopen, won or lost.
stagestringThe stage, whole value, case-insensitive.
company_idstringOnly deals linked to this company.
contact_idstringOnly deals linked to this contact.
external_deal_idstringThe deal with exactly this id of yours.
close_date_afterstringOnly deals closing on or after this ISO 8601 date.
close_date_beforestringOnly deals closing on or before this ISO 8601 date.
min_amountnumberOnly deals worth at least this much.
max_amountnumberOnly deals worth at most this much.
tags_anystringOnly deals with at least one of these tags, comma-separated. See Filter Lists by Tag.
tags_allstringOnly deals with every one of these tags. When both are sent, tags_any is ignored.
searchstringText to find in the name, the stage or external_deal_id, case-insensitive. At most 100 characters.
filtersstringTyped filters as a JSON array [{"field","op","value"}], at most 10. field is name, stage, status, currency, amount, close_date, createdate, lastmodifieddate or the key of a custom property. op is one of eq, neq, in, nin, exists, contains, gt, gte, lt, lte. GET /v1/schema?entity=deals lists the typed fields and the operators each one takes. Use the parameters above for the company, the contact and tags.
sort_bystringlastmodifieddatename, stage, status, amount, close_date, createdate, lastmodifieddate, created_at or updated_at.
sort_dirstringdescasc or desc.
includestringprovenance adds properties_provenance to each full row: for each property, where its value came from (api or manual) and when.
Terminal window
curl "https://api.pathbound.ai/v1/deals?status=open&close_date_before=2026-12-31&sort_by=amount&fields=summary" \
-H "Authorization: Bearer $PATHBOUND_API_KEY"
{
"status": "success",
"deals": [
{
"deal_id": "6acd7a3e9b1c4d2e8f012345",
"id": "6acd7a3e9b1c4d2e8f012345",
"name": "Northwind annual plan",
"stage": "negotiation",
"status": "open",
"amount": 24000,
"currency": "USD",
"close_date": "2026-10-30T00:00:00.000Z",
"company_id": "665f1c2ab3d4e5f601234567",
"company_name": "Northwind Traders",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"contact_name": "Dana Lee",
"tags": ["renewal", "q4"]
}
],
"pagination": {
"page": 1,
"limit": 25,
"total_pages": 3,
"total_items": 62
},
"timestamp": "2026-10-13T09:16:12.730Z"
}

What a row holds depends on fields:

fieldsEach rowCounts against your plan
omitted, or fullThe deal object, plus company_name and contact_name.yes
summaryThe row shown above.yes
idsdeal_id, id, company_id and contact_id only.no

company_name is the linked company’s name, or its domain when it has no name. contact_name is the linked contact’s first and last name, or their email address when they have neither. Both are null when the deal has no such link. In the summary and ids shapes, company_id and contact_id are null for a deal that is not linked, and id repeats deal_id.

GET /v1/deals/:deal_id

Required scope: deals:read

Returns one deal with a short description of the company and the contact it is linked to. This read is metered.

ParameterTypeDescription
deal_idstringThe deal’s id (24 hex characters). Any other value is a 400.
ParameterTypeDefaultDescription
includestringprovenance adds properties_provenance to the deal.
{
"status": "success",
"deal": {
"deal_id": "6acd7a3e9b1c4d2e8f012345",
"_id": "6acd7a3e9b1c4d2e8f012345",
"properties": {
"name": "Northwind annual plan",
"stage": "negotiation",
"amount": 24000,
"currency": "USD",
"status": "open",
"close_date": "2026-10-30T00:00:00.000Z",
"createdate": "2026-10-13T09:14:02.118Z",
"lastmodifieddate": "2026-10-13T09:14:02.118Z"
},
"company_id": "665f1c2ab3d4e5f601234567",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"external_deal_id": "crm-48213",
"notes": [],
"tags": ["renewal", "q4"],
"creation_source": "api",
"first_seen_at": "2026-10-13T09:14:02.118Z",
"created_at": "2026-10-13T09:14:02.118Z",
"updated_at": "2026-10-13T09:14:02.118Z"
},
"company": {
"company_id": "665f1c2ab3d4e5f601234567",
"name": "Northwind Traders",
"domain": "example.com"
},
"contact": {
"contact_id": "665f1c2ab3d4e5f6012345ab",
"firstname": "Dana",
"lastname": "Lee",
"email": "[email protected]",
"jobtitle": "VP Operations"
},
"timestamp": "2026-10-13T09:17:03.044Z"
}

company and contact are null when the deal has no such link, or when the linked record no longer exists.

A deal that is not in your workspace is a 404 with code: "not_found".

PUT /v1/deals/:deal_id

Required scope: deals:write

Changes a deal’s properties, its links, or both. Only what you send changes, so an update that leaves out status does not reopen a won deal.

FieldTypeRequiredDescription
propertiesobjectnoProperties to set, with the same fields as in Create a Deal. They are merged into the stored ones. One rule differs from a create: only lastmodifieddate is set by Pathbound here, so a createdate you send is stored. Do not send it.
company_idstring | nullnoAn id links the deal to that company. null removes the link. Leave it out to keep the link.
contact_idstring | nullnoThe same, for the contact.

Things this endpoint does not do:

  • It has no supported way to remove a property. A property sent as null or "" is skipped and the stored value stays. A string that holds only spaces is the exception, and an unintended one: it removes the property, status and currency included, so trim what you send. close_date is stricter: when it is present it must be a valid date.
  • It cannot change external_deal_id. A request that carries the field is refused with 400.
  • It does not read tags. Use the tag endpoints.
  • It does not fill in the company. Linking a contact here leaves company_id as it was. Only Create a Deal takes the company from the contact.
Terminal window
curl -X PUT https://api.pathbound.ai/v1/deals/6acd7a3e9b1c4d2e8f012345 \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "properties": { "stage": "closed", "status": "won", "amount": 26500 } }'
{
"status": "success",
"message": "Deal updated successfully",
"deal": {
"deal_id": "6acd7a3e9b1c4d2e8f012345",
"_id": "6acd7a3e9b1c4d2e8f012345",
"properties": {
"name": "Northwind annual plan",
"stage": "closed",
"amount": 26500,
"currency": "USD",
"status": "won",
"close_date": "2026-10-30T00:00:00.000Z",
"createdate": "2026-10-13T09:14:02.118Z",
"lastmodifieddate": "2026-10-28T15:02:44.390Z"
},
"company_id": "665f1c2ab3d4e5f601234567",
"contact_id": "665f1c2ab3d4e5f6012345ab",
"external_deal_id": "crm-48213",
"notes": [],
"tags": ["renewal", "q4"],
"creation_source": "api",
"first_seen_at": "2026-10-13T09:14:02.118Z",
"created_at": "2026-10-13T09:14:02.118Z",
"updated_at": "2026-10-28T15:02:44.390Z"
},
"timestamp": "2026-10-28T15:02:44.431Z"
}

lastmodifieddate moves when the request carries properties. updated_at moves on every update.

A 404 means the deal, or the company or contact you asked to link, is not in your workspace. The error text says which.

DELETE /v1/deals/:deal_id

Required scope: deals:write

To record a deal that did not close, set its status to lost instead. Delete a deal that should never have existed, such as the copy a repeated import made: a copy marked lost still counts in every total of amounts.

{
"status": "success",
"message": "Deal deleted successfully",
"timestamp": "2026-10-28T15:10:09.118Z"
}
GET /v1/deals/:deal_id/notes

Required scope: deals:read

Returns every note on the deal, newest first. There is no pagination. This read is not metered.

{
"status": "success",
"notes": [
{
"_id": "6acd80f29b1c4d2e8f0123a1",
"content": "Procurement wants the DPA signed before the order form.",
"created_at": "2026-10-14T11:32:18.006Z",
"created_by": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"source": "api"
}
],
"timestamp": "2026-10-14T11:40:00.251Z"
}
FieldDescription
_idThe note’s id. It is the note_id of the two endpoints below.
contentThe note’s text.
created_atWhen the note was written. Changing the text does not move it.
created_byThe id of the user who wrote it. For a note written through this API, that is the user the API key belongs to.
sourceapi for a note written through this API or the MCP server, manual for one written in the dashboard.
POST /v1/deals/:deal_id/notes

Required scope: deals:write

FieldTypeRequiredDescription
contentstringyesThe note’s text, 1 to 10,000 characters. Surrounding whitespace is removed.
Terminal window
curl -X POST https://api.pathbound.ai/v1/deals/6acd7a3e9b1c4d2e8f012345/notes \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "content": "Procurement wants the DPA signed before the order form." }'
{
"status": "success",
"note": {
"_id": "6acd80f29b1c4d2e8f0123a1",
"content": "Procurement wants the DPA signed before the order form.",
"created_at": "2026-10-14T11:32:18.006Z",
"created_by": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"source": "api"
},
"timestamp": "2026-10-14T11:32:18.019Z"
}

A 429 with code: "QUOTA_EXCEEDED" and resource: "notes_per_deal" means the deal has reached its limit on notes.

PATCH /v1/deals/:deal_id/notes/:note_id

Required scope: deals:write

Replaces a note’s text. The body takes content, with the same rule as in Create a Deal Note.

{
"status": "success",
"message": "Note updated",
"timestamp": "2026-10-14T11:45:27.660Z"
}

The response does not return the note. A 404 means the deal or the note was not found.

DELETE /v1/deals/:deal_id/notes/:note_id

Required scope: deals:write

Deletes the note for good.

{
"status": "success",
"message": "Note deleted",
"timestamp": "2026-10-14T11:50:03.305Z"
}

A deal carries tags like a contact or a company does, with the same rules and the same limit of 50. Three endpoints change them, all with deals:write:

POST /v1/deals/:deal_id/tags
DELETE /v1/deals/:deal_id/tags
POST /v1/deals/tags/bulk

They are documented with the other tag endpoints on the Tags page. You can also set tags when you create a deal, and filter the list with tags_any and tags_all.

The error format and the errors every endpoint shares (a missing or invalid key, rate limits) are described in the API overview. These are the ones specific to deals.

StatuscodeMeaning
400Validation failed: a parameter or a body field has the wrong type or format, for example a deal_id that is not an id, a limit over 100 or a close_date that is not ISO 8601. details lists each problem.
400A property was refused. The error text says why: an unknown status, a currency that is not three letters, an amount that is not a number from 0 to 1e15, a string value over 1,000 characters, too many or too large properties, or external_deal_id sent on an update.
400Invalid search parameter: search is longer than 100 characters or contains characters that are not allowed, such as $.
400invalid_filterAn entry of filters is malformed: not a JSON array, more than 10 entries, an unknown operator, or a value of the wrong type for the field.
402BILLING_PLAN_LIMIT, BILLING_TRIAL_EXPIRED, BILLING_SUBSCRIPTION_PAUSED, BILLING_SUBSCRIPTION_INACTIVEA read would take the workspace over its plan’s allowance, or the workspace has no active plan. No deal is returned. See Usage and metering.
403The API key lacks deals:read or deals:write.
404not_found on Get a Deal, none elsewhereThe deal or the note is not in your workspace, or the company or contact you asked to link is not.
400INVALID_LINKCreate a Deal: company or contact is not an object with exactly one of its keys, or is sent beside company_id or contact_id.
422CONTACT_NOT_FOUND, COMPANY_NOT_FOUNDCreate a Deal: the contact or company key finds no record in your workspace.
429QUOTA_EXCEEDEDA storage limit was reached. resource says which: deals for the workspace’s deals, notes_per_deal for one deal’s notes.