Questions · Updated 2026-09-28
How do I restrict an API key to one domain?
Restrict an API key to one domain with the domain_scope field on POST /api-keys, or on an existing key with PATCH /api-keys/{id}. A send from any other domain is refused with domain_scope_violation before the suppression, trust and domain gates run, and GET /api-keys shows the scope on every key.
Restrict an API key to one domain by setting domain_scope to that domain's name when you create the key with POST /api-keys, or on an existing key with PATCH /api-keys/{id}. The scope is compared with the From domain on every send. A send from any other domain is refused with domain_scope_violation, whose catalogue message is "This API key may only send from its scoped domain." GET /api-keys returns domain_scope on every key, so a scoped key is visible as one.
Set it
domain_scope has to name a domain already on the account. Otherwise the key is not created, and the refusal is invalid_parameter with the message "There is no domain "example.com" on this account, so a key cannot be scoped to it. Register it with POST /domains first, or leave domain_scope out." Its fix says "Register the domain with POST /domains first, then scope the key to it — or leave domain_scope out so the key can send from every domain on this account. GET /domains lists the domains you have." Registered is enough for the scope; the domain still has to verify before the key sends, and until then the send meets domain_not_verified at the domain gate, which is the subject of What does domain_not_verified mean?
PATCH /api-keys/{id} changes a key in place. The summary is "Rename a key or change its domain scope and scopes. The token is unchanged — use POST /api-keys/:id/rotate for that." The body must carry at least one of name, domain_scope or scopes. Send "domain_scope": null to clear the scope. The new scope applies from the key's next request, so an over-wide key is narrowed where it is rather than replaced.
curl -sS -X PATCH https://api.agentisend.com/api-keys/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42 \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"200
{
"budget_per_period": 1,
"created_at": "2026-09-04T09:14:00Z",
"domain_scope": "string",
"expires_at": "2026-09-04T09:14:00Z",
"id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
"last_used_at": "2026-09-04T09:14:00Z",
"name": "yourdomain.com",
"period": "hourly"
}What the send path does
The scope check runs before the blocklist, the suppression list, the trust gate and the domain gate, so a scoped key sending from the wrong domain hears about the scope and nothing else. The refusal names both domains: "This key may only send from example.com, not other.example." The catalogue fix is "Send from the scoped domain, or create a key without a domain scope via POST /api-keys." retryable is false, so change from or use another key.
POST /emails/preflight reports the same decision without sending. Its checks.domain_scope has ok: false and the detail "Key is scoped to example.com, not other.example." The one exception is the onboarding sender: a scoped key may still send from onboarding@agentisend.com to a member sign-in address while its own domain is pending, because that path is checked by its own rules and not by the scope.
What the key can read
For a sending_access key the scope narrows reads as well as sends. GET /emails and GET /emails/{id} return only messages that key sent from its scoped domain. A full_access key reads every message on the account whatever its scope, because the scope is a rule about sending, not about reading.
GET /api-keys states the scope alongside the rest. Its summary says permission, domain scope and the key's own budget ceiling are always returned, and the token never is. Pair the scope with a budget from PATCH /limits/keys/{id} when the key belongs to an agent: the scope decides where it may send from, the budget decides how much. Which API key permission should an AI agent have? covers the permission itself.