updateDeal
Update a deal by id: title, price, close date, stage.
Update a deal by id: title, price, close date, stage. THIS IS THE STAGE-MOVE OPERATION: send input { stageId } alone (from listPipelineStages); fields left out are untouched, and the move stamps lastStageChangeAt. A stage change is POTENTIALLY OUTBOUND by TWO paths: (1) entering the stage wakes any DEAL workflow triggered on it; (2) it fires a deal-updated event for EVERY attached contact, and a CONTACT workflow anywhere in the org can match the destination stage, so a clean pipeline does NOT prove the move is silent. Either can text or email a real person. Do NOT reason "reversible, so just do it": undoing the move does not recall a message. A move the person asked for by name is their yes; any other move of a deal with contacts needs their yes before it runs (a card counts). The deal, stage and linked contactIds/userIds must belong to your organization (or a pipeline shared with it). contactIds, propertyIds and userIds REPLACE the whole linked set: send every id to keep. Pass expectedUpdatedAt (updatedAt from getDeal, findDeals or a previous write) whenever you decided from an earlier read, so a concurrent edit fails loudly. It makes the write conditional and ALL-OR-NOTHING: a change since gives CONFLICT with nothing written, and a rejected userIds, participantSplits or customFieldValues rolls the whole edit back. The token is single-use: re-read before guarding again. Without it a later custom-field failure cannot undo the committed core write. A deleted deal is NOT_FOUND (do not retry); a bad token is BAD_USER_INPUT. MONEY AND CUSTOM FIELDS (the whole input is one variable, so discovery cannot expand it). One commission source per side: commissionAmountCents XOR commissionPercentBps (basis points, 300 = 3%, of priceCents), and teamSplitAmountCents XOR teamSplitPercentBps (of the COMMISSION). Both halves with values is rejected; one sent as null CLEARS that side. The server derives commissionCents (GCI) and returns it: read it there. A percent re-derives when priceCents changes; an amount does not. participantSplits is [{ userId, splitAmountCents | splitPercentBps }] of what remains after the team fee, within the pool. customFieldValues is [{ customFieldId (listDealCustomFields; fields are PER-PIPELINE), exactly ONE of textValue (TEXT/LINK) | numberValue (NUMBER/DOLLAR, a plain number, NOT cents) | dateValue (DATE, ISO 8601) | dropdownSelectedValues (DROPDOWN option LABELS, not ids) }]. The response echoes the money and custom fields: confirm the write from it.
curl https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer $GOLIATH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"operationId": "updateDeal",
"variables": {
"dealId": "<dealId>",
"input": {}
}
}'import { GoliathClient } from '@goliath-data/sdk'
const goliath = new GoliathClient({
apiKey: process.env.GOLIATH_API_KEY!,
baseUrl: 'https://server.goliathdata.com',
})
const result = await goliath.deals.updateDeal(
{
dealId: '<dealId>',
input: {}
},
{ idempotencyKey: crypto.randomUUID() },
)const res = await fetch('https://server.goliathdata.com/api/v1/call', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GOLIATH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
operationId: 'updateDeal',
variables: {
dealId: '<dealId>',
input: {}
}
}),
})
const { data, errors } = await res.json()Variables
The example sends only what the schema requires. Some operations also need one of several optional variables (one filter or another, say); the description above says which.
Every field, all the way down
[
{
"name": "dealId",
"type": "ID!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "input",
"type": "PipelineItemMutationInput!",
"required": true,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "title",
"type": "String",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "stageId",
"type": "ID",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "description",
"type": "String",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "priceCents",
"type": "BigInt",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "commissionAmountCents",
"type": "BigInt",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "commissionPercentBps",
"type": "Int",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "teamSplitAmountCents",
"type": "BigInt",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "teamSplitPercentBps",
"type": "Int",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "participantSplits",
"type": "[PipelineItemParticipantSplitInput!]",
"required": false,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "userId",
"type": "ID!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "splitAmountCents",
"type": "BigInt",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "splitPercentBps",
"type": "Int",
"required": false,
"description": null,
"defaultValue": null
}
]
},
{
"name": "closeDate",
"type": "DateTime",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "propertyIds",
"type": "[ID!]",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "propertyStrings",
"type": "[String!]",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "contactIds",
"type": "[ID!]",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "userIds",
"type": "[ID!]",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "fileIds",
"type": "[ID!]",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "order",
"type": "Int",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "customFieldValues",
"type": "[PipelineItemCustomFieldValueInput!]",
"required": false,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "customFieldId",
"type": "ID!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "textValue",
"type": "String",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "dateValue",
"type": "DateTime",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "numberValue",
"type": "Float",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "dropdownSelectedValues",
"type": "[String!]",
"required": false,
"description": null,
"defaultValue": null
}
]
}
]
},
{
"name": "expectedUpdatedAt",
"type": "DateTime",
"required": false,
"description": null,
"defaultValue": null
}
]Response
What comes back under data: a list shows as one entry, and a value as its type.
{
"data": {
"dealMutation": {
"updateDeal": {
"id": "<ID>",
"title": "…",
"priceCents": "…",
"closeDate": "2026-01-01T00:00:00.000Z",
"isArchived": true,
"updatedAt": "2026-01-01T00:00:00.000Z",
"lastStageChangeAt": "2026-01-01T00:00:00.000Z",
"commissionCents": "…",
"commissionAmountCents": "…",
"commissionPercentBps": 0,
"teamSplitAmountCents": "…",
"teamSplitPercentBps": 0,
"participantSplits": [
{
"user": {
"id": "<ID>",
"firstName": "…",
"lastName": "…"
},
"splitAmountCents": "…",
"splitPercentBps": 0
}
],
"customFieldValues": [
{
"id": "<ID>",
"textValue": "…",
"dateValue": "2026-01-01T00:00:00.000Z",
"numberValue": 0,
"dropdownSelectedValues": [
"…"
],
"dealCustomField": {
"id": "<ID>",
"name": "…",
"type": "DROPDOWN"
}
}
],
"stage": {
"id": "<ID>",
"name": "…"
},
"pipeline": {
"id": "<ID>",
"name": "…"
},
"contacts": [
{
"id": "<ID>",
"name": "…"
}
],
"properties": [
{
"id": "<ID>",
"address": {
"addressFull": "…"
}
}
]
}
}
}
}Errors
unknown_variableA variable the operation does not declare. A typo is refused rather than silently dropped.
unauthorizedNo bearer header, or the key is invalid or revoked.
insufficient_scopeThe key lacks the scope the operation requires.
forbiddenA referenced record does not exist, was deleted, or is outside your organization. The gateway cannot tell those apart.
idempotency_conflictA request with this Idempotency-Key is still in flight.
Idempotency
Send an Idempotency-Key of 1 to 255 characters. A finished request retried with the same key replays its stored response, marked Idempotency-Replayed: true; one still in flight returns 409 idempotency_conflict. Keys expire after 24 hours. Write without fear →