> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Your First API Call

> A guided walkthrough of a substantive Volunteer API call — fetching a single user with full detail, understanding the difference between list-shape and detail-shape resources, and parsing the nested fields.

This page walks through a more substantive API call than the bare-minimum Quickstart — fetching a single user by ID and parsing the rich detail response that comes back. By the end, you'll understand the difference between **list-shape** and **detail-shape** resources, how nested fields like `participations` and `profile_field_values` work, and the practical pattern for handling responses where the spec and live API don't perfectly agree.

If you haven't completed the Quickstart, start there. This page assumes you have a working token and have made at least one successful request.

## What we're building

A function that fetches a single user with full detail — name, contact info, participation history, profile field responses — and parses the response into a clean object the rest of the integration can use.

```javascript JavaScript theme={null}
const user = await getUserDetail(userId);
console.log(`${user.fullName} has ${user.participations.length} participations`);
```

The pattern applies to most "fetch a single record" workflows across the API. Once you understand it for users, it transfers directly to projects, groups, and other resources.

## List shape vs. detail shape

A core pattern in the Volunteer API: **list endpoints return abbreviated resources; single-resource endpoints return fuller ones.**

For users specifically:

| Endpoint                   | Returns                                                                                          | Schema                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
| `GET /users` (list)        | An array of `UserResource` — the basic profile fields                                            | Each user has core identity fields                    |
| `GET /users/{id}` (single) | A `UserDetailResource` — the basic fields plus participation history and profile field responses | Includes everything in `UserResource` plus extensions |

This pattern is common in REST APIs designed around resource efficiency — list endpoints stay fast by returning only the fields most callers need, and single-resource endpoints provide the deep dive when needed.

<Note>
  **⚠️ Spec gap:** The OpenAPI spec's `UserDetailResource` schema only formally documents two properties (`participations` and `profile_field_values`). In practice, the live API likely also returns the base `UserResource` fields alongside these — `id`, `first_name`, `email`, etc. — making `UserDetailResource` a superset of `UserResource`, not a replacement for it.

  The code patterns on this page assume the superset shape. Confirm against the live API for production-critical workflows.
</Note>

***

## Step 1: get a user ID

Most "fetch single user" workflows start by knowing the ID. Common ways to get one:

| Source                  | Notes                                                                                                                                                       |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Previous list call      | `GET /users` returns each user's `id`. Capture and use it.                                                                                                  |
| Search by name or email | `GET /users?name_like=wayne` or `?email_like=bruce@wayne.example` returns matching users. Take the first match (or surface for disambiguation if multiple). |
| External system         | Some integrations get the VOMO user ID by some other path — a previous sync, a stored mapping, a webhook from another system.                               |

For this walkthrough, assume you have a user ID — say `12345`.

## Step 2: fetch the user

```bash cURL theme={null}
curl https://api.vomo.org/v1/users/12345 \
  -H "Authorization: Bearer $VOMO_API_TOKEN" \
  -H "Accept: application/json"
```

A typical successful response (truncated):

```json theme={null}
{
  "data": {
    "type": "user",
    "id": 12345,
    "first_name": "Bruce",
    "last_name": "Wayne",
    "full_name": "Bruce Wayne",
    "email": "bruce@wayne.example",
    "address": "1007 Mountain Drive, Gotham",
    "phone": "+15551234567",
    "birthday": "1972-02-19",
    "gender": "M",
    "updated_at": "2025-03-15T14:22:10Z",
    "created_at": "2024-08-01T09:00:00Z",
    "user_status": "VERIFIED",
    "membership_status": "ACCEPTED",
    "membership_role": "VOLUNTEER",
    "participations": [
      {
        "guests": 0,
        "checked_in_at": "2025-02-15T09:00:00Z",
        "checked_out_at": "2025-02-15T13:00:00Z",
        "signed_up_at": "2025-02-01T10:30:00Z",
        "hours": "4.00",
        "verified": true,
        "role": "Volunteer",
        "project_id": "789",
        "project_date_id": "1011"
      }
    ],
    "profile_field_values": [
      {
        "field_id": 42,
        "field_label": "T-shirt size",
        "value": "Medium"
      }
    ]
  }
}
```

Three things to notice about the response:

| Element                                                   | Description                                                                                     |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `data` is an object (not an array)                        | Single-resource endpoints return a single object inside `data`, not a single-element array      |
| Base user fields are present                              | `first_name`, `email`, etc. are at the top level of `data` alongside the detail-specific fields |
| Nested arrays for participations and profile field values | The detail response includes related records embedded in the main response                      |

