List campaigns
GET /campaigns
Description
Lists the campaigns belonging to your account, newest first, as a cursor-paginated collection. A campaign is the gift collection offered on a send. It groups the products a recipient can be given, and it's the thing you reference from Create a gift request to drive what gets offered and how.
Each item in the list carries the full campaign shape: the same object Get a campaign returns, including its product, variant, stock-level, and warehouse details.
When to use
Use this endpoint to discover which campaigns exist, for example to
present a picker, or to find the id you'll pass as campaign_id when you
create a gift request. When you already know the
campaign you want and only need that one, fetch it directly with
Get a campaign rather than paging the whole list.
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 campaigns to return per page. Defaults
to 25, which is also the maximum: each campaign carries its full nested
gift collection, so the page is capped to keep response size and time
predictable. 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 campaign 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.
Ordering
sort (string · optional) is the field to order by, - prefix for
descending. Defaults to -created_at (newest first); created_at, name,
-name, updated_at, and -updated_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. Two kinds behave differently when nothing matches, and the difference is worth knowing before you debug an empty page:
- An exact-match filter (
status,owner,team,supported_country_codes) given an unrecognized value matches nothing and returns an empty page. - A substring filter (
name,owner.name,owner.email) is case-insensitive and matches anywhere in the value.
status (string · optional) filters by public status. Accepts a single value
(available or archived) or a comma-separated list of both.
name (string · optional) matches campaigns whose name contains the value.
Values longer than 255 characters are truncated.
owner (string · optional) filters by the owning user's id. 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 owner instead of the campaign. owner.email is a substring match,
so a bare domain such as @example.com selects everyone on it.
team (string · optional) filters by the owning user's team. Accepts a single
UUID or a comma-separated list.
supported_country_codes (string · optional) filters by shipping
destination. Accepts a single ISO 3166-1 alpha-2 code or a comma-separated list,
and matches a campaign that supports any of them.
created_at (object · optional) filters by creation time with bracketed range
operators: created_at[gte], created_at[lte], created_at[gt], and
created_at[lt]. Values are ISO 8601 timestamps, and several operators combine
into an interval, so
created_at[gte]=2026-01-01T00:00:00.000Z&created_at[lt]=2026-04-01T00:00:00.000Z
is the first quarter. An unknown operator or a non-ISO-8601 value 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 with the defaults.
- cURL
- JavaScript
- Python
- Ruby
curl -X GET "https://api.andopen.co/campaigns" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
const response = await fetch("https://api.andopen.co/campaigns", {
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 campaign of page.data) {
console.log(campaign.id, campaign.name, campaign.status);
}
import requests
response = requests.get(
"https://api.andopen.co/campaigns",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
)
response.raise_for_status()
page = response.json()
for campaign in page["data"]:
print(campaign["id"], campaign["name"], campaign["status"])
require "net/http"
require "json"
uri = URI("https://api.andopen.co/campaigns")
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 |campaign|
puts "#{campaign['id']} #{campaign['name']} #{campaign['status']}"
end
Paging through every campaign
To walk the whole collection, keep fetching while has_more is true, passing
the last campaign'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/campaigns?after=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05"
async function listAllCampaigns() {
const campaigns = [];
let after;
do {
const url = new URL("https://api.andopen.co/campaigns");
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();
campaigns.push(...page.data);
const lastId = page.data.at(-1)?.id;
after = page.has_more && lastId ? lastId : undefined;
} while (after);
return campaigns;
}
import requests
def list_all_campaigns():
campaigns = []
after = None
while True:
response = requests.get(
"https://api.andopen.co/campaigns",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
params={"after": after} if after else {},
)
response.raise_for_status()
page = response.json()
campaigns.extend(page["data"])
if not page["has_more"] or not page["data"]:
return campaigns
after = page["data"][-1]["id"]
require "net/http"
require "json"
def list_all_campaigns
campaigns = []
after = nil
loop do
uri = URI("https://api.andopen.co/campaigns")
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)
campaigns.concat(page["data"])
break campaigns 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
campaigns plus a boolean cursor flag. Paging is driven by has_more and the
after cursor.
| Field | Description |
|---|---|
data (array · required) | The campaigns in this page. Each entry is a full campaign object, identical to what Get a campaign returns; see that page for the per-field detail of the nested gift collection. |
has_more (boolean · required) | Whether more campaigns 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.status (string · required) | Each campaign's status, one of available, archived. data.products (array · required) is its inline gift-collection tree. |
data.owner (object · required · nullable) | Each campaign's owner summary. supported_country_codes and gift_request_statistics come inline too, so a picker can show who owns a campaign, where it ships, and how much it has been used without a second call. |
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Q2 Customer Appreciation",
"description": "Send a thank-you gift to our top accounts.",
"status": "available",
"web_url": "https://acme.andopen.co/sender/gift-history/campaigns/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"default_email_message": {
"subject": "A gift for you",
"body": "Thanks for being a valued customer. Enjoy your gift!"
},
"owner": {
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Dana Okafor",
"email": "dana@acme.example"
},
"supported_country_codes": [
"IE",
"GB",
"US"
],
"shipping_from": [
"IE"
],
"gift_request_statistics": {
"total": 120,
"redeemed": 84,
"dispatched": 12,
"delivered": 70
},
"offering": {
"selection_mode": "one",
"visible_at_redemption": true
},
"archived_at": null,
"products": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
"name": "Leather Notebook",
"enabled": true,
"images": [
{
"name": "leather-notebook.jpg",
"url": "https://files.andopen.co/rndd63mwgj94j2vf3v2ngnye0hkn",
"urls": {
"thumbnail": "https://files.andopen.co/vw2mvwevyqxh98l7esf67nvunr4h",
"large": "https://files.andopen.co/mz4rc8kx1nvq7j93wtf5bd2hs0yl",
"large_wide": "https://files.andopen.co/q7pz3n8htv2ke0d5rjw94flxs6cb"
}
}
],
"variants": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
"name": "Medium / Black",
"sku": "NB-LTHR-MED-BLK",
"enabled": true,
"position": 1,
"price": {
"amount": 1500,
"currency": "EUR"
},
"stock_levels": [
{
"id": "d4e5f6a7-b8c9-0123-defa-123456789012",
"quantity": 42,
"warehouse": {
"id": "e5f6a7b8-c901-2345-efab-234567890123",
"name": "EU Central — Frankfurt",
"enabled": true,
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
},
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}
],
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}
],
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}
],
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}
],
"has_more": true
}
Per-field notes
- Every item is the full campaign. The list does not return a trimmed
summary; each entry includes the same product, variant, stock-level, and
warehouse details as the single-campaign endpoint. This is why the page size
is capped at
25. 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.- Exact-match filtering is forgiving. An unknown value for
status,owner,team, orsupported_country_codesyields an empty page, not a400, so an emptydataarray can mean either "no matches" or "filter typo". The gift-collection tree within each campaign follows the same warehouse-filtering rules described on Get a campaign.
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, an unsupportedsortfield, a malformedcreated_atoperator or timestamp, andowner=meunder service authentication are allinvalid_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 campaigns (forbidden).429 rate_limit_error: too many requests; back off and retry (rate_limited). TheRateLimit-*response headers tell you the budget and reset time.