List users
GET /users
Description
Lists the users on your account, newest first, as a cursor-paginated collection. Each item carries the full user shape: the same object Get a user returns, with the team and active budget embedded inline.
What you get back depends on the token you send. An admin or service token lists every user in the account. A non-admin user token lists only itself, so the page holds a single item.
When to use
Use this endpoint to build a picker of who can send gifts, to reconcile budgets
across a team, or to resolve a set of sender and owner ids in one call
instead of fetching each user separately.
When you already have a user's id, fetch it directly with
Get a user. To read the user your own token authenticates as, use
Get the current user.
Parameters
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.
Query parameters
All three are optional, and this endpoint takes no filters: page and sort are the only controls it offers.
limit (integer · optional) is how many users to return per page. Defaults to
25, which is also the maximum. A larger value is clamped to 25 server-side
rather than rejected; a non-numeric, zero, or negative value is rejected with a
400.
after (string · uuid · optional) is the keyset pagination cursor. Pagination is
cursor-based, not page-numbered: to fetch the next page, pass the id of the
last user in the page you just received. Omit it for the first page. A
cursor that references no existing record is rejected with a 400. Stop paging
when the response's has_more is false.
sort (string · optional) is the field to order by, - prefix for
descending. Defaults to -created_at (newest first); created_at, email, and
-email are also accepted. Any other field is rejected with a 400.
Worked examples
The cURL version is the raw HTTP call; the JavaScript, Python, and Ruby versions are idiomatic equivalents. Each requests the first page ordered by email.
- cURL
- JavaScript
- Python
- Ruby
curl -X GET "https://api.andopen.co/users?sort=email" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
const url = new URL("https://api.andopen.co/users");
url.searchParams.set("sort", "email");
const response = await fetch(url, {
headers: {
Authorization: "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const page = await response.json();
for (const user of page.data) {
console.log(user.email, user.team?.name, user.budget?.remaining.amount);
}
import requests
response = requests.get(
"https://api.andopen.co/users",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
params={"sort": "email"},
)
response.raise_for_status()
page = response.json()
for user in page["data"]:
team = user["team"]
budget = user["budget"]
print(
user["email"],
team["name"] if team else None,
budget["remaining"]["amount"] if budget else None,
)
require "net/http"
require "json"
uri = URI("https://api.andopen.co/users")
uri.query = URI.encode_www_form(sort: "email")
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
page = JSON.parse(response.body)
page["data"].each do |user|
puts "#{user['email']} #{user.dig('team', 'name')} #{user.dig('budget', 'remaining', 'amount')}"
end
Paging through every user
To walk the whole collection, keep fetching while has_more is true, passing
the last user's id as the after cursor each time. The cURL version shows a
single follow-up request; the others loop until the last page.
- cURL
- JavaScript
- Python
- Ruby
curl -X GET "https://api.andopen.co/users?after=9f8e7d6c-5b4a-3210-fedc-ba9876543210" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
async function listAllUsers() {
const users = [];
let after;
do {
const url = new URL("https://api.andopen.co/users");
if (after) url.searchParams.set("after", after);
const response = await fetch(url, {
headers: {
Authorization: "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const page = await response.json();
users.push(...page.data);
const lastId = page.data.at(-1)?.id;
after = page.has_more && lastId ? lastId : undefined;
} while (after);
return users;
}
import requests
def list_all_users():
users = []
after = None
while True:
response = requests.get(
"https://api.andopen.co/users",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
params={"after": after} if after else {},
)
response.raise_for_status()
page = response.json()
users.extend(page["data"])
if not page["has_more"] or not page["data"]:
return users
after = page["data"][-1]["id"]
require "net/http"
require "json"
def list_all_users
users = []
after = nil
loop do
uri = URI("https://api.andopen.co/users")
uri.query = URI.encode_www_form(after: after) if after
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
page = JSON.parse(response.body)
users.concat(page["data"])
break users unless page["has_more"] && page["data"].any?
after = page["data"].last["id"]
end
end
Response shape
A successful call returns 200 OK with a page object: a data array of users
plus a boolean cursor flag. Paging is driven by has_more and the after
cursor.
| Field | Description |
|---|---|
data (array · required) | The users in this page. Each entry is a full user object, identical to what Get a user returns; see that page for the per-field detail of the team and budget. |
has_more (boolean · required) | Whether more users exist beyond this page. When true, fetch the next page by passing the last item's id as the after cursor; when false, you've reached the end. |
data.team (object · required · nullable) | Each user's team summary, embedded inline. |
data.budget (object · required · nullable) | Each user's active budget, also inline. See Get a user for the per-field detail of both. |
{
"data": [
{
"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"
}
}
}
],
"has_more": false
}
Per-field notes
- Every item is the full user, budget included. There is no trimmed list variant, so a single page gives you every figure you'd otherwise fetch one user at a time.
has_moredrives paging, not a total count. The response carries no total and nonextURL. Treathas_more: falseas the only signal to stop, and build the next request from the last item'sid.data[].teamanddata[].budgetare eachnullwhen unset. Anullbudget means no budget governs that user's sends, which reads differently from a budget whoseremainingis zero.- Budget figures are money objects in the smallest currency unit, and each user
carries their own
currency. Convert before you total a mixed-currency page.
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. |
| 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.
The ones you'll meet in practice:
400 validation_error: a query parameter was rejected, each naming the offending one inparam. Anaftercursor referencing no existing record isinvalid_reference. Alimitthat is not a positive integer and an unsupportedsortfield are bothinvalid_format.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 list 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.