# Error handling and API limits

Salesforce error arrays, per-record failure policies and staying within the daily API request allowance.

## How Salesforce reports errors {#how-salesforce-reports-errors}

A failed REST call answers with a JSON array of error objects - an array, because one request can violate several rules at once. Each object contains an errorCode, a human-readable message and, for field-level problems, the fields involved:

```json
[
  {
    "errorCode": "REQUIRED_FIELD_MISSING",
    "message": "Required fields are missing: [Name]",
    "fields": ["Name"]
  }
]
```

Because error responses are JSON like any other, they arrive in your service as the parsed value - a list instead of the dict a successful call returns. That makes the check straightforward:

```python
response = conn.post('/sobjects/Campaign/', record)

# Salesforce answers with a list of error objects when the request fails
if isinstance(response, list):
    error = response[0]
    error_code = error['errorCode']
    message = error['message']
    raise Exception(f'Salesforce error {error_code}: {message}')
```

The codes you will actually meet:

| errorCode | What it means |
| --- | --- |
| REQUIRED\_FIELD\_MISSING | A field the object requires was not in the request |
| INVALID\_FIELD | A field name does not exist on the object - usually a typo or a missing \_\_c |
| INVALID\_FIELD\_FOR\_INSERT\_UPDATE | The field exists but cannot be written, e.g. a formula field |
| MALFORMED\_QUERY | The SOQL text does not parse |
| NOT\_FOUND | The record ID does not exist or was deleted |
| ENTITY\_IS\_DELETED | The record is in the recycle bin |
| INVALID\_SESSION\_ID | The access token expired or was revoked |
| REQUEST\_LIMIT\_EXCEEDED | The org's daily API request allowance is used up |
| UNABLE\_TO\_LOCK\_ROW | Another transaction holds the record - retrying usually succeeds |

## Failing one record or failing the run {#failing-one-record-or-failing-the-run}

A service that handles one request at a time - a REST channel creating one record - should raise, as above. The caller receives the error in the HTTP response and is responsible for handling it.

A sync that processes hundreds of records needs a policy, because one bad record must not silently take down the other 499. The pattern is to catch per record, log with enough context to find the culprit, count, and continue:

```python
updated = 0
failed  = 0

for record in records:
    try:
        self.process_one(conn, record)
        updated += 1
    except Exception:
        failed += 1
        record_id = record['Id']
        self.logger.warning('Could not process `%s`, e:`%s`', record_id, format_exc())

self.logger.info('Sync done, updated: %s, failed: %s', updated, failed)
```

With the end-of-run summary, the log answers the question "did last night's sync work?" directly. If failures should page someone, the counters are also the right place to trigger a notification.

The exception to log-and-continue is an error that will affect every remaining record - INVALID\_SESSION\_ID or REQUEST\_LIMIT\_EXCEEDED means the next 499 calls will fail exactly like the first one did, so raise immediately and let the run stop.

## The daily API request allowance {#the-daily-api-request-allowance}

Every Salesforce org has a rolling 24-hour limit on API requests - from 15,000 calls per day in smaller editions upwards, shared by all integrations connected to the org, yours and everyone else's. An integration that works flawlessly in a sandbox can exhaust a production org's allowance simply by being inefficient, and when the allowance is gone, every integration in the company stops.

The current usage is one GET away:

```python
response = conn.get('/limits/')

api_requests = response['DailyApiRequests']
remaining = api_requests['Remaining']
max_allowed = api_requests['Max']

self.logger.info('API requests remaining: %s of %s', remaining, max_allowed)
```

Logging this at the end of each scheduled run produces a usage trend at the cost of one request per run, and a service that raises an alert when Remaining drops below a threshold is a few lines more.

## Spending the allowance well {#spending-the-allowance-well}

The techniques from the other guides in this section are also the ones that keep request counts low, so this is mostly a checklist:

- [Traverse relationships](https://zato.io/docs/dev/salesforce/soql-relationships-and-pagination.html) in one SOQL query instead of fetching parents record by record - one query with Account.Name in the select list replaces a thousand per-record lookups.
- [Compare before writing](https://zato.io/docs/dev/salesforce/updates-and-deletes.html) - a record that has not changed costs zero write calls.
- Filter queries on LastModifiedDate so each run reads only the records changed since the previous run.
- Let the [scheduler](https://zato.io/docs/dev/examples/scheduler.html) set the pace - a sync every two hours costs a predictable number of requests per day, which you can compute in advance and compare against the allowance.

## Learn more {#learn-more}

- [Salesforce integration overview](https://zato.io/integrations/salesforce/) - Start here to see how teams build Salesforce integrations
- [The REST API from Python](https://zato.io/docs/dev/salesforce/rest-api-python.html) - Connected apps, credentials, connections and the self.salesforce API
- [Create and query records](https://zato.io/docs/dev/salesforce/create-and-query-records.html) - A REST channel that creates campaigns and reads them back
- [SOQL relationships and pagination](https://zato.io/docs/dev/salesforce/soql-relationships-and-pagination.html) - Parent traversal in the select list and the nextRecordsUrl loop
- [Upserts and external IDs](https://zato.io/docs/dev/salesforce/upserts-and-external-ids.html) - Idempotent create-or-update, keyed by your own identifiers
- [Updates and deletes](https://zato.io/docs/dev/salesforce/updates-and-deletes.html) - Diff-driven partial updates and deleting records by ID
- [A CRM sync, end to end](https://zato.io/docs/dev/salesforce/crm-sync-scenario.html) - A scheduled one-way sync with correlation keys and drift detection
