The API, in plain words
Everything your team does in the CRM, your own code and AI assistants can do too: one REST API, an MCP server for assistants, and webhooks when things happen.
Your first call, in three steps
You need an account with the API on its plan, and to be an owner or admin. It takes about two minutes.
- Make a keyIn the app, open Developers and choose New API key. Give it a name you will recognise, like Website or Claude, and tick only what it needs. Copy the key: it starts adaab_ and is shown once.
- Check it worksAsk who the key belongs to. The answer names your organisation and lists every operation this key may call.
- Do something realPick an operation below. Each one has its fields, what comes back, the errors to expect, and the same call in curl, JavaScript, Python and as an MCP tool call, ready to copy.
curl https://arm.adab.solutions/api/v1/account \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/account", {
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
console.log(await res.json());import os, requests
res = requests.get("https://arm.adab.solutions/api/v1/account", headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"})
print(res.json())Keep keys out of your code: put them in an environment variable such as ADAAB_API_KEY, as the examples do.
Keys and sign-in
An owner or admin makes a key under Developers in the app, and chooses what it may do. Send it on every request as a bearer token. A key can only call what its organisation’s plan includes and what its permissions allow; the rest answer 403.
A key acts as the person who made it: notes and tasks it writes show their name, and it never has more permissions than they do. If they leave the team or lose a permission, the key loses it too. Revoke a key under Developers and anything using it stops at once.
Authorization: Bearer adaab_…
Every address below starts with https://arm.adab.solutions/api/v1. The same API as an OpenAPI document: /api/v1/openapi.json
Lists, dates and ids
| Ids | Every record has an id of 12 letters and digits, like k3Jd9QmZp2Lx. They never change. |
|---|---|
| Lists | Lists come back as { "data": [ … ] }. Most take limit (up to 200, 50 by default) and offset. To get everything, add limit to offset until fewer than limit come back. |
| Dates and times | Send days like 2026-10-01. Times come back in UTC, like 2026-10-01T09:30:00.000Z. |
| Money | Amounts are in pounds (or the currency given), not pennies: send 25.50 for £25.50. Currencies are three letters and default to GBP. |
| Safe retries | Recording a donation takes your own external_id, so sending the same gift twice records it once. Adding a contact with an email or phone that already exists updates them instead of making a duplicate. |
| Sending data | GET requests take fields in the address (?q=amina). POST and PATCH take a JSON body with Content-Type: application/json. |
Errors and limits
Anything that goes wrong comes back with an HTTP status and a sentence saying what happened. Wrong input also lists each problem with the field it is about:
{
"error": "Invalid input",
"issues": [
{
"path": "email",
"message": "Invalid email address"
}
]
}
| Code | What it means | What to do |
|---|---|---|
400 | The input was wrong. The answer lists each problem with the field it is about. | Fix the fields named in issues and send it again. |
401 | No key, or a key that was revoked. | Check the Authorization header, or make a new key. |
402 | The organisation is paused: its trial ended or payment failed. It works again once someone chooses a plan. | An owner chooses a plan under Plan and billing. |
403 | The plan does not include this, or the key lacks the permission. | Call /account to see what this key may do; make a key with the permission it needs. |
404 | Nothing with that id in this organisation. | Check the id, or search for the record first. |
429 | More than 600 requests a minute from one key. Wait a moment and try again. | Slow down and retry after the time in the RateLimit headers. |
500 | Something went wrong on our side. | Try again in a minute. If it keeps happening, tell us the time and the address you called. |
Lists take limit (up to 200, 50 by default) and offset. Ids are 12 letters and digits. Dates are like 2026-10-01; times come back in UTC.
AI assistants (MCP)
Claude and other assistants that speak the Model Context Protocol connect to the address below with the same key. Each operation on this page becomes a tool, and an assistant only sees the tools its key may use.
https://arm.adab.solutions/mcp
Each tool carries the same descriptions as this page, so the assistant knows what every field means. Tools that only read are marked read-only, so a careful assistant can use them without asking you first.
claude mcp add --transport http adaab https://arm.adab.solutions/mcp \
--header "Authorization: Bearer adaab_…"{
"mcpServers": {
"adaab": {
"type": "http",
"url": "https://arm.adab.solutions/mcp",
"headers": {
"Authorization": "Bearer adaab_…"
}
}
}
}Signed in? The step-by-step guide in the app, under Developers, fills in your address and checks the connection for you. Connect an assistant
Webhooks
Add an https address under Developers and choose the events. Each one arrives as a JSON POST with an X-Adaab-Signature header, t=<time>,v1=<signature>: the signature is an HMAC-SHA256, with your endpoint’s secret, of the time, a dot and the body. A failed delivery is tried again after 1, 2, 4, 8 and 16 minutes.
Check the signature before trusting a webhook, and refuse ones whose time is more than five minutes old:
import crypto from "node:crypto";
function verify(header, rawBody, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}import hmac, hashlib, time
def verify(header, raw_body, secret):
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
want = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts["v1"])contact_createdstatus_changedtag_addedlist_joinedmessage_sentdonation_received
Supporters (contacts and leads)
GET /contacts
Search contacts by name, email or phone (q), status, tag or list. Newest first.
Use this to find people before acting on them: look someone up by part of their name, email or phone, or pull everyone with a status, tag or on a list. Results are newest first and paged with limit and offset. To walk the whole database, keep adding limit to offset until fewer than limit come back.
| Field | Type | Notes |
|---|---|---|
qquery |
string | Words to look for in the name, email or phone. Partial matches count: "amin" finds Amina. Up to 200 characters.Example "amina" |
statusquery |
string | Only people with exactly this status, e.g. donor or lead. |
tagquery |
string | Only people with this tag. |
list_idquery |
string | Only people on this list (see lists_list for ids). |
limitquery |
integer | How many to return, 1 to 200. From 1 to 200. Default 50.Example 20 |
offsetquery |
integer | How many to skip, for the next page. From 0 to 9007199254740991. Default 0. |
What comes back: { data: [contact, …] }. Each contact has its id, names, email, phone, status, source, campaign, tags, custom fields and opt-outs.
curl "https://arm.adab.solutions/api/v1/contacts?q=amina&limit=20" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/contacts?q=amina&limit=20", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"q": "amina",
"limit": 20
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "contacts_search",
"arguments": {
"q": "amina",
"limit": 20
}
}
}Example response
{
"data": [
{
"id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"phone": "+447700900123",
"status": "donor",
"source": "website",
"campaign": "Ramadan 2026",
"tags": [
"monthly",
"volunteer"
],
"fields": {
"city": "Leeds"
},
"email_opt_out": false,
"whatsapp_opt_out": false,
"created_at": "2026-09-14T10:22:05.000Z",
"updated_at": "2026-10-01T08:00:00.000Z"
}
]
}GET /contacts/:id
One contact with their notes, open tasks, lists and giving summary.
Everything about one person in one call: their details, the last 20 notes, the lists they are on and a giving summary per currency. Use it before writing to someone or when an assistant needs context about a supporter.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The contact's id, 12 letters and digits.Example "k3Jd9QmZp2Lx" |
What comes back: The contact, plus notes (newest first), lists ({ id, name }) and giving ({ currency, gifts, total, last_gift }).
404No contact with that id in this organisation. Search for them with contacts_search.
curl "https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "contacts_get",
"arguments": {
"id": "k3Jd9QmZp2Lx"
}
}
}Example response
{
"id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"phone": "+447700900123",
"status": "donor",
"source": "website",
"campaign": "Ramadan 2026",
"tags": [
"monthly",
"volunteer"
],
"fields": {
"city": "Leeds"
},
"email_opt_out": false,
"whatsapp_opt_out": false,
"created_at": "2026-09-14T10:22:05.000Z",
"updated_at": "2026-10-01T08:00:00.000Z",
"notes": [
{
"id": "Nte1Aa2Bb3Cc",
"body": "Prefers WhatsApp.",
"created_at": "2026-09-20T12:00:00.000Z"
}
],
"lists": [
{
"id": "Lst7Hq2Wn4Ra",
"name": "Monthly givers"
}
],
"giving": [
{
"currency": "GBP",
"gifts": 7,
"total": 350,
"last_gift": "2026-09-30"
}
]
}POST /contacts
Create a contact, or update the existing one with the same email or phone (upsert). Fires automations and webhooks.
Adds a person, or updates them if someone with the same email or phone is already there, so it is safe to call from a sign-up form every time. Only the fields you send are set. New people start any "new contact" automations and send the contact.created webhook; updates send contact.updated.
| Field | Type | Notes |
|---|---|---|
first_namebody |
string | Their first name. Up to 100 characters.Example "Amina" |
last_namebody |
string | Their last name. Up to 100 characters.Example "Khan" |
emailbody |
email | null | Their email address. Used to find an existing person.Example "amina@example.org" |
phonebody |
string | null | Their phone number, best with the country code (+44…). Used to find an existing person. |
statusbody |
string | Where they are with you, e.g. lead, donor, volunteer. Up to 80 characters. |
sourcebody |
string | Where they came from, e.g. website or event. Up to 200 characters.Example "website" |
campaignbody |
string | The campaign that brought them in. Up to 200 characters. |
tagsbody |
string[] | Labels to add. Up to 50. Up to 50.Example ["newsletter"] |
fieldsbody |
object | Custom columns by key, e.g. {"city": "Leeds"}. See Columns in the app for the keys. |
email_opt_outbody |
boolean | true if they asked not to be emailed. |
whatsapp_opt_outbody |
boolean | true if they asked not to get WhatsApp messages. |
sms_opt_outbody |
boolean | true if they asked not to get text messages. |
What comes back: { created, contact }. created is true for a new person, false when an existing one was updated.
400Send at least an email, a phone or a first name.
curl -X POST "https://arm.adab.solutions/api/v1/contacts" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"Amina","last_name":"Khan","email":"amina@example.org","source":"website","tags":["newsletter"]}'const res = await fetch("https://arm.adab.solutions/api/v1/contacts", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"source": "website",
"tags": [
"newsletter"
]
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"source": "website",
"tags": [
"newsletter"
]
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "contacts_create",
"arguments": {
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"source": "website",
"tags": [
"newsletter"
]
}
}
}Example response
{
"created": true,
"contact": {
"id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"phone": "+447700900123",
"status": "donor",
"source": "website",
"campaign": "Ramadan 2026",
"tags": [
"monthly",
"volunteer"
],
"fields": {
"city": "Leeds"
},
"email_opt_out": false,
"whatsapp_opt_out": false,
"created_at": "2026-09-14T10:22:05.000Z",
"updated_at": "2026-10-01T08:00:00.000Z"
}
}PATCH /contacts/:id
Change a contact. Only the fields given change; custom fields are merged.
Changes some of a person's details. Fields you leave out stay as they are, and custom fields are merged, so sending {"fields": {"city": "York"}} keeps their other custom fields.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The contact's id.Example "k3Jd9QmZp2Lx" |
first_namebody |
string | Up to 100 characters. |
last_namebody |
string | Up to 100 characters. |
emailbody |
email | null | |
phonebody |
string | null | |
statusbody |
string | Up to 80 characters.Example "donor" |
sourcebody |
string | Up to 200 characters. |
campaignbody |
string | Up to 200 characters. |
tagsbody |
string[] | Up to 50. |
fieldsbody |
object | Custom columns by key, e.g. {"city": "Leeds"}Example {"city":"York"} |
email_opt_outbody |
boolean | |
whatsapp_opt_outbody |
boolean | |
sms_opt_outbody |
boolean |
What comes back: The contact as it is now.
404No contact with that id in this organisation.
curl -X PATCH "https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"donor","fields":{"city":"York"}}'const res = await fetch("https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx", {
method: "PATCH",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"status": "donor",
"fields": {
"city": "York"
}
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.patch(
"https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"status": "donor",
"fields": {
"city": "York"
}
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "contacts_update",
"arguments": {
"id": "k3Jd9QmZp2Lx",
"status": "donor",
"fields": {
"city": "York"
}
}
}
}Example response
{
"id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"phone": "+447700900123",
"status": "donor",
"source": "website",
"campaign": "Ramadan 2026",
"tags": [
"monthly",
"volunteer"
],
"fields": {
"city": "York"
},
"email_opt_out": false,
"whatsapp_opt_out": false,
"created_at": "2026-09-14T10:22:05.000Z",
"updated_at": "2026-10-01T08:00:00.000Z"
}POST /contacts/:id/notes
Add a note to a contact.
Writes a note on someone's record, as the person who made the API key. Good for logging a conversation, a phone call or what an assistant found out.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The contact's id.Example "k3Jd9QmZp2Lx" |
body requiredbody |
string | The note, up to 5,000 characters. Up to 5000 characters.Example "Called to say thank you. Happy to hear about the winter appeal." |
What comes back: The note: { id, body, created_at }.
404No contact with that id.
curl -X POST "https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx/notes" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Called to say thank you. Happy to hear about the winter appeal."}'const res = await fetch("https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx/notes", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"body": "Called to say thank you. Happy to hear about the winter appeal."
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/contacts/k3Jd9QmZp2Lx/notes",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"body": "Called to say thank you. Happy to hear about the winter appeal."
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "notes_add",
"arguments": {
"id": "k3Jd9QmZp2Lx",
"body": "Called to say thank you. Happy to hear about the winter appeal."
}
}
}Example response
{
"id": "Nte9Zz8Yy7Xx",
"body": "Called to say thank you. Happy to hear about the winter appeal.",
"created_at": "2026-10-04T09:30:00.000Z"
}Customer lists
GET /lists
Every list, with how many people are on it.
Every list in the organisation with how many people are on it. Use the ids with contacts_search, lists_add_contacts and letters_queue.
No fields: just call it.
What comes back: { data: [{ id, name, description, members }, …] }, by name.
curl "https://arm.adab.solutions/api/v1/lists" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/lists", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/lists",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "lists_list",
"arguments": {}
}
}Example response
{
"data": [
{
"id": "Lst7Hq2Wn4Ra",
"name": "Monthly givers",
"description": "Anyone with an active standing order",
"members": 214
}
]
}POST /lists/:id/members
Add contacts to a list (fires "joined list" automations).
Puts people on a list. People already on it are left alone and not counted. Each newly added person starts the list's "joined list" automations.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The list's id.Example "Lst7Hq2Wn4Ra" |
contact_ids requiredbody |
string[] | The people to add, 1 to 500 ids. Up to 500.Example ["k3Jd9QmZp2Lx"] |
What comes back: { added }: how many were newly added.
404No list with that id in this organisation.
curl -X POST "https://arm.adab.solutions/api/v1/lists/Lst7Hq2Wn4Ra/members" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids":["k3Jd9QmZp2Lx"]}'const res = await fetch("https://arm.adab.solutions/api/v1/lists/Lst7Hq2Wn4Ra/members", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"contact_ids": [
"k3Jd9QmZp2Lx"
]
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/lists/Lst7Hq2Wn4Ra/members",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"contact_ids": [
"k3Jd9QmZp2Lx"
]
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "lists_add_contacts",
"arguments": {
"id": "Lst7Hq2Wn4Ra",
"contact_ids": [
"k3Jd9QmZp2Lx"
]
}
}
}Example response
{
"added": 1
}Tasks and reminders
GET /tasks
Tasks that are not done yet (or done ones), optionally for one contact.
Things the team has to do. By default only open tasks, soonest due first; send done=true for finished ones.
| Field | Type | Notes |
|---|---|---|
contact_idquery |
string | Only tasks about this person.Example "k3Jd9QmZp2Lx" |
donequery |
boolean | true for finished tasks, false (the default) for open ones. Default false. |
limitquery |
integer | From 1 to 200. Default 50. |
offsetquery |
integer | From 0 to 9007199254740991. Default 0. |
What comes back: { data: [{ id, title, due_on, contact_id, assignee_id, done_at, created_at }, …] }.
curl "https://arm.adab.solutions/api/v1/tasks?contact_id=k3Jd9QmZp2Lx" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/tasks?contact_id=k3Jd9QmZp2Lx", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/tasks",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"contact_id": "k3Jd9QmZp2Lx"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "tasks_list",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx"
}
}
}Example response
{
"data": [
{
"id": "Tsk5Bv8Cx1Ny",
"title": "Call Amina about Gift Aid",
"due_on": "2026-10-07",
"contact_id": "k3Jd9QmZp2Lx",
"assignee_id": "Usr4Ee5Rr6Tt",
"done_at": null,
"created_at": "2026-10-01T09:00:00.000Z"
}
]
}POST /tasks
Create a task, optionally about a contact and for a member (defaults to the key's owner).
Adds a task, optionally about a person and for a team member. Without assignee_id it goes to whoever made the API key.
| Field | Type | Notes |
|---|---|---|
title requiredbody |
string | What needs doing, up to 200 characters. Up to 200 characters.Example "Call Amina about Gift Aid" |
due_onbody |
string | The day it is due, like 2026-10-07.Example "2026-10-07" |
contact_idbody |
string | The person it is about.Example "k3Jd9QmZp2Lx" |
assignee_idbody |
string | The team member who should do it. |
What comes back: { id, title, due_on }.
404The contact was not found.400assignee_id is not a member of this organisation.
curl -X POST "https://arm.adab.solutions/api/v1/tasks" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Call Amina about Gift Aid","due_on":"2026-10-07","contact_id":"k3Jd9QmZp2Lx"}'const res = await fetch("https://arm.adab.solutions/api/v1/tasks", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"title": "Call Amina about Gift Aid",
"due_on": "2026-10-07",
"contact_id": "k3Jd9QmZp2Lx"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/tasks",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"title": "Call Amina about Gift Aid",
"due_on": "2026-10-07",
"contact_id": "k3Jd9QmZp2Lx"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "tasks_create",
"arguments": {
"title": "Call Amina about Gift Aid",
"due_on": "2026-10-07",
"contact_id": "k3Jd9QmZp2Lx"
}
}
}Example response
{
"id": "Tsk5Bv8Cx1Ny",
"title": "Call Amina about Gift Aid",
"due_on": "2026-10-07"
}POST /tasks/:id/done
Mark a task done.
Ticks a task off. Doing it twice is harmless: the first finish time is kept.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The task's id.Example "Tsk5Bv8Cx1Ny" |
What comes back: { id, done_at }.
404No task with that id.
curl -X POST "https://arm.adab.solutions/api/v1/tasks/Tsk5Bv8Cx1Ny/done" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/tasks/Tsk5Bv8Cx1Ny/done", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/tasks/Tsk5Bv8Cx1Ny/done",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "tasks_complete",
"arguments": {
"id": "Tsk5Bv8Cx1Ny"
}
}
}Example response
{
"id": "Tsk5Bv8Cx1Ny",
"done_at": "2026-10-04T11:15:00.000Z"
}Gifts (donations, funds and Gift Aid claims)
GET /donations
Donations, newest first, filtered by contact, fund or date range.
Gifts received, newest first. Refunded gifts are included with refunded_on set, so you can match your own records exactly.
| Field | Type | Notes |
|---|---|---|
contact_idquery |
string | Only this person's gifts.Example "k3Jd9QmZp2Lx" |
fundquery |
string | Only gifts to this fund, e.g. General or Zakat. |
fromquery |
string | Received on or after this day.Example "2026-01-01" |
toquery |
string | Received on or before this day. |
limitquery |
integer | From 1 to 200. Default 50. |
offsetquery |
integer | From 0 to 9007199254740991. Default 0. |
What comes back: { data: [{ id, contact_id, amount, currency, received_on, fund, method, reference, gift_aid, claim_id, pledge_id, refunded_on, created_at }, …] }.
curl "https://arm.adab.solutions/api/v1/donations?contact_id=k3Jd9QmZp2Lx&from=2026-01-01" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/donations?contact_id=k3Jd9QmZp2Lx&from=2026-01-01", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/donations",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"contact_id": "k3Jd9QmZp2Lx",
"from": "2026-01-01"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "donations_list",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx",
"from": "2026-01-01"
}
}
}Example response
{
"data": [
{
"id": "Don2Rt6Yp9Ke",
"contact_id": "k3Jd9QmZp2Lx",
"amount": "50.00",
"currency": "GBP",
"received_on": "2026-09-30",
"fund": "General",
"method": "card",
"reference": "WEB-1042",
"gift_aid": true,
"claim_id": null,
"pledge_id": null,
"refunded_on": null,
"created_at": "2026-09-30T18:02:11.000Z"
}
]
}POST /donations
Record a donation for a contact. Give external_id (your system's id) so a retry never records it twice. Fires thank-you automations.
Records a gift from a person. Send external_id with your own system's id for the gift: if the same external_id comes again (a retry, a re-sync) nothing is recorded twice. Recording a gift starts thank-you automations and sends the donation.created webhook. Tick gift_aid only when they have a valid declaration.
| Field | Type | Notes |
|---|---|---|
contact_id requiredbody |
string | Who gave.Example "k3Jd9QmZp2Lx" |
amount requiredbody |
number | How much, e.g. 25 or 25.50. At most 10000000.Example 25 |
currencybody |
string | Three-letter currency, GBP by default. Default "GBP". |
received_onbody |
string | The day it arrived; today if left out. |
fundbody |
string | Which fund it goes to. General by default. Up to 80 characters. Default "General".Example "Winter appeal" |
methodbody |
string | How it was paid, e.g. card, cash, bank transfer. Up to 60 characters. Default "".Example "card" |
referencebody |
string | Your reference for it, shown on their record. Up to 120 characters. Default "". |
gift_aidbody |
boolean | true if Gift Aid can be claimed on it. Default false.Example true |
pledge_idbody |
string | A pledge of theirs this pays towards. |
external_idbody |
string | Your system's id for the gift, so it is never recorded twice. Up to 200 characters.Example "stripe_pi_3Px" |
What comes back: { recorded: true, donation } or, when the external_id was seen before, { recorded: false, reason }.
404The contact was not found.400The pledge belongs to someone else.
curl -X POST "https://arm.adab.solutions/api/v1/donations" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id":"k3Jd9QmZp2Lx","amount":25,"fund":"Winter appeal","method":"card","gift_aid":true,"external_id":"stripe_pi_3Px"}'const res = await fetch("https://arm.adab.solutions/api/v1/donations", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"contact_id": "k3Jd9QmZp2Lx",
"amount": 25,
"fund": "Winter appeal",
"method": "card",
"gift_aid": true,
"external_id": "stripe_pi_3Px"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/donations",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"contact_id": "k3Jd9QmZp2Lx",
"amount": 25,
"fund": "Winter appeal",
"method": "card",
"gift_aid": True,
"external_id": "stripe_pi_3Px"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "donations_record",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx",
"amount": 25,
"fund": "Winter appeal",
"method": "card",
"gift_aid": true,
"external_id": "stripe_pi_3Px"
}
}
}Example response
{
"recorded": true,
"donation": {
"id": "Don2Rt6Yp9Ke",
"amount": "25.00",
"currency": "GBP",
"received_on": "2026-10-04",
"fund": "Winter appeal"
}
}POST /donations/:id/refund
Mark a donation refunded. It stays on record but stops counting and is never claimed for Gift Aid.
Marks a gift as refunded. It stays on their record for history but no longer counts in totals and is never claimed for Gift Aid. If it was already in a Gift Aid claim, the answer tells you which claim needs adjusting.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The donation's id.Example "Don2Rt6Yp9Ke" |
reasonbody |
string | Why, for the record. Up to 500 characters. Default "".Example "Paid twice by mistake" |
refunded_onbody |
string | The day of the refund; today if left out. |
What comes back: { refunded: true, gift_aid_claim_to_adjust }: the claim id, or null.
404No donation with that id.
curl -X POST "https://arm.adab.solutions/api/v1/donations/Don2Rt6Yp9Ke/refund" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason":"Paid twice by mistake"}'const res = await fetch("https://arm.adab.solutions/api/v1/donations/Don2Rt6Yp9Ke/refund", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"reason": "Paid twice by mistake"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/donations/Don2Rt6Yp9Ke/refund",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"reason": "Paid twice by mistake"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "donations_refund",
"arguments": {
"id": "Don2Rt6Yp9Ke",
"reason": "Paid twice by mistake"
}
}
}Example response
{
"refunded": true,
"gift_aid_claim_to_adjust": null
}GET /donations/summary
Income by month and fund over a date range (default: the last 12 months).
Income totals by month, fund and currency, leaving out refunds. Handy for a dashboard or for an assistant answering "how did the appeal do?".
| Field | Type | Notes |
|---|---|---|
fromquery |
string | First day to count; 12 months ago if left out.Example "2026-09-01" |
toquery |
string | Last day to count; today if left out.Example "2026-09-30" |
What comes back: { data: [{ month, fund, currency, gifts, total }, …] } in month order.
curl "https://arm.adab.solutions/api/v1/donations/summary?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/donations/summary?from=2026-09-01&to=2026-09-30", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/donations/summary",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"from": "2026-09-01",
"to": "2026-09-30"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "donations_summary",
"arguments": {
"from": "2026-09-01",
"to": "2026-09-30"
}
}
}Example response
{
"data": [
{
"month": "2026-09",
"fund": "General",
"currency": "GBP",
"gifts": 42,
"total": 3150.5
}
]
}GET /recurring-gifts
Active recurring gifts (standing orders, Direct Debits, card subscriptions).
Regular gifts that are still running: standing orders, Direct Debits and card subscriptions.
| Field | Type | Notes |
|---|---|---|
contact_idquery |
string | Only this person's.Example "k3Jd9QmZp2Lx" |
What comes back: { data: [{ id, contact_id, amount, currency, frequency, fund, method, gift_aid, started_on }, …] }.
curl "https://arm.adab.solutions/api/v1/recurring-gifts?contact_id=k3Jd9QmZp2Lx" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/recurring-gifts?contact_id=k3Jd9QmZp2Lx", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/recurring-gifts",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"contact_id": "k3Jd9QmZp2Lx"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "recurring_gifts_list",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx"
}
}
}Example response
{
"data": [
{
"id": "Rec3Qq4Ww5Ee",
"contact_id": "k3Jd9QmZp2Lx",
"amount": "10.00",
"currency": "GBP",
"frequency": "monthly",
"fund": "General",
"method": "direct_debit",
"gift_aid": true,
"started_on": "2025-01-01"
}
]
}GET /donors/rfm
Donors scored 1-5 on recency, frequency and money given, with a segment: Champions, Loyal, Big givers, New, At risk, Lapsed, Needs attention.
Scores every donor from 1 to 5 on how recently, how often and how much they give, and names a segment. Use it to choose who to thank, upgrade or win back.
| Field | Type | Notes |
|---|---|---|
segmentquery |
"Champions" | "Loyal" | "Big givers" | "New" | "At risk" | "Lapsed" | "Needs attention" | Only donors in this segment.Example "At risk" |
contact_idquery |
string | Only this donor. |
currencyquery |
string | Which currency to score in, GBP by default. Default "GBP". |
limitquery |
integer | How many, 1 to 2,000. From 1 to 2000. Default 200.Example 50 |
What comes back: { data: [{ contact_id, first_name, last_name, email, last_gift, gifts, total, r, f, m, segment }, …] }. r, f and m are the 1 to 5 scores; up to 500 donors.
curl "https://arm.adab.solutions/api/v1/donors/rfm?segment=At+risk&limit=50" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/donors/rfm?segment=At+risk&limit=50", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/donors/rfm",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"segment": "At risk",
"limit": 50
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "donors_rfm",
"arguments": {
"segment": "At risk",
"limit": 50
}
}
}Example response
{
"data": [
{
"contact_id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"email": "amina@example.org",
"last_gift": "2025-11-02",
"gifts": 9,
"total": 640,
"r": 2,
"f": 4,
"m": 4,
"segment": "At risk"
}
]
}Pledges: promises to give, tracked against donations
GET /pledges
Pledges with how much has been paid and their status (open, overdue, fulfilled, cancelled).
Promises to give, with how much has come in against each.
| Field | Type | Notes |
|---|---|---|
contact_idquery |
string | Only this person's pledges. |
statusquery |
"open" | "overdue" | "fulfilled" | "cancelled" | open, overdue, fulfilled or cancelled.Example "overdue" |
What comes back: { data: [{ id, contact_id, amount, currency, fund, due_on, notes, cancelled_on, created_at, paid, status }, …] }: open and overdue first, up to 500.
curl "https://arm.adab.solutions/api/v1/pledges?status=overdue" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/pledges?status=overdue", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/pledges",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"status": "overdue"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "pledges_list",
"arguments": {
"status": "overdue"
}
}
}Example response
{
"data": [
{
"id": "Plg4Mn7Qs2Vb",
"contact_id": "k3Jd9QmZp2Lx",
"amount": "500.00",
"currency": "GBP",
"fund": "Building",
"due_on": "2026-09-01",
"notes": "",
"cancelled_on": null,
"created_at": "2026-06-01T10:00:00.000Z",
"paid": 200,
"status": "overdue"
}
]
}POST /pledges
Record a promise to give. Donations recorded with its pledge_id count towards it.
Records a promise to give. Gifts recorded later with this pledge_id count towards it, and it becomes fulfilled when they add up.
| Field | Type | Notes |
|---|---|---|
contact_id requiredbody |
string | Who promised.Example "k3Jd9QmZp2Lx" |
amount requiredbody |
number | How much they promised. At most 10000000.Example 500 |
currencybody |
string | Three-letter currency, GBP by default. Default "GBP". |
fundbody |
string | Which fund. General by default. Up to 80 characters. Default "General".Example "Building" |
due_onbody |
string | When it should be paid by.Example "2026-12-31" |
notesbody |
string | Anything worth remembering. Up to 2000 characters. Default "". |
What comes back: { id }: the new pledge's id. Send it as pledge_id when recording the gifts that pay it.
curl -X POST "https://arm.adab.solutions/api/v1/pledges" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id":"k3Jd9QmZp2Lx","amount":500,"fund":"Building","due_on":"2026-12-31"}'const res = await fetch("https://arm.adab.solutions/api/v1/pledges", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"contact_id": "k3Jd9QmZp2Lx",
"amount": 500,
"fund": "Building",
"due_on": "2026-12-31"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/pledges",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"contact_id": "k3Jd9QmZp2Lx",
"amount": 500,
"fund": "Building",
"due_on": "2026-12-31"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "pledges_create",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx",
"amount": 500,
"fund": "Building",
"due_on": "2026-12-31"
}
}
}Example response
{
"id": "Plg4Mn7Qs2Vb"
}Sponsorship schemes (orphans, students...)
GET /beneficiaries
Sponsorship beneficiaries and their current sponsor, if any.
People or projects that can be sponsored, with their sponsor if they have one.
| Field | Type | Notes |
|---|---|---|
schemequery |
string | Only this sponsorship scheme. |
unsponsoredquery |
boolean | true to see only those still waiting for a sponsor. Default false.Example true |
What comes back: { data: [{ id, code, name, scheme, country, sponsor_id, amount, currency, started_on }, …] }.
curl "https://arm.adab.solutions/api/v1/beneficiaries?unsponsored=true" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/beneficiaries?unsponsored=true", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/beneficiaries",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"unsponsored": True
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "beneficiaries_list",
"arguments": {
"unsponsored": true
}
}
}Example response
{
"data": [
{
"id": "Ben5Tt6Yy7Uu",
"code": "ORP-0142",
"name": "Yusuf",
"scheme": "Orphan care",
"country": "Kenya",
"sponsor_id": null,
"amount": null,
"currency": null,
"started_on": null
}
]
}Call centre: calling campaigns with outcomes and targets
GET /call-campaigns
Calling campaigns with calls made, money raised and pledged against target.
Phone campaigns with progress: calls made, money raised and pledged, against the target.
No fields: just call it.
What comes back: { data: [{ id, name, list_id, target, active, created_at, calls, people, raised, pledged, outcomes }, …] }.
curl "https://arm.adab.solutions/api/v1/call-campaigns" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/call-campaigns", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/call-campaigns",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "call_campaigns_list",
"arguments": {}
}
}Example response
{
"data": [
{
"id": "Cmp8Fz3Gh6Jw",
"name": "Winter thank-you calls",
"list_id": "Lst7Hq2Wn4Ra",
"target": 5000,
"active": true,
"created_at": "2026-09-01T09:00:00.000Z",
"calls": 38,
"people": 35,
"raised": 1240,
"pledged": 600,
"outcomes": [
{
"outcome": "donated",
"n": 12
}
]
}
]
}POST /call-campaigns/:id/next
Take the next person to call in a campaign (or your call still open). Returns the call id and the contact.
Gives you the next person to ring in a campaign, and holds them for you so nobody else calls them at the same time. If you already have a call open, you get that one back.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The campaign's id.Example "Cmp8Fz3Gh6Jw" |
What comes back: { call_id, contact } or, when nobody is left, { call: null, message }.
curl -X POST "https://arm.adab.solutions/api/v1/call-campaigns/Cmp8Fz3Gh6Jw/next" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/call-campaigns/Cmp8Fz3Gh6Jw/next", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/call-campaigns/Cmp8Fz3Gh6Jw/next",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "calls_next",
"arguments": {
"id": "Cmp8Fz3Gh6Jw"
}
}
}Example response
{
"call_id": "Cal1Xs5Dk8Tq",
"contact": {
"id": "k3Jd9QmZp2Lx",
"first_name": "Amina",
"last_name": "Khan",
"phone": "+447700900123",
"email": "amina@example.org"
}
}POST /calls/:id
Close your open call with its outcome. donated records a donation, pledged a pledge, callback a task.
Closes your open call with what happened. donated also records the gift (send amount), pledged records a pledge, and callback makes a task for callback_on.
| Field | Type | Notes |
|---|---|---|
id requiredpath |
string | The call id from calls_next.Example "Cal1Xs5Dk8Tq" |
outcome requiredbody |
"answered" | "donated" | "pledged" | "callback" | "no_answer" | "voicemail" | "declined" | "wrong_number" | "do_not_call" | What happened on the call.Example "donated" |
notesbody |
string | Notes from the call. Up to 5000 characters. Default "".Example "Gave over the phone." |
amountbody |
number | For donated or pledged: how much. At most 10000000.Example 30 |
fundbody |
string | The fund for the gift or pledge. Up to 80 characters. Default "General". |
callback_onbody |
string | For callback: the day to ring again. |
What comes back: { logged: true }.
curl -X POST "https://arm.adab.solutions/api/v1/calls/Cal1Xs5Dk8Tq" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"outcome":"donated","amount":30,"notes":"Gave over the phone."}'const res = await fetch("https://arm.adab.solutions/api/v1/calls/Cal1Xs5Dk8Tq", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"outcome": "donated",
"amount": 30,
"notes": "Gave over the phone."
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/calls/Cal1Xs5Dk8Tq",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"outcome": "donated",
"amount": 30,
"notes": "Gave over the phone."
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "calls_log",
"arguments": {
"id": "Cal1Xs5Dk8Tq",
"outcome": "donated",
"amount": 30,
"notes": "Gave over the phone."
}
}
}Example response
{
"logged": true
}Letters: merge-field letters and a print queue
POST /letters
Queue a letter template for contacts (or everyone on a list) to be printed. Contacts missing a merge field are skipped.
Puts a letter template in the print queue for some people, or everyone on a list. People missing something the letter needs (like an address) are skipped and counted.
| Field | Type | Notes |
|---|---|---|
template_id requiredbody |
string | The letter template.Example "Tpl6Wq9Er3Zu" |
contact_idsbody |
string[] | The people to write to. Up to 5000. Default []. |
list_idbody |
string | Or everyone on this list.Example "Lst7Hq2Wn4Ra" |
What comes back: { queued, skipped, already }: letters queued, people skipped for missing details, and people who already had this letter waiting.
400Send contact_ids or a list_id.
curl -X POST "https://arm.adab.solutions/api/v1/letters" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id":"Tpl6Wq9Er3Zu","list_id":"Lst7Hq2Wn4Ra"}'const res = await fetch("https://arm.adab.solutions/api/v1/letters", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"template_id": "Tpl6Wq9Er3Zu",
"list_id": "Lst7Hq2Wn4Ra"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/letters",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"template_id": "Tpl6Wq9Er3Zu",
"list_id": "Lst7Hq2Wn4Ra"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "letters_queue",
"arguments": {
"template_id": "Tpl6Wq9Er3Zu",
"list_id": "Lst7Hq2Wn4Ra"
}
}
}Example response
{
"queued": 212,
"skipped": 2,
"already": 0
}Email + WhatsApp templates and builder
GET /templates
Email and WhatsApp templates that can be sent.
The email and WhatsApp templates you can send with messages_send.
No fields: just call it.
What comes back: { data: [{ id, channel, name, subject, active }, …] }.
curl "https://arm.adab.solutions/api/v1/templates" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/templates", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/templates",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "templates_list",
"arguments": {}
}
}Example response
{
"data": [
{
"id": "Tpl6Wq9Er3Zu",
"channel": "email",
"name": "Thank you for your gift",
"subject": "Thank you, {{first_name}}",
"active": true
}
]
}POST /messages
Send an email or WhatsApp template to a contact through the outbox (opt-outs, caps and pauses apply).
Sends one template to one person through the outbox, exactly as the team would. Opt-outs, daily caps and paused channels are respected, so a message may be held or blocked rather than sent at once; check its status with messages_list.
| Field | Type | Notes |
|---|---|---|
contact_id requiredbody |
string | Who to send to.Example "k3Jd9QmZp2Lx" |
template_id requiredbody |
string | An active template (see templates_list).Example "Tpl6Wq9Er3Zu" |
What comes back: { queued, message_id }. queued is false when nothing could be sent, for example because they opted out.
400The template is switched off or does not exist.404The contact was not found.
curl -X POST "https://arm.adab.solutions/api/v1/messages" \
-H "Authorization: Bearer $ADAAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id":"k3Jd9QmZp2Lx","template_id":"Tpl6Wq9Er3Zu"}'const res = await fetch("https://arm.adab.solutions/api/v1/messages", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"contact_id": "k3Jd9QmZp2Lx",
"template_id": "Tpl6Wq9Er3Zu"
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.post(
"https://arm.adab.solutions/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
json={
"contact_id": "k3Jd9QmZp2Lx",
"template_id": "Tpl6Wq9Er3Zu"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "messages_send",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx",
"template_id": "Tpl6Wq9Er3Zu"
}
}
}Example response
{
"queued": true,
"message_id": "Msg3Ui7Op1As"
}Account, usage and message history
GET /account
The organisation this key belongs to: its plan and billing status, the features it has, and the operations this key may call.
Who am I? The organisation the key belongs to, its plan and whether it is paused, the features it has and exactly which operations this key may call. A good first call to check a key works.
No fields: just call it.
What comes back: { organisation, plan, features, key }.
curl "https://arm.adab.solutions/api/v1/account" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/account", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/account",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "account_get",
"arguments": {}
}
}Example response
{
"organisation": {
"id": "Org1Nn2Mm3Bb",
"name": "Hope Foundation",
"slug": "hope",
"timezone": "Europe/London"
},
"plan": {
"id": "growth",
"name": "Growth",
"status": "active",
"trial_ends_at": null,
"locked": false,
"seats": {
"used": 4,
"limit": 10
}
},
"features": [
"contacts",
"lists",
"donations",
"api"
],
"key": {
"id": "Key9Lk2Jh5Gf",
"permissions": [
"contacts:read"
],
"operations": [
"contacts_search",
"contacts_get"
]
}
}GET /usage
What the organisation used between two dates (default: this calendar month): messages sent and received by channel, workflow runs, API requests, supporters and storage.
What the organisation used over a period: messages sent by channel and received, automation runs, API requests, supporters and storage. Dates are in the organisation's time zone.
| Field | Type | Notes |
|---|---|---|
fromquery |
string | First day; the start of this month if left out.Example "2026-09-01" |
toquery |
string | Last day, included; now if left out.Example "2026-09-30" |
What comes back: { from, to, supporters, storage_bytes, messages: { sent, sent_total, received }, workflow_runs, api_requests }.
curl "https://arm.adab.solutions/api/v1/usage?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/usage?from=2026-09-01&to=2026-09-30", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/usage",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"from": "2026-09-01",
"to": "2026-09-30"
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "usage_get",
"arguments": {
"from": "2026-09-01",
"to": "2026-09-30"
}
}
}Example response
{
"from": "2026-08-31T23:00:00.000Z",
"to": "2026-09-30T23:00:00.000Z",
"supporters": 1840,
"storage_bytes": 5242880,
"messages": {
"sent": {
"email": 920,
"whatsapp": 140
},
"sent_total": 1060,
"received": 88
},
"workflow_runs": 310,
"api_requests": 4521
}GET /messages
The message history, newest first: every email, WhatsApp, SMS, Instagram and Apple message sent or received. Filter by channel, direction, status, supporter or date.
Every message sent or received, newest first, with its status and any error. Use it to check what happened to a message from messages_send, or to read someone's conversation.
| Field | Type | Notes |
|---|---|---|
channelquery |
"email" | "whatsapp" | "sms" | "instagram" | "apple" | email, whatsapp, sms, instagram or apple.Example "email" |
directionquery |
"in" | "out" | in for received, out for sent. |
statusquery |
"queued" | "sending" | "sent" | "delivered" | "read" | "failed" | "blocked" | "dry_run" | "received" | Only messages with this status. |
contact_idquery |
string | Only this person's messages.Example "k3Jd9QmZp2Lx" |
sincequery |
string | From this day. |
untilquery |
string | Up to and including this day. |
limitquery |
integer | From 1 to 200. Default 50.Example 10 |
offsetquery |
integer | From 0 to 9007199254740991. Default 0. |
What comes back: { data: [message, …], total, limit, offset }.
curl "https://arm.adab.solutions/api/v1/messages?contact_id=k3Jd9QmZp2Lx&channel=email&limit=10" \
-H "Authorization: Bearer $ADAAB_API_KEY"const res = await fetch("https://arm.adab.solutions/api/v1/messages?contact_id=k3Jd9QmZp2Lx&channel=email&limit=10", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.ADAAB_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const data = await res.json();import os, requests
res = requests.get(
"https://arm.adab.solutions/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['ADAAB_API_KEY']}"},
params={
"contact_id": "k3Jd9QmZp2Lx",
"channel": "email",
"limit": 10
},
)
res.raise_for_status()
data = res.json(){
"method": "tools/call",
"params": {
"name": "messages_list",
"arguments": {
"contact_id": "k3Jd9QmZp2Lx",
"channel": "email",
"limit": 10
}
}
}Example response
{
"data": [
{
"id": "Msg3Ui7Op1As",
"contact_id": "k3Jd9QmZp2Lx",
"channel": "email",
"direction": "out",
"status": "delivered",
"recipient": "amina@example.org",
"subject": "Thank you, Amina",
"body": "…",
"template_id": "Tpl6Wq9Er3Zu",
"automation_id": null,
"broadcast_id": null,
"error": null,
"created_at": "2026-10-04T09:31:00.000Z",
"sent_at": "2026-10-04T09:31:02.000Z"
}
],
"total": 1,
"limit": 10,
"offset": 0
}