Plan with AI

Get started →
Guides
Core Concepts5 min read

Handling Errors

A resilient integration treats an unsuccessful request as a decision point: inspect the HTTP status and response body, decide whether the failure is permanent or transient, and retry only when another attempt can reasonably succeed.

Workvivo API requests use a static API key created under API Keys & JWT Settings. Send it as a Bearer token, include the organisation's Workvivo-Id header, and request JSON responses with Accept: application/json. There is no separate sandbox or staging environment, so use bounded retries and test new error-handling logic carefully against production.

The Usual Error Envelope

Most validation, permission, and resource errors use the same top-level structure:

curl -X GET "https://api.workvivo.com/v1/users?take=not-a-number" \
  -H "Authorization: Bearer $WORKVIVO_TOKEN" \
  -H "Workvivo-Id: $WORKVIVO_ORG_ID" \
  -H "Accept: application/json"
{
    "status": "error",
    "data": null,
    "meta": {
        "errors": [
            {
                "path": "{take}",
                "message": "The take must be an integer."
            }
        ]
    }
}

The fields have distinct roles:

  • status identifies the response as an error.
  • data is null when no resource data is returned.
  • meta.errors is an array because one request can fail validation in several places.
  • Each error's path points to the relevant field or request location. The braces shown above are part of the returned value.
  • Each error's message explains that individual problem.

Read every item in meta.errors and use path to associate feedback with the relevant input. Treat message as human-readable text rather than a stable code for application logic.

Early Request Failures

Requests rejected before normal validation can use a simpler body. For example, an authentication failure returns a message, while the required organisation-header check returns an error:

{
    "message": "Unauthenticated."
}
{
    "error": "Missing header Workvivo-Id"
}

Use the HTTP status as the primary control signal, then parse the body defensively:

export async function readWorkvivoError(response) {
    let body = null;

    try {
        body = await response.json();
    } catch {
        // Some infrastructure failures may not include a JSON body.
    }

    const envelopeErrors = body?.meta?.errors;
    const errors =
        Array.isArray(envelopeErrors) && envelopeErrors.length > 0
            ? envelopeErrors
            : [
                  {
                      path: null,
                      message:
                          body?.message ??
                          body?.error ??
                          (response.statusText || `HTTP ${response.status}`),
                  },
              ];

    return {
        status: response.status,
        errors,
    };
}

Common Status Codes

Status What it means What to do
400 Bad Request The request is malformed or a field, parameter, or required header is invalid. Inspect every returned error, correct the request, and send it again only after it has changed.
401 Unauthorized The Bearer token is missing or the static API key is not accepted. Verify the key and Authorization header. Create or replace the key in API Keys & JWT Settings if needed.
403 Forbidden The API key is valid, but its app does not have permission or access for the operation. Correct the app permissions or requested access before trying again.
404 Not Found The requested resource or identifier could not be found in the organisation accessible to the app. Check the identifier and reconcile records that may have been removed.
409 Conflict The operation conflicts with the resource's current state, such as another operation already being in progress. Read or reconcile the current state. Do not repeat the unchanged request automatically.
422 Unprocessable Entity The request was understood but cannot be processed because of its input or current business condition. Correct the input or condition before trying again.
5xx Server Error Workvivo or upstream infrastructure could not complete an otherwise valid request. Retry with a bounded backoff. If failures continue, stop and surface the error for investigation.

A network error or client-side timeout has no HTTP status because no complete response arrived. Treat it as potentially transient, but remember that a timed-out write may still have completed on the server.

Decide What to Retry

Retry when the failure may disappear without changing the request:

  • A connection or other network failure.
  • A client-side timeout.
  • A 5xx response.

Do not automatically retry an unchanged 4xx request. Those responses indicate that the request, credentials, permissions, identifier, or current resource state needs attention first.

Retries are safest for reads. After a write times out or loses its connection, the outcome is uncertain: the server may have completed the operation even though the client did not receive the response. Reconcile the resource's current state before deciding whether to repeat that write.

Exponential Backoff with Jitter

Immediate retries can amplify a temporary failure. Exponential backoff increases the maximum delay after each failed attempt, while jitter chooses a random delay within that maximum so multiple workers do not retry in lockstep.

For attempt n, this example calculates:

  • Maximum delay: min(maxDelay, baseDelay × 2^(n - 1))
  • Actual delay: a random value from 0 to the maximum delay

Keep both the delay and the number of attempts bounded. The following modern Node.js helper retries GET requests after network errors, timeouts, and 5xx responses:

import { setTimeout as sleep } from 'node:timers/promises';

const baseUrl = 'https://api.workvivo.com/v1';
const token = process.env.WORKVIVO_TOKEN;
const organisationId = process.env.WORKVIVO_ORG_ID;

if (!token || !organisationId) {
    throw new Error('WORKVIVO_TOKEN and WORKVIVO_ORG_ID are required');
}

function isRetryableStatus(status) {
    return status >= 500 && status <= 599;
}

export async function workvivoGetWithRetry(path, options = {}) {
    const {
        maxAttempts = 4,
        baseDelayMs = 500,
        maxDelayMs = 8_000,
        timeoutMs = 10_000,
    } = options;

    for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
        let response;

        try {
            response = await fetch(`${baseUrl}${path}`, {
                method: 'GET',
                headers: {
                    Authorization: `Bearer ${token}`,
                    'Workvivo-Id': organisationId,
                    Accept: 'application/json',
                },
                signal: AbortSignal.timeout(timeoutMs),
            });
        } catch (error) {
            if (attempt === maxAttempts) {
                throw error;
            }
        }

        if (response?.ok) {
            return response;
        }

        if (
            response &&
            (!isRetryableStatus(response.status) || attempt === maxAttempts)
        ) {
            return response;
        }

        await response?.body?.cancel();

        const maximumDelayMs = Math.min(
            maxDelayMs,
            baseDelayMs * 2 ** (attempt - 1),
        );
        const delayMs = Math.random() * maximumDelayMs;

        console.warn(
            `Attempt ${attempt} failed; retrying in ${Math.round(delayMs)}ms`,
        );

        await sleep(delayMs);
    }

    throw new Error('Retry loop ended unexpectedly');
}

The helper returns the final HTTP response, including a non-retryable 4xx or a 5xx after the last attempt. The caller should check response.ok and use the error parser above before deciding how to surface or record the failure.

Production Checklist

  • Send Authorization, Workvivo-Id, and Accept on every request.
  • Branch on the HTTP status before inspecting optional body fields.
  • Capture every item in meta.errors, with the request method, path, status, and attempt number.
  • Never log API keys or other sensitive request data.
  • Retry only transient failures, with bounded attempts, exponential backoff, and jitter.
  • Reconcile writes with uncertain outcomes before repeating them.
  • Stop retrying and alert an operator when the attempt limit is reached.