<Warning>
  The OpenAPI spec documents `data` for `GET /users/{id}` as `type: array` (per audit finding #50), but the live API returns a single object as shown above. Code should expect an object, not an array. If your code is generated from the spec, it may need manual adjustment.
</Warning>

***

## Step 3: parse the response

A parsing helper that handles the differences between spec and live API:

```javascript JavaScript theme={null}
async function getUserDetail(userId) {
  const response = await fetch(
    `https://api.vomo.org/v1/users/${userId}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.VOMO_API_TOKEN}`,
        Accept: 'application/json',
      },
    }
  );

  if (response.status === 404) {
    return null; // User doesn't exist
  }

  if (!response.ok) {
    throw new Error(`Failed to fetch user ${userId}: ${response.status}`);
  }

  const result = await response.json();
  const raw = result.data;

  // Parse into a clean, integration-friendly shape
  return {
    id: raw.id,
    firstName: raw.first_name,
    lastName: raw.last_name,
    fullName: raw.full_name,
    email: raw.email,
    phone: raw.phone ?? null,
    address: raw.address ?? null,
    birthday: raw.birthday ? new Date(raw.birthday) : null,
    gender: raw.gender ?? null,
    createdAt: new Date(raw.created_at),
    updatedAt: new Date(raw.updated_at),
    userStatus: raw.user_status,
    membershipStatus: raw.membership_status,
    membershipRole: raw.membership_role,
    participations: (raw.participations ?? []).map(parseParticipation),
    profileFieldValues: (raw.profile_field_values ?? []).map(parseProfileFieldValue),
  };
}

function parseParticipation(raw) {
  return {
    guests: raw.guests ?? 0,
    checkedInAt: raw.checked_in_at ? new Date(raw.checked_in_at) : null,
    checkedOutAt: raw.checked_out_at ? new Date(raw.checked_out_at) : null,
    signedUpAt: raw.signed_up_at ? new Date(raw.signed_up_at) : null,
    // hours: typed integer in spec but live returns fractional values
    hours: typeof raw.hours === 'string' ? parseFloat(raw.hours) : raw.hours,
    verified: raw.verified ?? false,
    role: raw.role ?? null,
    // project_id and project_date_id: typed string in spec; parse to integer for joins
    projectId: raw.project_id ? parseInt(raw.project_id, 10) : null,
    projectDateId: raw.project_date_id ? parseInt(raw.project_date_id, 10) : null,
  };
}

function parseProfileFieldValue(raw) {
  return {
    fieldId: raw.field_id,
    fieldLabel: raw.field_label,
    value: raw.value,
  };
}
```

Three patterns this parser gets right:

| Pattern                                             | Why it matters                                                                      |
| --------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **`?? null` for optional fields**                   | Tolerates missing fields rather than crashing on `undefined.something`              |
| **`hours` parsing as float**                        | Spec types it integer but live returns `"4.00"` — see audit finding #40             |
| **`project_id` / `project_date_id` parsing to int** | Spec types them string but they're IDs that join to integer-typed records elsewhere |
| **`new Date()` wrapping for ISO strings**           | Native Date objects are easier to work with than ISO strings in downstream code     |

***

## Step 4: handle the four-case shape

When fetching a single resource, four cases can occur:

| HTTP status        | What it means                   | Code response                               |
| ------------------ | ------------------------------- | ------------------------------------------- |
| `200 OK`           | User found and returned         | Parse and return                            |
| `404 Not Found`    | User with that ID doesn't exist | Return `null`; caller decides how to handle |
| `401 Unauthorized` | Token invalid                   | Don't retry; alert                          |
| `5xx`              | Transient server error          | Retry with backoff                          |

```javascript JavaScript theme={null}
async function getUserDetailRobust(userId) {
  try {
    const response = await fetch(
      `https://api.vomo.org/v1/users/${userId}`,
      { headers: { Authorization: `Bearer ${token}` } }
    );

    switch (true) {
      case response.ok:
        return parseUser(await response.json());

      case response.status === 404:
        return null;

      case response.status === 401:
        await alertCredentialIssue(userId);
        throw new AuthError('VOMO token is invalid');

      case response.status === 403:
        throw new ForbiddenError(`No permission for user ${userId}`);

      case response.status >= 500:
        // Transient — caller should retry with backoff
        throw new TransientError(`Server error: ${response.status}`);

      default:
        throw new Error(`Unexpected status: ${response.status}`);
    }
  } catch (err) {
    if (err instanceof AuthError || err instanceof ForbiddenError) {
      throw err;
    }
    if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
      throw new TransientError(`Network error: ${err.message}`);
    }
    throw err;
  }
}
```

The classification matters because retry logic depends on it. See [Error Handling](/volunteer/error-handling) for the broader pattern.

***

## Step 5: do something useful with the parsed user

A few patterns for what to do once you have the user object:

### Calculate total hours volunteered

```javascript JavaScript theme={null}
const user = await getUserDetail(12345);

