Feature Request: Search users by profile details on GET /v1/users
P
Perry Bunn
## Summary
Add server-side search to the users collection endpoint so API consumers can find users by
profile details — name, email, phone, address fields, and custom user properties — without
downloading the full member roster.
## Current behavior
GET /v1/users
accepts these query parameters:| Parameter | Type | Behavior |
|-----------------|---------|-------------------------------------------------------|
|
_limit
| integer | Page size, default 20, max 100 ||
_offset
| integer | Pagination offset ||
_since
| integer | Unix epoch seconds; users modified after this time ||
user_list_ids
| string | Comma-separated user list IDs ||
phone_number
| string | Exact match, normalized ||
email
| string | Exact match, case-insensitive |GET /v1/users/{id}
fetches a single user by ID.So today a user is reachable by
ID, exact email, or exact phone number
— nothing else.The user object itself carries far more addressable data:
- first_name,last_name,alternate_name
- other_emails[],other_phone_numbers[]
- address(address1,address2,city,state,zip_code,country)
- chapter_id,chapter_ids[],branch_id
- preferred_language,second_language
- custom_user_properties(organization- and chapter-scoped, enumerable via
GET /v1/custom_user_properties
, each with an id
, key
, name
, and field_type
)None of these can be used to locate a user.
## Problem
Our Discord bot needs to resolve a person to a Solidarity Tech profile when all we have is a
partial detail — a name a member typed or a Discord handle stored as a custom
user property. There is no server-side way to do this.
The only workaround is to paginate the entire user collection and filter client-side. That is
expensive in a way that scales badly:
- Page size caps at 100, so a roster of Nmembers costsceil(N / 100)requests.
- The rate limit is 60 requests per 30 seconds (~2/sec), so 10,000 members is ~100 requests
and ~50 seconds of wall-clock time for a
single lookup
.- The practical response is to mirror the whole roster into a local cache, which means we are
maintaining a stale copy of member PII on our own infrastructure purely to work around a
missing query parameter.
- _sincehelps keep such a cache warm but does not help a cold lookup, and it does not
surface deletions.
Filtering on
custom_user_properties
is the sharpest pain: those fields are whereorganization-specific identity lives (Discord username, membership number, steward role), and
they are precisely the fields with no lookup path at all.
## Proposed API Changes
Two additions, either of which is independently useful.
### 1. Free-text search parameter
GET /v1/users?q=jordan%20rivera
- q(string, optional): case-insensitive substring match across a documented set of fields —
suggested default:
first_name
, last_name
, alternate_name
, email
, other_emails
,phone_number
, other_phone_numbers
.- Multi-token input should match tokens independently ("jordan rivera"matches
first_name="Jordan"
, last_name="Rivera"
).- Results paginate with the existing _limit/_offsetand return the usual
meta.total_count
, so callers can detect over-broad queries.- Composes with existing filters (q+user_list_ids+_sinceare ANDed).
Optionally,
q_fields=first_name,last_name
to narrow which fields participate.### 2. Per-field filters
GET /v1/users?last_name=rivera&city=greensboro
GET /v1/users?custom_user_properties[discord_username]=jrivera
- Scalar filters for first_name,last_name,alternate_name,city,state,zip_code,
chapter_id
, branch_id
, preferred_language
.- custom_user_properties[<key>]=<value>keyed by thekeyalready returned from
GET /v1/custom_user_properties
. For Multiple Checkboxes
fields, match if the value ispresent in the array.
- Multiple filters AND together. Repeating a parameter (or comma-separating) ORs within that
field.
- Match semantics should be documented per field; exact match is acceptable for all of these
and is simpler than prefix or fuzzy matching. Consistency with the existing
email
parameter's case-insensitivity would be ideal.
### Errors
Reuse the existing
422 invalid user filter
response for unknown fields, unknown customproperty keys, and queries below a minimum length (if one is enforced).
## Acceptance criteria
- A user with a known first and last name can be located in onerequest, with no prior
knowledge of their ID, email, or phone number.
- A user can be located by the value of a custom user property in one request.
- New parameters are optional; omitting them preserves today's response exactly.
- Results respect the caller's existing permission scope — search must not widen visibility
beyond what paginating
GET /v1/users
already returns for that API key.- Pagination metadata (total_count,limit,offset) reflects the filtered set.
## Alternatives considered
- Client-side cache of the full roster.What we do now. Duplicates member PII onto
third-party infrastructure, goes stale, and still costs a full crawl to seed.
- Saved filters / user lists.user_list_idsfiltering works well for pre-defined
segments, but lists are authored in the UI and cannot express an ad-hoc lookup for one
person by name.
- Webhook-driven local index.Requires building and operating a search index for what is
fundamentally a database query on Solidarity Tech's side, and inherits every consistency
problem of the cache approach.
## Impact
This unblocks any integration that starts from a human-supplied identifier rather than a
Solidarity Tech ID: Discord and Slack bots, volunteer check-in tooling, canvassing apps, and
help-desk lookups. It also reduces load on the API — one filtered query replaces a full-roster
crawl that today runs right up against the rate limit.