> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.awardspring.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.awardspring.com/_mcp/server.

# List donors.

GET https://api.awardspring.com/api/v1/donors

Returns one page at a time. With no `sort` and no `q`, donors are listed by
`id`, lowest first.


Sort with `sort`. `name` lists donors A to Z, ignoring case: an organization by its
organization name, and a person by last name, then first name. A person with no last name
is listed by first name, and a donor with no name at all comes last. `last_contacted`
lists the most recently contacted donors first, by `last_contacted_at`; donors who
have never been contacted come last. Donors who tie are listed by `id`: lowest first
for `name`, highest first for `last_contacted`.


Narrow the list with `level` to donors at one or more levels, for example
`level=major,principal`. A donor's level comes from their giving, so a request made as
a signed-in user who may not see donors' giving returns `403 permission_denied` when
it filters by level. Sorting is open to everyone who can list donors.


Narrow the list with `giving_status` to donors with one or more giving statuses, for
example `giving_status=recurring,new`. `Recurring`: the donor has a recurring gift
that is still running (not cancelled or stopped, and not past its end date) and at least one
payment from it has gone through. `New`: the donor's first gift was in the last 12
months. `Lapsed`: the donor's last gift was more than 12 months ago. The 12 months count
back from now, not from the start of a fiscal year. A donor who fits more than one takes the
first of `Recurring`, `New`, `Lapsed`. A donor who has never given has no giving
status, and neither does one whose first gift was more than 12 months ago and who has given
again within the last 12 months. With `level` as well, a donor must match both. A
request made as a signed-in user who may not see donors' giving returns
`403 permission_denied` when it filters by giving status.


Search with `q` (at least `2` characters). Every word you type must match the
start of a word in the donor's first name, last name or organization, or the start of
their email address, so `jane smi` finds Jane Smith. Matches inside a word are not
found: `co` finds `Coworking` but not `Waco`. Where digits and letters
touch, a word is also split there, so `25co` finds `25N Coworking`.


Without `sort`, search results come in three groups: first, donors whose full name or
organization starts with the search (for a search with no spaces, compared with the spaces
taken out, so `janesmith` finds Jane Smith); next, donors where every word starts a
word in their name or organization; last, donors matched only through their email address.
Within a group, donors are listed by `id`, lowest first, so the order holds from one
page to the next. With `sort`, the search only decides which donors are listed and
the sort decides their order.

`sort`, `level`, `giving_status` and `q` work together. Send the same ones
on every page: a cursor sent with a different `sort`, `level`, `giving_status`
or `q` returns `400 invalid_cursor`. A search shorter than `2` characters, or an
unknown `level`, `giving_status` or `sort`, returns `400 validation_failed`
with `param` set to the parameter at fault.


Assistant capability: `springiq_list_donors`. Required permission: Donors (applies when called on behalf of a signed-in person).

Reference: https://docs.awardspring.com/api-reference/donors/list

## Authentication

- `X-Spring-API-Key` header (required) — Secret API key issued per institution. Send it on every request.

## Servers

- `https://api.awardspring.com` (UnitedStates, default)
- `https://api.awardspring.ca` (Canada)

## Request

### Query parameters

- `q` (string, optional) — Search text (`?q=`), at least `2` characters; a shorter search returns `400 validation_failed`. Each word must match the start of a word in the donor's first name, last name or organization, or the start of their email address, ignoring case: `co` finds `Coworking` and `cole@school.edu` but not `Waco`. Where digits and letters touch, a word is also split there, so `25co` searches as `25 co`. A search with no spaces also matches the start of the full name or organization with its spaces taken out, so `janesmith` finds Jane Smith. Leave it out to list every donor.
- `level` (list of enum, optional) — Only donors at these levels (`?level=`): `Annual`, `Mid`, `Major` or `Principal`, the values of the donor's `level` field, in any case. Send several comma-separated (`level=major,principal`) or repeat the parameter; a donor at any of them is listed. Any other value returns `400 validation_failed`. Level is worked out from giving, so a signed-in user who may not see donors' giving gets `403 permission_denied`. Leave it out for every level.
  - Allowed values: `Annual`, `Mid`, `Major`, `Principal`
