Get a user
GET /users/{id}
Description
Retrieves a single user by their id, with their team and active budget
embedded inline. A user is someone on your account who can send gifts: the
sender on a gift request and the owner of a campaign are both users.
What you can read depends on the token you send. An admin or service token may read any user in the account. A non-admin user token may read only itself.
When to use
Use this endpoint when you have a user's id from somewhere else in the API,
typically a gift request's sender or a campaign's owner, and you need more
than the { id, name, email } summary those carry. Budget is the usual reason:
checking what a sender has left before you queue more gifts against them.
To read the user your own token authenticates as, use
Get the current user, which needs no id. To discover users in the
first place, use List users.
Parameters
Path
id (string · uuid · required) is the user to retrieve. An id that matches no user
you're allowed to read returns 404, whether or not that user exists. Existence
is not enumerable, so a 404 here means "not yours or not there" and never
distinguishes the two.
Headers
AndOpen-API-Version (string · required) is the API version this request
targets. Always send 2026-05. Requests authenticate with a bearer token; see
Authentication for how to present it and
Environments for the regional base URL to send it to.
Worked examples
The cURL version is the raw HTTP call; the JavaScript, Python, and Ruby versions
are idiomatic equivalents. Replace the path id with the user you want.
- cURL
- JavaScript
- Python
- Ruby
curl -X GET "https://api.andopen.co/users/9f8e7d6c-5b4a-3210-fedc-ba9876543210" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
const userId = "9f8e7d6c-5b4a-3210-fedc-ba9876543210";
const response = await fetch(`https://api.andopen.co/users/${userId}`, {
headers: {
Authorization: "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
console.log(user.name, user.team?.name);
if (user.budget) {
console.log(user.budget.remaining.amount, user.budget.remaining.currency);
}
import requests
user_id = "9f8e7d6c-5b4a-3210-fedc-ba9876543210"
response = requests.get(
f"https://api.andopen.co/users/{user_id}",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
)
response.raise_for_status()
user = response.json()
team = user["team"]
print(user["name"], team["name"] if team else None)
budget = user["budget"]
if budget:
print(budget["remaining"]["amount"], budget["remaining"]["currency"])
require "net/http"
require "json"
user_id = "9f8e7d6c-5b4a-3210-fedc-ba9876543210"
uri = URI("https://api.andopen.co/users/#{user_id}")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer <api_key>"
request["AndOpen-API-Version"] = "2026-05"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
response.value # raises on a non-2xx response
user = JSON.parse(response.body)
puts "#{user['name']} #{user.dig('team', 'name')}"
budget = user["budget"]
puts "#{budget['remaining']['amount']} #{budget['remaining']['currency']}" if budget
Response shape
A successful call returns 200 OK with the user as a flat top-level JSON
object. The team and budget are returned inline rather than as references you
have to fetch separately.
| Field | Description |
|---|---|
name (string · required) | The user's full name. |
email (string · email · required) | Their email address. Both this and the name are account-internal, so treat them as staff data and keep them away from recipient-facing surfaces. |
team (object · required · nullable) | The user's team, embedded as an { id, name } summary, or null when they belong to none. There is no team endpoint, so this summary is the only place the team's name appears. |
budget (object · required · nullable) | The user's single active budget for the current period, or null when they have none. Its time_unit is one of monthly, quarterly, annually, and it carries four money figures: total, spent, committed, and remaining. |
{
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Dana Okafor",
"email": "dana@acme.example",
"team": {
"id": "1122aabb-ccdd-eeff-0011-223344556677",
"name": "Sales EMEA"
},
"budget": {
"id": "7c1f5e2a-9b3d-4f8a-b1c2-0d4e6f8a1b2c",
"time_unit": "monthly",
"total": {
"amount": 500000,
"currency": "EUR"
},
"spent": {
"amount": 180000,
"currency": "EUR"
},
"committed": {
"amount": 60000,
"currency": "EUR"
},
"remaining": {
"amount": 260000,
"currency": "EUR"
}
}
}
Per-field notes
budget.total.amount(integer · required) shows the shape all four figures share: a whole number in the smallest unit of the budget'scurrency, never a decimal. Atotalof{ "amount": 500000, "currency": "EUR" }is a €5,000.00 budget, and2500would be €25.00.remainingistotallessspentandcommitted, floored at zero.spentis gifting the recipient has redeemed;committedis gifting already drawn against the total but not yet redeemed. A sender with unredeemed gifts in flight therefore has less to spend thantotallessspentsuggests, which is the figure to check before you queue more.- All four figures share the budget's own currency. Compare them to each other freely, and convert before comparing across users on different currencies.
budgetisnullfor a user with no active budget, and that is not the same as a zero budget. Anullmeans no budget governs their sends; a zeroremainingmeans one does and it is exhausted.teamandbudgetare read-only here. This endpoint reads users; it has no write counterpart in2026-05.
Error cases
Every failure uses the shared error model: a top-level errors array of
objects carrying type, code, message, and (for field-level problems)
param. See Errors for the full type/code taxonomy and how
to handle each category; the codes below are the ones this endpoint produces.
| Status | Meaning |
|---|---|
| 400 | The request is malformed or violates a precondition. Typical triggers are a missing or unsupported `AndOpen-API-Version` header, a request body that is not valid JSON, or pagination parameters that fail server-side validation (`limit` non-numeric or non-positive, `after` referencing a record that does not exist). |
| 401 | Authentication failed: missing or invalid bearer token. |
| 403 | Authorized but not permitted to access this resource. |
| 404 | The requested resource does not exist. |
| 429 | Too many requests. Rate limit exceeded. |
| 500 | Internal server error. |
| 503 | Service unavailable. The account may be under maintenance. Check the `Retry-After` header. |
Every error carries a type, one of: validation_error, authentication_error, authorization_error, not_found_error, rate_limit_error, api_error.
A read takes no body, so there's nothing to validate. The failure you'll handle most is a missing or out-of-scope user:
404 not_found_error: no user with thatidis visible to your token (not_found). A non-admin token reading anyone but itself lands here.400 api_error:unsupported_api_versionwhen theAndOpen-API-Versionheader is missing or unsupported (returned before authentication).401 authentication_error: the bearer token is missing, invalid, or expired (unauthorized).403 authorization_error: the token is valid but not permitted to read users (forbidden).429 rate_limit_error: too many requests; back off and retry (rate_limited). TheRateLimit-*response headers tell you the budget and reset time.500/503 api_error: a fault on our side; rare, and safe to retry after a short delay (honorRetry-Afteron a503).