What you will build
An agency, a group with several brands, a platform sending for its customers: one account ends up holding dozens of domains and IPs that belong to different owners. Resource groups put a name on each set, and most read endpoints take a group filter, so every figure can be read per brand or per client. The API lets you:
- mirror your client list: one group per client, created and filled from your CRM or your billing;
- produce one report per client by calling the same endpoints with a different
group; - onboard domains faster by claiming those your ESP or Google Postmaster already know about.
A group is a filter, not a permission: every member of the account still sees every domain. To separate access for real, use separate accounts.
Before you start
- An API key created from the dashboard, under Account, Developer, on a plan that includes API access and resource groups.
- Verified domains or IPs to group: see API: monitor your domains and API: monitor your sending IPs.
export SA_KEY="sa_live_your_key"
export SA_API="https://senderaudit.com/api/v1"
import os, requests
API = "https://senderaudit.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SA_KEY']}"
Step 1: create the groups
POST /groups creates a group from a name, unique on the account.
curl -s -X POST "$SA_API/groups" \
-H "Authorization: Bearer $SA_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Retail"}'
group = session.post(f"{API}/groups", json={"name": "Acme Retail"}).json()
group_id = group["id"]
GET /groups lists them with their member counts, how many your plan allows (limit) and how many are active (active_count). The plan limit counts active groups: PATCH /groups/{id} with {"is_active": false} archives a group, for a client who left, without losing it, and frees its place.
Step 2: fill them
Two ways, depending on where your source of truth sits.
From the group: PUT /groups/{id}/members replaces the whole membership with the lists you send. Idempotent, which makes it the right call for a sync job.
session.put(f"{API}/groups/{group_id}/members", json={
"domains": ["6f1c2b9e-3d4a-4e8f-9a51-0c7d2e8b4f13", "0b7e5a2c-..."],
"ips": ["c2d9e1f0-..."],
})
From the domain or the IP: PUT /groups/membership/domain/{domain_id} (and its IP twin) sets the groups of one asset, as a list of group ids. A domain can belong to several groups.
Everything is identified by its id, the UUID returned by GET /domains, GET /ips and GET /groups. GET /groups/assignable returns, in one call, every verified domain and IP with the groups it already belongs to: the starting point to compare with your own list.
A sync job
# clients = {"Acme Retail": ["acme.com", "acme-shop.com"], ...} from your CRM
assignable = session.get(f"{API}/groups/assignable").json()
domain_ids = {d["label"]: d["id"] for d in assignable["domains"]}
groups = {g["name"]: g["id"] for g in session.get(f"{API}/groups").json()["groups"]}
for client, domains in clients.items():
gid = groups.get(client) or session.post(f"{API}/groups", json={"name": client}).json()["id"]
wanted = [domain_ids[d] for d in domains if d in domain_ids]
session.put(f"{API}/groups/{gid}/members", json={"domains": wanted, "ips": []})
Step 3: read per group
Pass the group’s id as group to scope the figures:
| Endpoint | Scoped by group |
|---|---|
GET /overview | Score, KPIs, actions and assets of the group only |
GET /domains, GET /ips | The group’s members |
GET /dmarc/... (overview, stats, daily, domains-summary, senders, reports, reporters) | Reports of the group’s domains |
GET /tlsrpt/... (summary, stats, reports) | Reports of the group’s domains |
GET /recommendations | Recommendations on the group’s domains |
for name, gid in groups.items():
ov = session.get(f"{API}/overview", params={"group": gid}).json()
dmarc = session.get(f"{API}/dmarc/overview", params={"group": gid, "days": 30}).json()
print(f'{name:20} score {ov["global"]["score"]} DMARC compliance {dmarc["kpis"]["compliance"]}')
group id that does not exist, belongs to another account or points to an archived group is ignored: the call answers with the figures of the whole account, without an error. It keeps a stale filter from breaking a screen, but in a per-client report it would show one client everybody’s data. Check the id against GET /groups, and that the group is active, before using it.domain wins over group when you pass both.
Going further
Domains your integrations discovered
When an ESP or Google Postmaster Tools is connected (see API: analyse your sending performance), the sending domains they know about become candidates. GET /domains/candidates lists them, each with where it came from (discovered_from, provider_label) and the DKIM selectors found.
POST /domains/candidates/{id}/claimadds it to the account. Whenintegration_verifiedistrue, the integration already proves you control it and the domain is added verified, with nothing to publish. Otherwise it is added unverified and goes through the usual verification.POST /domains/candidates/{id}/dismisssets it aside. It only comes back if it is discovered again.
The claim answers with the domain’s id as domain_id: pass it straight to PUT /groups/membership/domain/{domain_id} to add it to its group.
Quotas
Every client added eats into the same allowances. Check them before a batch rather than halfway through:
| Endpoint | What it counts |
|---|---|
GET /domains/quota | Monitored hosts, and the plan’s limits |
GET /ips/quota | Monitored IP addresses |
GET /dmarc/quota | DMARC messages this month, when the plan has a limit |
GET /integrations/esp/quota | ESP events over the period |
GET /auth/me/scan-usage | Scans today |
Deleting a group
DELETE /groups/{id} deletes the group and its memberships. The domains and IPs stay, with their history.
Limits and errors
| Situation | Answer |
|---|---|
| Plan without resource groups | 403 |
| Plan’s limit of active groups reached | 400 |
| Group name already used | 409 |
Group, or single domain or IP, id unknown on the account | 404 |
Unknown domain or IP in a members list | 400, nothing is changed |
| Too many calls on an endpoint | 429, with a Retry-After header |
| Missing or invalid key | 401 |
Related
- API reference: Groups and Domains
- API: supervise your account
- API: monitor your domains