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-sent",
"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",
"companyId": "c_1a2b",
"lostReason": null,
"notes": null,
"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. Must exist. 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.
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.
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.

Bodytitle and consentSiteId are required. 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.

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.

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 or consentSiteId, a pipelineId or stageId that does not exist, an archived pipelineId, a status that disagrees with the stageId, 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, 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.