Skip to main content

Deals

An opportunity — a titled, valued thing a contact or a company might buy — moving through the stages of a pipeline until it is won or lost.

Scopes and the site rule are shared with every CRM resource — see companies.

Plan availability

Deals are part of the CRM suite, included from Starter — a plan without it answers 403 plan_required with code: "crm". A deal is also a CRM record: it counts with contacts and companies against the plan's records band, and on a plan that hard-bands (Free) a create past the band answers 403 plan_required with code: "crm_records_quota", with the key given back. See companies for the same rule on the other collections.

The deal object​

{
"id": "d_3c9a",
"object": "deal",
"title": "Wholesale beans — Q4",
"pipelineId": "p_7b2e",
"stageId": "proposal-price-quote",
"status": "open",
"amountCents": 250000,
"currency": "usd",
"lineItems": [
{ "productId": "prod_8c1d", "name": "House blend, 5 lb", "quantity": 50, "unitAmountCents": 4500, "currency": "usd" },
{ "productId": null, "name": "Delivery", "quantity": 1, "unitAmountCents": 25000, "currency": "usd" }
],
"expectedCloseAt": "2026-12-31T00:00:00.000Z",
"closedAt": null,
"stageChangedAt": "2026-09-05T18:23:23.941Z",
"ownerUid": "u_9f1c",
"contactId": "k7d2b9f104",
"contactRoles": [
{ "contactId": "k7d2b9f104", "role": "Decision Maker", "primary": true },
{ "contactId": "m2a8c4e771", "role": "Evaluator", "primary": false }
],
"companyId": "c_1a2b",
"lostReason": null,
"notes": null,
"type": "New Business",
"leadSource": "Trade show",
"nextStep": "Walk the second location with the owner",
"probability": null,
"forecastCategory": "bestCase",
"campaignId": "cmp_spring",
"custom": { "segment": "enterprise" },
"nextTaskAt": "2026-09-12T15:00:00.000Z",
"siteId": "site_a1b2c3",
"created": "2026-09-01T09:00:00.000Z",
"updated": "2026-09-05T18:23:23.941Z"
}
FieldTypeNotes
idstringOpaque deal id.
objectstringAlways "deal".
titlestringRequired. Trimmed, truncated to 200 characters. Writable.
pipelineIdstringThe pipeline the deal is in. Writable on create only: a deal's stages are its pipeline's, and moving one between pipelines is a new deal in the other pipeline, not an edit. Omit it to use the default among the active pipelines — seeding one if the organization has none. An archived pipeline is refused.
stageIdstringThe stage the deal is in, one of its pipeline's stages[].id. Writable — see moving a deal.
statusstringopen, won or lost. Follows the stage's kind, so the two can never disagree on a stored deal. Writable — see moving a deal.
amountCentsinteger | nullThe value, in the currency's minor unit. A whole number, 0 or more. Writable on a deal with no line items; with line items it is their sum — see line items.
currencystringThree-letter ISO 4217 code, lowercase. usd when never set. Writable; USD is stored as usd. On a deal with line items, changing it needs the lineItems resent in the new currency.
lineItemsarrayThe products behind the amount, each { productId, name, quantity, unitAmountCents, currency }. Empty when the amount is typed. Writable — see line items.
expectedCloseAtstring | nullISO 8601 instant. Writable.
closedAtstring | nullWhen the deal was won or lost. Set by a move into a closed stage and cleared by a move back to an open one. Read-only.
stageChangedAtstring | nullWhen the deal last moved — what "stuck in stage" reports read. Read-only.
ownerUidstring | nullThe member responsible. Must be a member of your organization. Writable.
contactIdstring | nullThe contact the deal is with — the Primary of contactRoles. Must exist. Writable: setting it makes that contact Primary, adding them to contactRoles with no role when the deal did not name them; null leaves the deal with no Primary.
contactRolesarrayEvery contact on the deal and the part each plays — Salesforce's Opportunity Contact Roles — each { contactId, role, primary }. See contact roles. A deal written before roles reads as its contactId, Primary, with role: null. Writable.
companyIdstring | nullThe company the deal is with. Must exist. Writable.
lostReasonstring | nullFree text, 5,000 characters. Writable.
notesstring | nullFree text, 5,000 characters. Writable.
typestring | nullSalesforce's Type: one of the organization's deal types — New Business, Existing Business or its own (picklist). Matched in any case and stored as the list spells it; a value outside the active list is a 400 naming the values it allows, and a deal keeps a value it already holds. A create that sends none takes the list's default, when it has one. Writable.
leadSourcestring | nullWhere the deal came from: one of the organization's lead sources, judged like type. Never defaulted; a lead's conversion stamps the lead's own. Writable.
nextStepstring | nullWhat happens next, 255 characters. Writable.
probabilityinteger | nullThis deal's own chance of closing, 0–100, over its stage's; null uses the stage's. What the weighted forecast multiplies an open deal by. Every move clears it. Writable.
forecastCategorystring | nullomitted, pipeline, bestCase, commit or closed — a fixed set, as in Salesforce. Every move sets it from the new stage's; null only on a deal stored before the field existed, which forecasts as its stage's. Writable.
campaignIdstring | nullThe campaign the deal is attributed to (Salesforce's Primary Campaign Source). Must be one of the organization's live campaigns; a deal keeps the one it names after that campaign is retired. Writable.
customobjectThe organization's deal custom fields, keyed by field key; {} when the deal has none. Judged against the deal definitions: a key that is not one, a retired field, or a value the type cannot hold is a 400 naming custom.<key>. A PATCH merges the keys it sends; null clears one. Writable.
nextTaskAtstring | nullWhen the earliest open task filed against the deal is due, or null when none is — its next activity, and what the console's "stuck deals" figure counts. Maintained by every task write, this resource included. Read-only.
mediaIdsstring[]Files from the organization's media library attached to this record, by media id, at most 20. An empty array clears them. Writable.
siteIdstringThe site the deal was created from. Read-only.
created / updatedstring | nullISO 8601.

Endpoints​

List deals​

GET /v1/deals — scope crm:read. Paginated, ordered by deal id unless updatedAfter is given.

ParamNotes
contactIdDeals with this contact.
companyIdDeals with this company.
pipelineIdDeals in this pipeline.
ownerUidDeals owned by this member.
statusopen, won or lost. Anything else is a 400 — ?status=closed matching nothing would be a plausible page, and a plausible page is the one you don't check.
typeDeals of this type, spelled exactly as stored.
leadSourceDeals from this lead source, spelled exactly as stored.
forecastCategoryDeals in this forecast category.
campaignIdDeals attributed to this campaign.
updatedAfterDeals updated after this instant, oldest change first — the sync filter.
limit, cursorStandard pagination.
# everything open for one account
curl "https://app.aglyn.com/api/v1/deals?companyId=c_1a2b&status=open" \
-H "Authorization: Bearer aglyn_sk_…"

Only one filter narrows the query itself — in the order listed, so an id before a status — and the rest are checked on the page. A combined filter can therefore return a short page.

Retrieve a deal​

GET /v1/deals/{dealId} — scope crm:read. Returns a deal, or 404 not_found ("No such deal").

Add a deal​

POST /v1/deals — scope crm:write. Accepts an Idempotency-Key, scoped to the organization.

Body — title is required. consentSiteId names the site the record came from; leave it out for a record from no site, which belongs to the whole organization. It is required only when your organization keeps each site's records apart. pipelineId picks the pipeline; stageId and/or status place the deal in it (see below); neither means the first open stage of the pipeline.

curl -X POST "https://app.aglyn.com/api/v1/deals" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Idempotency-Key: 9a41f0c2-…" \
-H "Content-Type: application/json" \
-d '{"title":"Wholesale beans — Q4","amountCents":250000,"currency":"usd","contactId":"k7d2b9f104","companyId":"c_1a2b","ownerUid":"u_9f1c","consentSiteId":"site_a1b2c3"}'

Returns 201 with the created deal, or 200 with the original when an Idempotency-Key replays.

Update a deal​

PATCH /v1/deals/{dealId} — scope crm:write. Takes no Idempotency-Key. Send only what changes; an omitted key is left alone, an explicit null clears an optional field, and {} is a no-op.

Moving a deal​

A deal moves by stageId, by status, or by both:

  • stageId picks a stage, and status follows the stage's kind. Moving to the pipeline's won stage marks the deal won.
  • status alone picks the pipeline's one won or lost stage — or, for open, its first open stage. A reopened deal has to land somewhere, and the top of the pipeline is the only somewhere that needs no second field.
  • Both together must agree. {"stageId":"won","status":"open"} is a 400 naming status ("Must match the stage, which is won") rather than a guess in favor of either, because whichever one we picked would be the one you did not mean.

Every move stamps stageChangedAt. A move into a won or lost stage sets closedAt; a move back to an open stage clears it.

Every move also sets forecastCategory from the new stage and clears probability back to null, so the deal forecasts at the new stage's odds — as Salesforce re-defaults a probability when the stage changes. A body that sends either beside the move keeps what it sent: {"stageId":"negotiation-review","probability":95} moves the deal and sets 95%.

A move into the won stage — and a POST that creates a deal there — also sets the linked contact's lifecycle stage to customer for the deal's site, the way an order does: a contact with no stage or an earlier one becomes a customer, and a later stage (evangelist, other) is never lowered. Read it back on the contact — the deal's own representation does not change. There is no flag to turn this off; a won deal is a customer by definition.

Contact roles​

contactRoles is the whole list, replaced as sent:

FieldTypeNotes
contactIdstringA contact of the organization. Each contact once.
rolestring | nullOne of the organization's contact roles — Salesforce's Business User, Decision Maker, Economic Buyer, Economic Decision Maker, Evaluator, Executive Sponsor, Influencer, Technical Buyer, Other, or its own (picklist). Judged like type; a contact keeps the role it already holds on the deal after the value is deactivated. null or omitted for none.
primarybooleanAt most one entry. Omitted is false.

At most fifty contacts. The Primary is contactId: the deal stores the Primary's id there, and a list with no Primary clears it. A body that sends both must agree — contactId naming the Primary, or null beside a list with none — or it is a 400 naming contactId. A body that sends contactId alone keeps the list and makes that contact Primary. A refusal is named under fields.contactRoles: "At most one contact may be Primary", "Entry 1: c_9 is on the list twice", "No such contact in this organization: c_9", or the roles the list allows.

curl -X PATCH "https://app.aglyn.com/api/v1/deals/d_3c9a" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Content-Type: application/json" \
-d '{"contactRoles":[{"contactId":"k7d2b9f104","role":"Decision Maker","primary":true},{"contactId":"m2a8c4e771","role":"Evaluator"}]}'

Deleting or erasing a contact takes them off every deal's contactRoles (and clears contactId where they were the Primary); merging moves them to the surviving contact.

Line items​

lineItems is the list of products behind the amount. Each line is:

FieldTypeNotes
productIdstring | nullA catalog product the line came from, or null for a line priced by hand. Not checked against the catalog.
namestringRequired. Trimmed, truncated to 120 characters. Copied rather than joined, so a renamed or deleted product does not change what the deal was for.
quantityintegerA whole number, 1 to 1,000,000.
unitAmountCentsintegerPer unit, in the currency's minor unit. A whole number, 0 or more.
currencystringOptional on write; defaults to the deal's. Must equal the deal's currency — the lines' sum is one number in one unit.

At most fifty lines. The rules:

  • With one or more lines, amountCents is their sum, written by the server beside them. Sending amountCents in the same body is a 400 naming amountCents; so is sending it alone to a deal that has lines.
  • lineItems: [] (or null) clears the lines and hands the amount back to you. The last sum stays until you retype it.
  • Changing currency on a deal with lines is a 400 naming currency unless the body also carries lineItems in the new currency.
  • A refused line is named — "Line 2: the quantity must be a whole number from 1 to 1,000,000" — under fields.lineItems.
curl -X PATCH "https://app.aglyn.com/api/v1/deals/d_3c9a" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Content-Type: application/json" \
-d '{"lineItems":[{"productId":"prod_8c1d","name":"House blend, 5 lb","quantity":50,"unitAmountCents":4500},{"name":"Delivery","quantity":1,"unitAmountCents":25000}]}'

The deal comes back with amountCents: 250000.

curl -X PATCH "https://app.aglyn.com/api/v1/deals/d_3c9a" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Content-Type: application/json" \
-d '{"status":"won","amountCents":262500}'

Delete a deal​

DELETE /v1/deals/{dealId} — scope crm:write. Accepts an Idempotency-Key, scoped to the organization.

{ "id": "d_3c9a", "object": "deal", "deleted": true }

The deal alone is removed; its tasks and activities keep their dealId. A missing deal is 404 not_found, unless the call carries the key of the delete that removed it.

Errors​

StatustypeWhen
400bad_requestcode: "validation_failed" — a missing title, a missing consentSiteId where the organization keeps its sites apart, a pipelineId or stageId that does not exist, an archived pipelineId, a status that disagrees with the stageId, a type or leadSource outside the organization's active lists, a probability that is not a whole number 0–100, a forecastCategory outside the five, a campaignId that is not one of the organization's live campaigns, an amountCents that is not a whole number 0 or more or that is sent to a deal with line items, a currency that is not a three-letter code or that a deal's line items are not in, a line item that fails its rules, an expectedCloseAt that is not an ISO 8601 instant, a contactId or companyId that does not exist, an ownerUid who is not a member, a custom entry that is not a deal field or does not fit its type (named as custom.<key>), or pipelineId on a PATCH. On the list, a ?status= outside the three values or a malformed ?updatedAfter=. fields names each key.
403plan_requiredcode: "crm" — the plan doesn't include the CRM suite. code: "crm_records_quota" — the CRM records band is full on a plan that doesn't meter the overage.
403insufficient_scopeKey lacks crm:read / crm:write.
404not_found"No such deal".
405method_not_allowedAllow: GET, POST on /v1/deals, GET, PATCH, DELETE on one deal.
409conflictcode: "idempotency_in_progress".

See Conventions → Errors for the shared envelope.

  • Pipelines — the stages a deal's stageId names.
  • Tasks and activities — the work and the history against a deal, each filterable by dealId.