- `giving_status` (list of enum, optional) — Only donors with these giving statuses (`?giving_status=`): `Recurring`, `New` or `Lapsed`, the values of the donor's `giving_status` field, in any case. Send several comma-separated (`giving_status=recurring,new`) or repeat the parameter; a donor with any of them is listed. With `level` as well, a donor must match both. Any other value returns `400 validation_failed`. Giving status is worked out from giving, so a signed-in user who may not see donors' giving gets `403 permission_denied`. Leave it out for every donor, including those with no giving status.
  - Allowed values: `Recurring`, `New`, `Lapsed`
- `sort` (enum, optional) — The order to list donors in (`?sort=`): `name` or `last_contacted`. Leave it out for the default order. Any other value returns `400 validation_failed`.
  - Allowed values: `name`, `last_contacted`
- `limit` (integer, optional, default: 25) — Page size. Defaults to 25. Values outside 1–100 are clamped rather than rejected.
- `starting_after` (string, optional) — Opaque cursor. Pass the `next_cursor` from the previous page to page forward. Mutually exclusive with `ending_before`; if both are supplied, `starting_after` wins.
- `ending_before` (string, optional) — Opaque cursor. Pass the `previous_cursor` from the current page to page backward.
- `include` (list of enum, optional) — Optional extras to add to the response, comma-separated. `total_count` is the only supported value: it adds `total_count`, the number of records that match this request across all pages. The count is taken fresh at the moment of each request, so it can change while you page; ask for it on the first page. Without it, `total_count` is left out of the response. Asking or not never invalidates a cursor. Any other value returns `400 validation_failed`.
  - Allowed values: `total_count`

## Response

### 200

OK

- `object` (string, optional, nullable) — Discriminator constant. Always `"list"`, mirroring Stripe's list objects.
- `url` (string, optional, nullable) — The request path this collection was served from (e.g. `/api/v1/scholarships`).
- `has_more` (boolean, optional) — True when another page exists in the current paging direction.
- `next_cursor` (string, optional, nullable) — Opaque cursor for the next page (pass as `starting_after`). Null when there is none.
- `previous_cursor` (string, optional, nullable) — Opaque cursor for the previous page (pass as `ending_before`). Null when there is none.
- `total_count` (integer, optional, nullable) — The number of records that match the request across all pages, counted fresh on each request. Present only when the request includes `include=total_count`; otherwise the key is omitted.
- `data` (list of DonorListItemV1, optional, nullable) — The page of items.

## Errors

### 400 Bad Request Error

Bad Request

- `error` (V1ErrorBody, optional) — Stripe-shaped error object for the `/api/v1/*` surface. Serialized snake\_case (via V1SnakeCaseNamingStrategy) and always nested under the `error` key of a V1ErrorEnvelope: `{ "error": { "type": "...", "code": "...", "message": "...", "param": null, "doc_url": null, "recovery": "..." } }`

### 403 Forbidden Error

Forbidden

- `error` (V1ErrorBody, optional) — Stripe-shaped error object for the `/api/v1/*` surface. Serialized snake\_case (via V1SnakeCaseNamingStrategy) and always nested under the `error` key of a V1ErrorEnvelope: `{ "error": { "type": "...", "code": "...", "message": "...", "param": null, "doc_url": null, "recovery": "..." } }`

### 429 Too Many Requests Error

Rate limit exceeded for this API key. Retry after the interval in the `Retry-After` header.

- `error` (V1ErrorBody, optional) — Stripe-shaped error object for the `/api/v1/*` surface. Serialized snake\_case (via V1SnakeCaseNamingStrategy) and always nested under the `error` key of a V1ErrorEnvelope: `{ "error": { "type": "...", "code": "...", "message": "...", "param": null, "doc_url": null, "recovery": "..." } }`

## Types

### DonorListItemV1

- `object` (string, optional, nullable) — Stripe-style resource discriminator. Stable, snake_case string naming the resource type. Serialized first so it reads as the leading field of every V1 resource body.
- `id` (string, optional, nullable) — The donor's stored ID: 9 characters, sent exactly as stored.
- `first_name` (string, optional, nullable)
- `last_name` (string, optional, nullable)
- `email` (string, optional, nullable)
- `organization` (string, optional, nullable)
- `phone` (string, optional, nullable)
- `role` (enum, optional, nullable) — Whether this is an individual donor or a donor organization: `Individual` or `Organization`. Mirrors the `role` field on the create request.
  - Allowed values: `Individual`, `Organization`
