GraphQL
GraphQL queries, mutations and real-time subscriptions over the CRM, including your custom objects.
POST /v1/graphql runs queries and mutations over leads, contacts, accounts,
opportunities, tasks, quotes and your custom objects; subscriptions run over a
WebSocket on the same path. Every resolver calls the same services as REST, so
record visibility, Profile permissions, field-level security, validation rules,
before-save hooks and plan entitlements match exactly.
- API keys and OAuth tokens need the
READ_scope of each entity the query touches (for exampleREAD_OPPORTUNITIESandREAD_ACCOUNTSabove), andWRITE_for mutations. A missing scope fails that field, not the whole operation. - Lists take
pageandlimit(default 25, capped at 100) and return{ nodes, pageInfo }. - Queries are limited before anything runs: 10,000 characters, depth 8, and a complexity of 5,000, where each list multiplies the cost of its children by its
limit(QUERY_TOO_LARGE,QUERY_TOO_DEEP,QUERY_TOO_COMPLEX). Ask for smaller pages or fewer nested lists. - GraphQL is part of API access (Enterprise, and every developer org).
Mutations
Create, update and delete leads, contacts, accounts, opportunities, tasks and
custom object records. Each mutation takes a typed input generated from the
REST body it mirrors: LeadCreateInput has the fields of POST /v1/leads
(required fields are non-null), LeadUpdateInput those of
PATCH /v1/leads/:id (all optional), and so on for Contact, Account,
Opportunity and Task. Enum fields use the GraphQL enums (stage: PROSPECTING,
not a string), ids are ID, and customFields is JSON keyed by field API
name. Unknown fields are rejected by the schema before anything runs; the same
DTO validation as REST still runs on every write.
Each custom object also gets typed writes built from its field definitions:
createCustom<Name>Record(input: Custom<Name>CreateInput!),
updateCustom<Name>Record(id, input: Custom<Name>UpdateInput!) and
deleteCustom<Name>Record(id), where a field marked required is non-null on
create. Introspect the schema (or open the GraphQL explorer in Settings) to see
every input field and its type.
Changed Oct 3 2026: mutation inputs used to be
JSON. A query that declares$input: JSON!now fails validation; declare the typed input ($input: LeadCreateInput!) instead. Inline object literals keep working, except that enum values are now written without quotes.
| Mutation | Returns |
|---|---|
createLead(input: LeadCreateInput!), createContact, createAccount, createOpportunity, createTask | the record, read back as you (fields you can't read are null) |
updateLead(id, input: LeadUpdateInput!) … updateTask(id, input) | the record |
deleteLead(id) … deleteTask(id) | { id deleted } |
createCustom<Name>Record(input), updateCustom<Name>Record(id, input), deleteCustom<Name>Record(id) | the typed custom record / { id deleted } |
createCustomRecord(object, data), updateCustomRecord(object, id, data), deleteCustomRecord(object, id) | any custom object by API name, with data as JSON |
- Each write runs the full save path: Profile create/edit/delete permission, field-level security, ownership and record sharing, validation rules, before-save hooks, stage requirements, workflows, webhooks and the audit trail.
- Errors carry the same codes as REST in
errors[].extensions.code, for exampleVALIDATION_RULE_FAILED,FIELD_NOT_EDITABLE,STAGE_REQUIREMENTS_NOT_MET,BEFORE_SAVE_HOOK_REJECTED,API_KEY_SCOPE_INSUFFICIENT,FEATURE_ACCESS_DENIEDandHTTP_404. A failed write fails only its own field; earlier writes in the same operation have already happened (mutations run in order, without a transaction). - At most 10 writes per operation (
TOO_MANY_MUTATIONS). - Send an
Idempotency-Keyheader and a retried request replays the first response instead of writing twice — see Idempotency.
Subscriptions
Subscribe to record changes over WebSocket with the
graphql-ws protocol
(graphql-transport-ws subprotocol) at wss://api.salesos.org/v1/graphql.
Authenticate in connection_init with the same credentials as HTTP:
- Events come from the event catalog — the same events
webhooks receive.
objectstakeslead,contact,account,opportunity,taskandcustom:<apiName>;typestakes catalog event types. Both are optional. - You only receive events about records you could read over REST: each event
is re-checked as you (visibility, sharing, Profile, plan, scopes) and the
record is read fresh, with fields you can't see set to
nulland dropped fromchangedFields. Deletes are checked against the record's last owner. - Session JWTs, API keys and OAuth tokens work; keys and tokens need the
entity's
READ_scope. The credential is re-checked every minute and the socket closes (4401with the reason, e.g.TOKEN_EXPIRED,API_KEY_REVOKED,OAUTH_TOKEN_REVOKED) when it is revoked or expires. - Limits: send
connection_initwithin 10 seconds; 10 subscriptions per connection (TOO_MANY_SUBSCRIPTIONS), 50 connections per organization (4429), 64 KB messages. The server pings every 30 seconds. A subscriber that falls more than 100 events behind loses the oldest. Only subscriptions run over the socket; send queries and mutations over HTTP. - Delivery is best-effort: a dropped connection misses events and nothing is replayed on reconnect. Use webhooks when every event must arrive.
Custom events
Your organization's custom events have their own subscription:
types is optional (all custom events). These are organization-level events,
not records, so every subscriber in the organization receives them; API keys
and OAuth tokens need READ_EVENTS or PUBLISH_EVENTS.
Custom fields
Every lead, contact, account, opportunity and quote has customFields: a JSON
object of the record's custom field values keyed by field API name, including
computed formula and roll-up values.
Custom objects
Each organization's schema includes its own custom objects. For an object with
API name project and fields title (text), budget (currency) and active
(checkbox), you get a typed object:
The type is Custom<PascalName> and the queries are custom<PascalName>Records
and custom<PascalName>Record. Number and currency fields are Float,
checkboxes Boolean, multi-picklists JSON, and everything else String.
A lookup or master-detail field is an ID (account) plus a resolved
<field>Ref: CustomLookup { id object label } (accountRef), where the label
is filled only for targets you can see. Record lists also take search,
filters (the REST filter JSON),
sortBy and sortOrder, and each record has lookups. The schema reflects a
definition change within about 30 seconds.
The same records are available untyped, which suits generic tools:
Custom objects need READ_CUSTOM_OBJECTS (and WRITE_CUSTOM_OBJECTS for
mutations) for API keys and the Custom Objects feature on your plan. Record
visibility is the same as REST: the object's profile permissions and data
access, record shares and master-detail inheritance.