List themes
GET /themes
Description
Lists your account's themes, most recently edited first, as a cursor-paginated collection. A theme sets how a gift's invitation email and landing page look and read: the colors, fonts, and logo, and the default wording the recipient sees.
Each item carries the full theme shape, the same object Get a theme returns.
When to use
Use this endpoint to show your users which themes they can send with, for
example as a picker of swatches built from each theme's appearance. Filter
with default=true to find the account's default theme: a gift request sent
without a campaign uses it, and so does a campaign with no theme of its own.
When you already have a theme's id, fetch it directly with
Get a theme.
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
Every one of these is optional, and sent together they page, sort, and filter the same collection.
Paging
limit (integer · optional) is how many themes to return per page. Defaults
to 25, and the most is 50: ask for more and you get 50. A non-numeric,
zero, or negative value is rejected with a 400.
after (string · uuid · optional) is the keyset pagination cursor. To fetch the next
page, pass the id of the last theme in the page you just received. Omit it
for the first page. A cursor that references no existing theme is rejected with
a 400. Stop paging when the response's has_more is false.
Ordering
sort (string · optional) is the field to order by, - prefix for
descending. Defaults to -updated_at, so the theme edited most recently comes
first; updated_at, created_at, and -created_at are also accepted. Any
other field is rejected with a 400.
Filtering
Filters combine with AND, so each one you add narrows the page further. None of them rejects a value it doesn't recognize: an unknown id, a malformed team UUID, or a blank substring matches nothing and returns an empty page.
default (string · optional) takes true or false. true returns the
account's default theme on its own, or an empty page when the account has no
default. false returns every other theme. Any other value matches nothing.
name (string · optional) matches themes whose name contains the value,
case-insensitively.
owner (string · optional) filters by the id of the user who created the
theme. The literal me resolves to the authenticated user, which needs user
authentication: a service token has no connecting user and gets a 400.
owner.name (string · optional) and owner.email (string · optional) match
against the creator instead of the theme. Both are case-insensitive substring
matches, so a bare domain such as @example.com in owner.email selects every
creator on it.
team (string · optional) filters by the creator's team. Accepts a single
UUID or a comma-separated list.
Worked examples
The cURL version is the raw HTTP call; the JavaScript, Python, and Ruby versions are idiomatic equivalents. Each requests the account's default theme.
- cURL
- JavaScript
- Python
- Ruby
curl -X GET "https://api.andopen.co/themes?default=true" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
const url = new URL("https://api.andopen.co/themes");
url.searchParams.set("default", "true");
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();
const defaultTheme = page.data[0];
console.log(defaultTheme?.name ?? "No default theme");
import requests
response = requests.get(
"https://api.andopen.co/themes",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
params={"default": "true"},
)
response.raise_for_status()
page = response.json()
default_theme = page["data"][0] if page["data"] else None
print(default_theme["name"] if default_theme else "No default theme")
require "net/http"
require "json"
uri = URI("https://api.andopen.co/themes")
uri.query = URI.encode_www_form(default: "true")
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)
default_theme = page["data"].first
puts default_theme ? default_theme["name"] : "No default theme"
Paging through every theme
To walk the whole collection, keep fetching while has_more is true, passing
the last theme'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/themes?limit=50&after=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
async function listAllThemes() {
const themes = [];
let after;
do {
const url = new URL("https://api.andopen.co/themes");
url.searchParams.set("limit", "50");
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();
themes.push(...page.data);
const lastId = page.data.at(-1)?.id;
after = page.has_more && lastId ? lastId : undefined;
} while (after);
return themes;
}
import requests
def list_all_themes():
themes = []
after = None
while True:
params = {"limit": 50}
if after:
params["after"] = after
response = requests.get(
"https://api.andopen.co/themes",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
params=params,
)
response.raise_for_status()
page = response.json()
themes.extend(page["data"])
if not page["has_more"] or not page["data"]:
return themes
after = page["data"][-1]["id"]
require "net/http"
require "json"
def list_all_themes
themes = []
after = nil
loop do
uri = URI("https://api.andopen.co/themes")
query = { limit: 50 }
query[:after] = after if after
uri.query = URI.encode_www_form(query)
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)
themes.concat(page["data"])
break themes 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 themes
plus a boolean cursor flag.
| Field | Description |
|---|---|
data (array · required) | The themes in this page. Each entry is a full theme object, identical to what Get a theme returns; see that page for the per-field detail. |
has_more (boolean · required) | Whether more themes 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.default (boolean · required) | Whether each theme is the account's default. At most one theme in the page, or in the account, is true. |
data.appearance (object · required) | Each theme's colors, heading font, and logo, enough to draw a swatch without opening the theme. |
{
"data": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Classic",
"default": true,
"appearance": {
"background_color": "#FCF8F2",
"text_color": "#000000",
"button_background_color": "#000000",
"button_text_color": "#FCF8F2",
"heading_font_family": "Playfair Display, serif",
"logo_url": "https://files.andopen.co/rndd63mwgj94j2vf3v2ngnye0hkn"
},
"owner": {
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Dana Okafor",
"email": "dana@acme.example"
},
"web_url": "https://acme.andopen.co/sender/themes/42",
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}
],
"has_more": false
}
Per-field notes
has_moredrives paging, not a total count. The response carries no total and nonextURL, so build the next request from the last item'sid.- An account may have no default theme. Then no item has
default: true, anddefault=truereturns an empty page. data[].ownerisnullwhen the theme's creator isn't recorded or is no longer a user. Filtering byowner,owner.name, orowner.emailnever matches those themes.
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, naming the offending one inparam. Anaftercursor referencing no existing theme isinvalid_reference. Alimitthat is not a positive integer and an unsupportedsortfield are bothinvalid_format, and so isowner=mesent with a service token, which has no user to resolve.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 themes (forbidden).429 rate_limit_error: too many requests; back off and retry (rate_limited). TheRateLimit-*response headers tell you the budget and reset time.