Skip to content

Use cases · Published 2026-09-29 · AgentiSend team

Onboarding sequence run by an agent

An agent sends the first welcome, then later notes on a schedule, to someone who just created an account. There is no delay step.

An agent sends a welcome to someone who just created an account with you, then later notes in that same sequence. Each note has its own send time, and each recipient already took the step that starts the sequence.

The ceiling

PATCH /limits/keys/{id} for the onboarding key:

{
  "budget_per_period": 200,
  "period": "monthly",
  "rate_ceiling_per_minute": 10
}

Two hundred is under the Free inclusion of 1,000 a monthly period. Ten a minute is under 600. A key can only lower these. The plan inclusion is still the account cap.

The send

"""FastAPI: one POST route that sends a welcome email.

    POST /send  {"email": "ada@example.com", "name": "Ada"}
    200         {"id": "..."}                     accepted
    400         {"error": "..."}                  not one address; the API was not called
    4xx/5xx     {"code": "...", "fix": "..."}     the API refused, and says what to do

Environment: AGENTISEND_API_KEY (a key with sending_access), MAIL_FROM (an
address on a domain you have verified), and optionally AGENTISEND_BASE_URL.
Run: uvicorn app:app
"""

from __future__ import annotations

import os
import re

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from agentisend import AgentiSend, AgentiSendError, idempotency_key

agentisend = AgentiSend()  # reads AGENTISEND_API_KEY and AGENTISEND_BASE_URL
MAIL_FROM = os.environ["MAIL_FROM"]

# One address, no spaces, a dot in the domain. The API checks properly; this
# only stops a form typo from costing a request.
ADDRESS = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

app = FastAPI()


class Signup(BaseModel):
    email: str
    name: str = "there"


@app.post("/send")
def send(signup: Signup) -> JSONResponse:
    email = signup.email.strip()
    if not ADDRESS.match(email):
        return JSONResponse({"error": "email must be one address, like you@example.com"}, status_code=400)

    try:
        sent = agentisend.send_email(
            {
                "from": MAIL_FROM,
                "to": email,
                "subject": "Welcome",
                "text": f"Hi {signup.name}, your account is ready.",
            },
            # Derived from the thing being done, never from the moment: a retry
            # after a timeout replays the first send instead of mailing twice.
            idempotency=idempotency_key("welcome", email),
        )
    except AgentiSendError as err:
        return JSONResponse({"code": err.code, "fix": err.fix}, status_code=err.status)
    return JSONResponse({"id": sent["id"]})
examples/fastapi/app.py, run by the test suite against a local AgentiSend server

The welcome starter is Welcome. A later note is a scheduled send: set scheduled_at on that message. The furthest it can sit is 30 days out.

The refusal

The same welcome text to the same address is held when it repeats inside the loop window. The code and the fix:

{
  "error": {
    "code": "approval_required",
    "message": "Blocked: this agent has sent 3 near-identical emails to customer@example.com within 60 minutes, which matches a retry or script loop; review the agent before sending more.",
    "fix": "Do not send it again: a person approves or rejects it in the console under Agents → Approvals, and approving sends it — the message then appears in GET /emails. action_id in this error names the held send; the key that asked cannot approve itself, and a retry waits on the same approval.",
    "docs_url": "https://agentisend.com/what-does-approval-required-mean#approval_required",
    "retryable": false
  }
}

Honest limit

An automation step is an email, a webhook, or a split. There is no delay step, so the agent schedules each later message itself. A scheduled welcome is counted against the budget when it is accepted, and cancelling it before it sends gives that count back.

Next