Part 1: How to Authenticate with the SuiteCRM v8 REST API Using OAuth2 · Part 2: SuiteCRM v8 REST API CRUD Operations (you are here)
New here? Start with Part 1: How to Authenticate with the SuiteCRM v8 REST API Using OAuth2 to set up OAuth2 and make your first GET request. This article assumes you have a working access_token and understand the required headers.
Once authenticated, you can perform full CRUD operations against the SuiteCRM v8 REST API — Create, Read, Update, and Delete records across any CRM module. This article covers the endpoints, request shapes, and patterns you need to query lists, write data, and manage relationships in production.
1. CRUD Operations — Real Business Scenarios
Create a Lead from a website form
Why: Capture inbound interest instantly instead of batch-importing CSV files.
Endpoint: POST {BASE_URL}/Api/V8/module
{
"data": {
"type": "Leads",
"attributes": {
"first_name": "Jane",
"last_name": "Prospect",
"email1": "This email address is being protected from spambots. You need JavaScript enabled to view it.
",
"lead_source": "Web Site"
}
}
}
cURL
curl -X POST "{BASE_URL}/Api/V8/module" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"data":{"type":"Leads","attributes":{"first_name":"Jane","last_name":"Prospect","email1":"This email address is being protected from spambots. You need JavaScript enabled to view it.
","lead_source":"Web Site"}}}'
PHP
<?php
$body = json_encode([
'data' => [
'type' => 'Leads',
'attributes' => [
'first_name' => 'Jane',
'last_name' => 'Prospect',
'email1' => 'This email address is being protected from spambots. You need JavaScript enabled to view it.
',
'lead_source' => 'Web Site',
],
],
]);
$ch = curl_init(getenv('SUITECRM_BASE_URL') . '/Api/V8/module');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/vnd.api+json',
'Accept: application/vnd.api+json',
'Authorization: Bearer ' . $accessToken,
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
JavaScript (fetch)
const response = await fetch(`${process.env.BASE_URL}/Api/V8/module`, {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
data: {
type: 'Leads',
attributes: {
first_name: 'Jane',
last_name: 'Prospect',
email1: 'This email address is being protected from spambots. You need JavaScript enabled to view it.
',
lead_source: 'Web Site',
},
},
}),
});
const lead = await response.json();
Update an Opportunity after payment
Why: Close the loop when your payment gateway confirms a deal.
Endpoint: PATCH {BASE_URL}/Api/V8/module — note the ID goes in the body, not the URL.
Common mistake: Sending PATCH to /Api/V8/module/Opportunities/{id}. The official endpoint is PATCH /Api/V8/module with data.id in the body.
{
"data": {
"type": "Opportunities",
"id": "YOUR_OPPORTUNITY_ID",
"attributes": {
"sales_stage": "Closed Won",
"amount": "15000.00"
}
}
}
cURL
curl -X PATCH "{BASE_URL}/Api/V8/module" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"data":{"type":"Opportunities","id":"YOUR_OPPORTUNITY_ID","attributes":{"sales_stage":"Closed Won","amount":"15000.00"}}}'
Close a Case after customer support
{
"data": {
"type": "Cases",
"id": "YOUR_CASE_ID",
"attributes": {
"status": "Closed",
"resolution": "Issue resolved via API integration."
}
}
}
Send the same PATCH endpoint with the case type, ID, and updated status attributes.
Delete a test record
Endpoint: DELETE {BASE_URL}/Api/V8/module/{moduleName}/{id}
curl -X DELETE "{BASE_URL}/Api/V8/module/Leads/YOUR_LEAD_ID" \
-H "Accept: application/vnd.api+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
PHP
<?php
$url = getenv('SUITECRM_BASE_URL') . '/Api/V8/module/Leads/' . $leadId;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/vnd.api+json',
'Authorization: Bearer ' . $accessToken,
],
]);
curl_exec($ch);
curl_close($ch);
JavaScript (fetch)
await fetch(`${process.env.BASE_URL}/Api/V8/module/Leads/${leadId}`, {
method: 'DELETE',
headers: {
'Accept': 'application/vnd.api+json',
'Authorization': `Bearer ${accessToken}`,
},
});
2. Filtering, Sorting, and Pagination
Listing every record in a module does not scale. Production integrations must filter, sort, and paginate.
Filter
Supported operators include eq, neq, gt, gte, lt, lte, combined with AND or OR.
{BASE_URL}/Api/V8/module/Accounts?filter[account_type][eq]=Customer
With explicit AND operator:
{BASE_URL}/Api/V8/module/Accounts?fields[Accounts]=name,account_type&filter[operator]=and&filter[account_type][eq]=Customer
Sort
Ascending by default. Prefix with - for descending. Single-field sort only.
{BASE_URL}/Api/V8/module/Accounts?sort=-name
Pagination
Why pagination matters: Fetching thousands of records in one call causes timeouts and incomplete syncs. Always paginate list endpoints in production.
{BASE_URL}/Api/V8/module/Accounts?fields[Account]=name,account_type&page[number]=3&page[size]=10
Response includes pagination metadata:
{
"meta": { "total-pages": 54 },
"data": [ ... ],
"links": {
"first": "/V8/module/Accounts?...&page[number]=1&page[size]=10",
"prev": "/V8/module/Accounts?...&page[number]=2&page[size]=10",
"next": "/V8/module/Accounts?...&page[number]=4&page[size]=10",
"last": "/V8/module/Accounts?...&page[number]=54&page[size]=10"
}
}
JavaScript pagination loop
let page = 1;
let hasMore = true;
while (hasMore) {
const res = await fetch(
`${process.env.BASE_URL}/Api/V8/module/Accounts?page[number]=${page}&page[size]=50`,
{ headers: { 'Accept': 'application/vnd.api+json', 'Authorization': `Bearer ${accessToken}` } }
);
const json = await res.json();
processRecords(json.data);
hasMore = json.links?.next != null;
page++;
}
3. Working with Relationships
Why: CRM value is in connected data — contacts on an account, cases on a contact.
Endpoint: GET {BASE_URL}/Api/V8/module/{moduleName}/{id}/relationships/{linkFieldName}
Discover the link field name
Do not guess relationship names. Fetch the parent record and inspect the relationships object:
"relationships": {
"AOS_Contracts": {
"links": {
"related": "/V8/module/Accounts/11a71596-83e7-624d-c792-5ab9006dd493/relationships/aos_contracts"
}
}
}
Or inspect via: GET {BASE_URL}/Api/V8/meta/fields/Accounts
Fetch related records
GET {BASE_URL}/Api/V8/module/Accounts/129a096c-5983-1d59-5ddf-5d95ec91c144/relationships/members
cURL
curl -X GET "{BASE_URL}/Api/V8/module/Accounts/129a096c-5983-1d59-5ddf-5d95ec91c144/relationships/members" \
-H "Accept: application/vnd.api+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Create a relationship
POST {BASE_URL}/Api/V8/module/{moduleName}/{id}/relationships/{linkFieldName}
{
"data": {
"type": "Contacts",
"id": "RELATED_CONTACT_ID"
}
}
Delete a relationship
DELETE {BASE_URL}/Api/V8/module/Accounts/{accountId}/relationships/{linkFieldName}/{relatedId}
4. Production Best Practices
- Security — Store credentials in environment variables or a secrets manager. Use HTTPS. Scope OAuth clients to minimum required role.
- Token management — Cache access tokens until near expiry, then refresh. Revoke compromised tokens from Admin.
- Performance — Request only needed fields via
fields[]. Paginate all list calls. - Logging — Log URLs and status codes on failure. Redact tokens from logs.
- Error handling — Retry 5xx with exponential backoff. Fail fast on 401 and 400.
Build a thin API client class that handles auth refresh, headers, and error parsing once — then reuse it everywhere.
5. Real Business Integrations
The same API primitives power diverse production systems:
WhatsApp integration
An inbound message triggers POST to create a Lead or Case, then PATCH updates the record as agents respond.
Voice AI receptionist
Speech-to-text captures caller intent. The service queries Accounts by phone filter and creates Cases when needed.
AI-powered lead qualification
A scoring service reads new Leads via filtered GET, enriches with PATCH, and creates Opportunities for qualified leads.
QuickBooks synchronization
Payment webhooks PATCH Opportunities to Closed Won. Scheduled jobs paginate Accounts against accounting records.
Custom customer portal
Portal users see their Account, Contacts, and Cases via scoped API calls with a dedicated OAuth2 service user.
6. Common Mistakes and Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
404 on /Api/access_token |
SuiteCRM 8 path missing /legacy/ |
Try {BASE_URL}/legacy/Api/access_token; confirm via Swagger |
| 401 on data requests | Expired or missing Bearer token | Refresh or re-authenticate (Part 1: Authentication) |
| PATCH returns 404 | ID sent in URL instead of body | Use PATCH /Api/V8/module with data.id in body |
| Relationship 404 | Wrong linkFieldName |
Inspect relationships on parent record |
| Empty field data | Field not in fields[] or ACL issue |
Add field to query; verify user role permissions |
| Custom field not writable | Field name differs from label | Check GET /Api/V8/meta/fields/{moduleName} |
7. Conclusion
The SuiteCRM API is the foundation for every serious integration — portals, messaging channels, AI tools, ERP sync, and custom apps. Combined with Part 1: How to Authenticate with the SuiteCRM v8 REST API Using OAuth2, you now have the full toolkit: OAuth2 auth, CRUD, filters, pagination, and relationships.
Next steps:
- Import the Postman collection from your instance.
- Explore
/Api/V8/meta/modulesand/Api/V8/meta/fields/{module}. - Build a reusable API client with token refresh.
- Ship one integration endpoint to production before expanding scope.
Frequently Asked Questions
How do I update a record?
PATCH /Api/V8/module with data.type, data.id, and data.attributes in the body.
How do I paginate list requests?
Use page[number] and page[size] on GET list endpoints. Follow links.next until exhausted.
How do I find relationship link names?
Inspect the relationships object on a GET record response, or use GET /Api/V8/meta/fields/{moduleName}.
Where is the full endpoint list?
{BASE_URL}/Api/docs/swagger/swagger.json or GET {BASE_URL}/Api/V8/meta/swagger.json.
Quick Summary
- CRUD: POST create, PATCH update (ID in body), DELETE by module/id.
- Lists: Filter, sort, paginate with query parameters.
- Relationships: Discover link names from record responses.
Need Help Building a SuiteCRM Integration?
Our team builds production SuiteCRM API integrations — custom portals, ERP sync, messaging channels, and more.
SuiteCRM 8 Development Services