- `level` (enum, optional, nullable) — The donor's level: `Annual`, `Mid`, `Major` or `Principal`. It is the level the institution's donor portal shows this donor, worked out from the total of every gift recorded for them: under $1,000 is `Annual`, from $1,000 is `Mid`, from $25,000 is `Major` and from $1,000,000 is `Principal`. When staff have set or held the donor's level by hand, that level is shown instead. Null when the request is made as a signed-in user who may not see donors' giving.
  - Allowed values: `Annual`, `Mid`, `Major`, `Principal`
- `last_contacted_at` (integer, optional, nullable) — When the donor was last contacted: the date of the newest email, phone call, meeting or note logged with them before today, in the institution's time zone. Gifts, pledges and emails sent by AwardSpring do not count. Null when no contact has been logged.
- `lifetime_giving` (double, optional, nullable) — The donor's lifetime giving, the same figure the donor's page in AwardSpring shows: every gift recorded for them up to now, plus their soft credits when their record is set to include soft credits. `0` for a donor with no gifts. Null when the request is made as a signed-in user who may not see donors' giving.
- `giving_status` (enum, optional, nullable) — The donor's giving status: `Recurring`, `New` or `Lapsed`. `Recurring` means the donor has a recurring gift that is still running (it has not been cancelled or stopped, and its end date, if it has one, has not arrived) and at least one payment from it has gone through. Otherwise the donor's gift dates decide: `New` means their first gift was in the last 12 months, and `Lapsed` means their last gift was more than 12 months ago. The 12 months count back from the moment of the request, not from the start of a fiscal year. A donor who fits more than one is `Recurring` before `New`, and `New` before `Lapsed`. Null for a donor who has never given, or whose first gift was more than 12 months ago and who has given again within the last 12 months. Also null when the request is made as a signed-in user who may not see donors' giving.
  - Allowed values: `Recurring`, `New`, `Lapsed`

### V1ErrorBody

Stripe-shaped error object for the `/api/v1/*` surface. Serialized snake\_case (via V1SnakeCaseNamingStrategy) and always nested under the `error` key of a V1ErrorEnvelope: `{ "error": { "type": "...", "code": "...", "message": "...", "param": null, "doc_url": null, "recovery": "..." } }`

- `type` (string, optional, nullable) — Coarse error category from V1ErrorType (e.g. `invalid_request_error`).
- `code` (string, optional, nullable) — Stable fine-grained machine code from ErrorCode (e.g. `donor_not_found`).
- `message` (string, optional, nullable) — Human-readable summary safe to surface to the caller.
- `param` (string, optional, nullable) — The request parameter the error relates to, when attributable to a single field; otherwise null.
- `doc_url` (string, optional, nullable) — Link to documentation for this error, when one exists; otherwise null.
- `recovery` (string, optional, nullable) — Plain-language guidance on how to recover from this error, suitable for showing to the person who triggered it. Not present on every error.
- `details` (any, optional) — Optional structured payload (e.g. a list of ValidationDetail for validation failures). Omitted from the wire when null so the canonical Stripe shape stays minimal.

## Examples

**Response**

```json
{
  "object": "string",
  "url": "string",
  "has_more": true,
  "next_cursor": "string",
  "previous_cursor": "string",
  "total_count": 1,
  "data": [
    {
      "object": "string",
      "id": "string",
      "first_name": "string",
      "last_name": "string",
      "email": "string",
      "organization": "string",
      "phone": "string",
      "role": "Individual",
      "level": "Annual",
      "last_contacted_at": 1,
      "lifetime_giving": 1.1,
      "giving_status": "Recurring"
    }
  ]
}
```

**SDK Code**

```python
import requests

url = "https://api.awardspring.com/api/v1/donors"

headers = {"X-Spring-API-Key": "<apiKey>"}

response = requests.get(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.awardspring.com/api/v1/donors';
const options = {method: 'GET', headers: {'X-Spring-API-Key': '<apiKey>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.awardspring.com/api/v1/donors"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("X-Spring-API-Key", "<apiKey>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.awardspring.com/api/v1/donors")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["X-Spring-API-Key"] = '<apiKey>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.awardspring.com/api/v1/donors")
  .header("X-Spring-API-Key", "<apiKey>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.awardspring.com/api/v1/donors', [
  'headers' => [
    'X-Spring-API-Key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.awardspring.com/api/v1/donors");
var request = new RestRequest(Method.GET);
request.AddHeader("X-Spring-API-Key", "<apiKey>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["X-Spring-API-Key": "<apiKey>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.awardspring.com/api/v1/donors")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```