const totalHours = user.participations.reduce((sum, p) => sum + (p.hours ?? 0), 0);
console.log(`${user.fullName} has volunteered ${totalHours} hours total`);
```

The `hours` parsing as float (rather than integer) matters here — a user with three participations of 1.5, 2.0, and 0.75 hours sums to 4.25, not the truncated integer 4.

### Find the user's most recent participation

```javascript JavaScript theme={null}
const user = await getUserDetail(12345);

const sortedParticipations = user.participations
  .filter((p) => p.checkedOutAt)
  .sort((a, b) => b.checkedOutAt - a.checkedOutAt);

const mostRecent = sortedParticipations[0];
if (mostRecent) {
  console.log(`Last volunteered: ${mostRecent.checkedOutAt.toISOString()}`);
}
```

This sorts the embedded participations by `checked_out_at` descending. For users with many participations, this is more efficient than a separate query.

### Display profile field values

```javascript JavaScript theme={null}
function getProfileFieldValue(user, label) {
  return user.profileFieldValues.find((v) => v.fieldLabel === label)?.value ?? null;
}

const tshirtSize = getProfileFieldValue(user, 'T-shirt size');
const dietaryRestrictions = getProfileFieldValue(user, 'Dietary restrictions');
```

The profile field values are keyed by label (the field's display name) — useful for displaying or filtering on custom data the customer has set up.

***

## Patterns for other detail-shape resources

The same pattern applies to other single-resource fetches in the Volunteer API:

### `GET /projects/{id}` returns `ProjectDetailResource`

Returns project metadata plus embedded data like dates, owners, and other relationships. Parse similarly to user detail.

### `GET /projects/date/{id}` returns Project Date detail

Returns a specific occurrence of a project with the embedded participations for that date.

### `GET /groups/{id}` returns a single group

Returns the group with its metadata. Members are fetched separately via `GET /groups/{id}/members`.

### `GET /organizations/{id}` returns organization detail

Returns the organization with its full address, logo, contact info, and parent/child organization relationships.

<Note>
  **⚠️ Spec gap:** Several detail-shape endpoints — `GET /organizations`, `GET /organizations/{id}`, `GET /campaigns`, `GET /campaigns/{id}`, `GET /projects/{id}`, `PUT /projects/{id}`, `GET /projects/date/{id}` — define their `200` responses with an empty `schema: {}` in the spec (per audit finding #4). The response shape is conveyed only through inline examples. Build parsers from the actual response shapes, treating the spec's inline examples as a guide.
</Note>

***

## What about creating or updating a user?

`POST /users` creates or updates a user (it's an upsert — see audit finding #47). The request body shape:

```javascript JavaScript theme={null}
async function upsertUser(userData) {
  const response = await fetch('https://api.vomo.org/v1/users', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      first_name: userData.firstName,
      last_name: userData.lastName,
      email: userData.email,
      phone: userData.phone,
      birthday: userData.birthday,
      gender: userData.gender,
      role: userData.role,
      // Note: snake_case property names in the request body
    }),
  });

  // 200 = updated existing user; 201 = created new user
  if (!response.ok) throw new Error(`Upsert failed: ${response.status}`);

  return parseUser(await response.json());
}
```

Notable points:

* The request body uses `snake_case` (consistent with the API's overall casing).
* The response distinguishes between `200` (updated) and `201` (created) — code can detect which behavior occurred.
* Matching for the upsert is typically done by `email` — submitting a user with an existing email updates that user; submitting with a new email creates one.

See [Create or Update a User](/volunteer/workflows/create-or-update-a-user) for the full upsert workflow.

***

## What's now in your toolkit

After this walkthrough, you have:

* A working "fetch user by ID" pattern with the detail-shape response handled correctly
* A parser that translates the API's snake\_case fields and audit-flagged type quirks into a clean integration-friendly shape
* An error-handling pattern for the four-case shape (200/404/401/5xx)
* The pattern transferable to other single-resource fetches (`GET /projects/{id}`, `GET /groups/{id}`, etc.)
* The upsert pattern for `POST /users`

This is enough to build most read-oriented integrations. The next step is wiring up multi-record reads with pagination, then choosing the right error-handling strategy.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Error Handling" icon="triangle-exclamation" href="/volunteer/error-handling">
    The full error-classification pattern for production-grade integration code.
  </Card>

  <Card title="Pagination" icon="list" href="/volunteer/pagination">
    The pattern for reading more than one page of users.
  </Card>

  <Card title="The Volunteer Data Model" icon="diagram-project" href="/volunteer/concepts/data-model">
    What other resources are available and how they relate.
  </Card>

  <Card title="Common Workflows" icon="route" href="/volunteer/workflows/list-users-with-filters">
    Recipes for the common tasks built on the patterns introduced here.
  </Card>
</CardGroup>
