# Errors and troubleshooting

This page is for developers handling failed VBAPI calls. It explains the error body, lists the common causes and fixes for each status code, shows which errors to retry and how, and lists what support needs from you.

The examples on this page only read data.

## What an error looks like

Errors use the same envelope as successful responses. `data` is `null`, and `error` holds a problem-details object:

| Field | Meaning |
|  --- | --- |
| `type` | A link for the status code, for example `https://httpstatuses.com/404`. |
| `title` | A short summary of the kind of error, for example `Not Found` or `Invalid Model State`. |
| `status` | The HTTP status code. |
| `detail` | What went wrong in this case. Read this first. |
| `instance` | The path that was called. |


`debug.activityID` identifies the request in VBA's logs.

Example `404` response (illustrative; field values are fake):

```json
{
  "data": null,
  "error": {
    "type": "https://httpstatuses.com/404",
    "title": "Not Found",
    "status": 404,
    "detail": "County not found for conditions: County_Code equals DOCS-CO9",
    "instance": "/counties/DOCS-CO9"
  },
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

When the request body fails validation, the title is `Invalid Model State` and `detail` is a JSON string that lists each field and its problems. Example `400` response (illustrative; field values are fake):

```json
{
  "data": null,
  "error": {
    "type": "https://httpstatuses.com/400",
    "title": "Invalid Model State",
    "status": 400,
    "detail": "{\"Subscriber_ID\":[\"Value cannot exceed 9 characters.\"]}",
    "instance": "/subscribers/DOCS-SUBSCRIBER1/members"
  },
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

Not every error has this body. A rejected token returns `401` with only `{"message":"Unauthorized"}`, and a missing or wrong API key returns `403` with `{"message":"Forbidden"}`; both come from the API gateway, not VBAPI. The body of a `429` is not documented. Do not assume every error body parses as the envelope; fall back to the HTTP status. Error bodies spell the support field `debug.ActivityID` (successful responses use `activityID`), so read it without regard to case.

## Status codes, causes, and fixes

| Status | Common causes | Fix | Retry? |
|  --- | --- | --- | --- |
| `400` `Invalid Model State` | A required field is missing (for example `subscriber_ID` on a member), a value is longer than the field's `maxLength`, a value has the wrong type, or the JSON is malformed. | Compare the body with the schema in the API reference. `detail` names each field. | No. Fix the request. |
| `400` `Invalid Parameter Value` | A bad `sortBy` (no `:asc` or `:desc`, or a field the record does not have), a create that sets a key VBA assigns ("Entity attribute ... cannot be set when creating."), or a login call without a valid Basic `Authorization` header. | Correct the parameter or header. | No. |
| `400` `Invalid Data Sent` | The database refused the change. For example, a value refers to a record that does not exist ("No matching records found in the 'Provider' table"), a delete is blocked by dependent records ("There are dependent records in the 'Claim' table"), or the key already exists. | Create or fix the referenced record first, or remove the dependent records. | No. |
| `400` `Invalid Page Count` | A Search request with `pageCount` over 750 ("For API calls, PageCount cannot exceed 750 records."). | Use a `pageCount` of 750 or less and page through the results. | No. |
| `401` | Wrong username or password at login, or a missing, expired, or malformed ID token on a data call. | Log in again and send `Authorization: Bearer <idToken>`. | Once, after logging in again. |
| `403` | A missing or wrong `x-api-key`. The body is `{"message":"Forbidden"}`. | Send your API key in `x-api-key` on every call. | No. |
| `404` | No record has the key in the path. `detail` names the table and key. | Check the key values and their order in the path. A `404` can be a normal "does not exist" answer. | No. |
| `422` `Invalid Entity` | A Search request with a selected field that is not in the SearchConfig, an empty `selectedFields`, or an unknown `criteria_Key`. | Check the request against [`GET /search-configs/{searchConfigId}`](/openapi/vbasoftware/vbasoftware.api/search-config/getsearchconfig). | No. |
| `429` | More than 25 requests per second sustained, or 50 in a burst, on your API key. | Slow down. See [Rate limits and performance](/developer-portal/concepts/rate-limits-and-performance). | Yes, with backoff. |
| `500` | An unexpected error inside VBAPI. One known cause: `page=0` on a list operation ("The offset specified in a OFFSET clause may not be negative"); list pages start at 1. | Check the request, then keep the `debug.ActivityID` and contact support. | Not automatically. |
| `502`, `503`, `504` | The service or gateway is temporarily unavailable or timed out. | Wait and try again. | Yes, with backoff. |


For the first call, these are the usual problems:

| What you see | Likely cause | Fix |
|  --- | --- | --- |
| `401` from `POST /user-authentication` | Wrong username or password, or the wrong client ID, client code, or database for that user. | Check each value with whoever issued your credentials. |
| `challengeName: NEW_PASSWORD_REQUIRED` instead of tokens | The user still has a temporary password. | Set a permanent password with [`POST /change-temporary-password`](/openapi/vbasoftware/vbasoftware.api/user/userchangetemppassword), then log in again. |
| `401` on a data call | The ID token is missing, expired, or not sent as `Bearer`. | Log in again, and send `Authorization: Bearer <idToken>`. |
| `403` with `{"message":"Forbidden"}` on every call | The `x-api-key` header is missing or wrong. | Send your API key in `x-api-key` on every request, login included. |
| `429` | More than 25 requests per second (burst 50) on your API key. | Slow down and retry with backoff. The samples do this for you. |
| Login or data calls fail for a database you expect to use | The `vbasoftware-database` value does not match a database your user can reach. | Use the database name you were given, exactly as issued. |


## Handle errors in code

The shared client turns every error status into an exception that carries the status, the `error` object, and the activity ID. Catch it where you can do something useful, for example treating a `404` as "not found":

C#
```csharp
    // Returns the group, or null if it does not exist. Other errors are logged and re-thrown.
    static async Task<JsonNode?> GetGroupOrNullAsync(VbapiClient api, string groupId)
    {
        try
        {
            return (await api.GetAsync($"/groups/{groupId}")).Data;
        }
        catch (VbapiException ex) when (ex.Status == HttpStatusCode.NotFound)
        {
            return null;
        }
        catch (VbapiException ex)
        {
            // ex.Problem holds type, title, status, detail, and instance
            Console.Error.WriteLine(
                $"GET /groups/{groupId} failed: {(int)ex.Status} {ex.Problem?["title"]}: {ex.Problem?["detail"]} (activityID={ex.ActivityId})");
            throw;
        }
    }
```

Python
```python
def get_group_or_none(api, group_id):
    """Return the group, or None if it does not exist. Other errors are logged and re-raised."""
    try:
        return api.get(f"/groups/{group_id}").data
    except VbapiError as error:
        if error.status == 404:
            return None
        # error.problem holds type, title, status, detail, and instance
        log.error("GET /groups/%s failed: %s %s: %s (activityID=%s)",
                  group_id, error.status, error.problem.get("title"), error.problem.get("detail"), error.activity_id)
        raise
```

TypeScript
```typescript
/** Returns the group, or undefined if it does not exist. Other errors are logged and re-thrown. */
async function getGroupOrUndefined(api: VbapiClient, groupId: string) {
  try {
    return (await api.get(`/groups/${groupId}`)).data;
  } catch (error) {
    if (!(error instanceof VbapiError)) throw error;
    if (error.status === 404) return undefined;
    // error.problem holds type, title, status, detail, and instance
    console.error(
      `GET /groups/${groupId} failed: ${error.status} ${error.problem?.title}: ${error.problem?.detail} (activityID=${error.activityId})`,
    );
    throw error;
  }
}
```

## Retry only what can succeed later

Retry `429`, `502`, `503`, and `504`, with exponential backoff and random jitter so that many clients do not retry at the same moment. On `401`, log in again once and retry. Do not retry other `4xx` errors: the same request fails the same way until you change it.

This is the request method of the shared client. It retries those four statuses up to five times, waiting about 1, 2, 4, 8, and 16 seconds plus up to a second of jitter, and logs in again once on `401`:

C#
```csharp
    public async Task<VbapiResponse> SendAsync(HttpMethod method, string pathAndQuery, object? body = null)
    {
        var retryable = method == HttpMethod.Get ? RetryableRead : RetryableWrite;
        var reauthenticated = false;
        for (var attempt = 0; ; attempt++)
        {
            if (_idToken is null || DateTimeOffset.UtcNow >= _expiresAt)
                await AuthenticateAsync();

            using var request = new HttpRequestMessage(method, $"{_baseUrl}{pathAndQuery}");
            request.Headers.Add("x-api-key", _apiKey);
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _idToken);
            request.Headers.Add("vbasoftware-database", _database);
            request.Headers.UserAgent.ParseAdd(_userAgent); // recommended: names your integration in VBA's logs
            if (body is not null)
                request.Content = JsonContent.Create(body);

            using var response = await _http.SendAsync(request);
            if (response.StatusCode == HttpStatusCode.Unauthorized && !reauthenticated)
            {
                _idToken = null;
                reauthenticated = true;
                continue;
            }
            if (retryable.Contains(response.StatusCode) && attempt < 5)
            {
                var delay = Math.Min(30, Math.Pow(2, attempt)) + Random.Shared.NextDouble(); // backoff with jitter
                await Task.Delay(TimeSpan.FromSeconds(delay));
                continue;
            }

            var payload = await ReadJsonAsync(response);
            if ((int)response.StatusCode >= 400)
                throw new VbapiException(response.StatusCode, payload?["error"], payload?["debug"]?["activityID"]?.GetValue<string>());
            return new VbapiResponse(response.StatusCode, payload, response.Headers);
        }
    }
```

Python
```python
    def request(self, method, path, params=None, body=None):
        """Call VBAPI with the required headers. Retries throttling and gateway errors, re-authenticates once on 401."""
        retryable = RETRYABLE_READ if method == "GET" else RETRYABLE_WRITE
        reauthenticated = False
        for attempt in range(6):
            if not self._id_token or time.time() >= self._expires_at:
                self.authenticate()
            response = self.session.request(
                method,
                f"{self.base_url}{path}",
                params=params,
                json=body,
                headers={
                    "x-api-key": self.api_key,
                    "Authorization": f"Bearer {self._id_token}",
                    "vbasoftware-database": self.database,
                    "User-Agent": self.user_agent,  # recommended: names your integration in VBA's logs
                },
                timeout=120,
            )
            if response.status_code == 401 and not reauthenticated:
                self._id_token = None
                reauthenticated = True
                continue
            if response.status_code in retryable and attempt < 5:
                time.sleep(min(30, 2 ** attempt) + random.uniform(0, 1))  # exponential backoff with jitter
                continue
            payload = self._json(response)
            if response.status_code >= 400:
                raise VbapiError(response.status_code, self._error(payload), self._activity_id(payload))
            return VbapiResponse(response.status_code, payload, response.headers)
        raise RuntimeError("unreachable")
```

TypeScript
```typescript
  async request<T = any>(method: Method, path: string, options: { params?: object; body?: unknown } = {}): Promise<VbapiResponse<T>> {
    const retryable = method.toUpperCase() === "GET" ? RETRYABLE_READ : RETRYABLE_WRITE;
    let reauthenticated = false;
    for (let attempt = 0; ; attempt++) {
      if (!this.idToken || Date.now() >= this.expiresAt) await this.authenticate();

      let response: AxiosResponse;
      try {
        response = await axios.request({
          method,
          url: `${this.baseUrl}${path}`,
          params: options.params,
          data: options.body,
          headers: {
            "x-api-key": this.apiKey,
            Authorization: `Bearer ${this.idToken}`,
            "vbasoftware-database": this.database,
            "User-Agent": this.userAgent, // recommended: names your integration in VBA's logs
          },
          timeout: 120_000,
        });
      } catch (error) {
        const failed = (error as AxiosError).response;
        if (!failed) throw error;
        if (failed.status === 401 && !reauthenticated) {
          this.idToken = undefined;
          reauthenticated = true;
          continue;
        }
        if (retryable.has(failed.status) && attempt < 5) {
          const delay = Math.min(30, 2 ** attempt) + Math.random(); // exponential backoff with jitter
          await new Promise((resolve) => setTimeout(resolve, delay * 1000));
          continue;
        }
        const payload: any = failed.data;
        throw new VbapiError(failed.status, field(payload, "error"), activityIdOf(payload));
      }
      return new VbapiResponse<T>(response.status, response.data, response.headers);
    }
  }
```

Be careful when a create or an advanced operation times out or returns `502` or `504`: the work may have finished even though you did not get the response. Before you send it again, read the record to check whether it already exists.

## Log the activityID

Every enveloped response, success or failure, carries `debug.activityID`. It is the one value that lets VBA find your request in its logs, so log it with every failure, next to the method, path, status, and `error.detail`. That is usually all the monitoring an integration needs to make support fast. Match the field name without regard to case when you read it, so that a change in capitalization does not lose it.

Contacting support
Send VBA support:

- the `debug.activityID` from the response
- the date and time of the call, with time zone
- the method and path, for example `POST /claim-batches`
- the full `error` object from the response
- the request body, with passwords, tokens, and personal data removed


Never send your password, API key, or tokens. See [Support and escalation](/developer-portal/help/support).

## Related

- [Requests and responses](/developer-portal/concepts/requests-and-responses): status codes for successful calls and the envelope.
- [Authentication and tokens](/developer-portal/concepts/authentication): login failures and challenges.
- [Rate limits and performance](/developer-portal/concepts/rate-limits-and-performance): avoiding `429`.