Skip to main content
The Vendaze API uses standard HTTP status codes and returns a consistent error format on every failure.

Error format

{
  "error": {
    "code": "not_found",
    "message": "Person not found.",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}
FieldDescription
codeMachine-readable error code. Use in error handling logic.
messageHuman-readable description for developers. Do not show to end users.
request_idUnique request ID. Include when contacting support.
A 2xx response never contains error. A non-2xx response always contains error. The request_id is also returned in the X-Request-ID response header on every request, including successful ones.

HTTP status codes

StatusMeaning
200Success. Resource read or updated.
201Created successfully.
204No content. Resource deleted. Empty body.
400Malformed request or missing required parameter.
401Unauthenticated. Token missing, invalid, or expired.
403Forbidden. Valid token but insufficient scope or denied operation.
404Not found. Resource does not exist, was deleted, or belongs to another workspace.
409Conflict. The request cannot be completed due to a conflict with existing data.
422Validation failed. See the fields object.
429Rate limit exceeded.
500Internal error. Something went wrong on the server.

Error code reference

Returned in three situations:
  • Missing or invalid header: "Missing or invalid Authorization header." the Authorization: Bearer <token> header is absent or malformed.
  • Invalid token: "Invalid or malformed token." the token signature failed verification or the token is not a valid JWT.
  • Missing claims: "Token is missing required claims." the token is structurally valid but is missing required internal claims (workspace_id, client_id, or vendaze_scopes). This should not happen with tokens issued by the Vendaze OAuth flow.
"Access token expired. Refresh using the refresh token."The access token has expired. Access tokens are valid for 1 hour. Use your refresh_token to obtain a new one via POST /oauth/token with grant_type=refresh_token. If the refresh token has also expired, the user must re-authorize.
"This endpoint requires the '{scope}' scope."The token does not have the required scope for this endpoint. The {scope} placeholder in the message is the exact scope that is missing (for example, "This endpoint requires the 'people:write' scope."). The user authorized your app with a set of scopes that does not include what this operation requires. You need to request a new authorization with the correct scopes.
The token is valid and has the right scope, but the operation was denied by a database-level permission check. The message varies by context:
  • Create: "You do not have permission to create this {entity}. Check that all fields are within your access level."
  • Update: "You do not have permission to update this {entity} or one of the provided values is not allowed for your access level."
  • Delete: "You do not have permission to delete this {entity}."
Common cause: setting owner_user_id to a user other than yourself when your role does not allow it.
"{Entity} not found." for example, "Person not found." or "Deal not found."The requested resource does not exist, has been soft-deleted, or belongs to a different workspace. The API does not distinguish between these cases to avoid leaking information about other workspaces.
"A record with this value already exists."The request conflicts with existing data. The response also includes a fields object identifying the conflicting field. Common cause: attempting to register an app with an email that is already in use.
"Validation failed."Input failed validation. The response includes a fields object mapping each invalid field to a description of the problem. Fix the fields listed and retry. See Validation errors for the full response shape.
Returned in two situations:
  • Malformed body: "Request body must be valid JSON." the request body could not be parsed as JSON.
  • Empty update: "No fields provided for update." a PATCH request was sent with no fields to modify.
"Rate limit exceeded. Try again in {N} seconds."Too many requests. The {N} in the message is the exact number of seconds to wait. The same value is also in the Retry-After response header. See Rate Limits for the full limits by method and endpoint type.
Returned in two situations:
  • Unexpected server error: "An unexpected error occurred." something failed on the server. Retry with exponential backoff. If the error persists, contact support with the request_id.
  • Partial update: "{Entity} updated but associations failed. Retry the associations." the main record was saved but the association update (tags, lists, deals, or custom fields) failed. The record itself is consistent. Retry only the association fields.

OAuth error codes

These codes are returned exclusively by the OAuth endpoints (/oauth/authorize, /oauth/token, /oauth/revoke, /v1/auth/register-app, /v1/auth/rotate-app).
"Invalid client credentials." the client_id or client_secret is incorrect. Verify your credentials. If you suspect the client_secret has been compromised, rotate it via POST /v1/auth/rotate-app.Also returned as 401 from /oauth/authorize when the client_id does not exist or the app is not active, and from /v1/auth/rotate-app when the email sent does not match the one registered with the app.
"The provided authorization grant is invalid, expired, or does not match."The authorization code is invalid, expired, or has already been used. Authorization codes expire in 10 minutes and are single-use. Also returned when a refresh_token is invalid or expired. In either case, the user must go through the full authorization flow again.
A required OAuth parameter is missing or invalid. The message identifies the specific parameter. Examples:
  • "Missing required parameter: client_id."
  • "Missing required parameter: redirect_uri."
  • "redirect_uri does not match any registered URI."
  • "Missing required parameter: token." (revoke)
  • "token must be a valid access_token. refresh_tokens are not accepted." (revoke)
"grant_type must be \"authorization_code\" or \"refresh_token\"."The grant_type field sent to /oauth/token is not one of the two supported values.
"response_type must be \"code\"."The response_type parameter sent to /oauth/authorize is not "code".

Validation errors

When validation fails, the response includes a fields object mapping each field to its problem:
{
  "error": {
    "code": "validation_error",
    "message": "Validation failed.",
    "fields": {
      "full_name": "String must contain at most 70 character(s).",
      "forecast_date": "Invalid ISO 8601 date format."
    },
    "request_id": "uuid"
  }
}

The request_id field

Every request, successful or not, gets a unique request_id. It appears in:
  • The error.request_id field on error responses
  • The X-Request-ID response header on all responses
When contacting support about an issue, always include the request_id. It allows the support team to locate the exact request in the logs and diagnose what happened.

Handling errors in production

Recoverable vs non-recoverable errors

Not all errors should be retried. Retrying a non-recoverable error wastes resources and delays surfacing the real problem. Recoverable errors are transient. Retry with backoff:
StatusCodeStrategy
429rate_limit_exceededWait the number of seconds in Retry-After, then retry.
500internal_errorRetry with exponential backoff (1s, 2s, 4s). Give up after 3 attempts.
Non-recoverable errors indicate a problem with the request or credentials. Do not retry automatically:
StatusCodeWhat to do
400bad_requestFix the request. The body is malformed or the update has no fields.
400invalid_grantThe code or refresh token is invalid. Redirect the user through the OAuth flow.
400invalid_requestA required OAuth parameter is missing or invalid. Fix the request.
400unsupported_grant_typeUse authorization_code or refresh_token.
401token_expiredRefresh the access token, then retry the original request.
401unauthorizedCheck that the token is present and correctly formatted.
401invalid_clientCheck your client_id and client_secret. Do not retry automatically.
403insufficient_scopeRequest a new authorization with the required scopes.
403forbiddenFix the request. The operation or field values are not allowed for your access level.
404not_foundThe resource does not exist. Do not retry.
409conflictResolve the conflict before retrying.
422validation_errorFix the fields listed in fields. Do not retry with the same payload.
async function apiCall(url, options) {
  const res = await fetch(url, options);

  if (!res.ok) {
    const { error } = await res.json();

    switch (error.code) {
      case 'token_expired':
        // Refresh the token and retry once
        await refreshAccessToken();
        return apiCall(url, options);

      case 'invalid_grant':
        // Refresh token is invalid or expired. User must re-authorize.
        throw new Error('Authorization expired. User must re-authorize.');

      case 'invalid_client':
        // Credentials are wrong. Do not retry.
        throw new Error('Invalid client credentials. Check client_id and client_secret.');

      case 'rate_limit_exceeded': {
        const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60');
        await new Promise((r) => setTimeout(r, retryAfter * 1000));
        return apiCall(url, options);
      }

      case 'not_found':
        return null;

      case 'internal_error':
        // Log the request_id for support, then surface to the caller
        console.error(`Server error. request_id: ${error.request_id}`);
        throw new Error(`Server error (request_id: ${error.request_id})`);

      default:
        throw new Error(`${error.code}: ${error.message} (request_id: ${error.request_id})`);
    }
  }

  return res.json();
}