Skip to content

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

Sign-in codes

A sign-in agent sends a code or a link to one person who asked for it. The same code to the same address is held once it repeats.

A sign-in agent sends a code or a one-time link to one person who just asked to sign in. The From address is on a domain you have verified, and each send goes to that one address.

The ceiling

PATCH /limits/keys/{id} for this key:

{
  "budget_per_period": 500,
  "period": "monthly",
  "rate_ceiling_per_minute": 60
}

Both numbers are lower than a new key: 1,000 a monthly period, and 600 a minute. A burst past the rate ceiling is refused and can be retried after the wait the refusal names.

The send

/**
 * Supabase "Send Email Hook" — you own the auth email instead of Supabase's
 * built-in SMTP.
 *
 * Supabase POSTs the sign-up / magic-link / recovery payload to an HTTPS
 * endpoint you host, signed with Standard Webhooks headers. This handler
 * verifies that signature, renders the link, and sends it through AgentiSend.
 * It is a plain `Request` -> `Response` function, so it drops into a Next.js
 * route handler, a Supabase Edge Function, Hono, or anything else that speaks
 * fetch.
 */
import { createHmac, timingSafeEqual } from 'node:crypto';
import { AgentiSend, AgentiSendError } from 'agentisend';

const agentisend = new AgentiSend();

function mailFrom(): string {
  const from = process.env.MAIL_FROM;
  if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
  return from;
}

/** The payload Supabase sends. Only the fields this handler reads are typed. */
export interface SendEmailHookPayload {
  user: { id: string; email: string };
  email_data: {
    token: string;
    token_hash: string;
    redirect_to: string;
    email_action_type: 'signup' | 'recovery' | 'invite' | 'magiclink' | 'email_change';
    site_url: string;
  };
}

const SUBJECTS: Record<SendEmailHookPayload['email_data']['email_action_type'], string> = {
  signup: 'Confirm your email',
  recovery: 'Reset your password',
  invite: 'You have been invited',
  magiclink: 'Your sign-in link',
  email_change: 'Confirm your new email',
};

/**
 * Standard Webhooks verification, as Supabase signs it: the signed content is
 * `id.timestamp.body`, HMAC-SHA256 under the base64 secret, and the header
 * carries one or more space-separated `v1,<signature>` pairs.
 */
export function verifySignature(
  secret: string,
  headers: { id: string; timestamp: string; signature: string },
  body: string,
): boolean {
  const key = Buffer.from(secret.replace(/^v1,whsec_|^whsec_/, ''), 'base64');
  const expected = createHmac('sha256', key)
    .update(`${headers.id}.${headers.timestamp}.${body}`)
    .digest('base64');
  return headers.signature.split(' ').some((candidate) => {
    const value = candidate.startsWith('v1,') ? candidate.slice(3) : candidate;
    const a = Buffer.from(value);
    const b = Buffer.from(expected);
    return a.length === b.length && timingSafeEqual(a, b);
  });
}

export async function POST(request: Request): Promise<Response> {
  const secret = process.env.SEND_EMAIL_HOOK_SECRET;
  if (!secret) throw new Error('Set SEND_EMAIL_HOOK_SECRET to the secret Supabase generated.');

  const body = await request.text();
  const ok = verifySignature(secret, {
    id: request.headers.get('webhook-id') ?? '',
    timestamp: request.headers.get('webhook-timestamp') ?? '',
    signature: request.headers.get('webhook-signature') ?? '',
  }, body);
  if (!ok) {
    return Response.json({ error: { http_code: 401, message: 'Bad signature' } }, { status: 401 });
  }

  const { user, email_data } = JSON.parse(body) as SendEmailHookPayload;
  const link =
    `${email_data.site_url}/auth/v1/verify` +
    `?token=${email_data.token_hash}` +
    `&type=${email_data.email_action_type}` +
    `&redirect_to=${encodeURIComponent(email_data.redirect_to)}`;

  try {
    await agentisend.emails.send(
      {
        from: mailFrom(),
        to: user.email,
        subject: SUBJECTS[email_data.email_action_type],
        html: `<p><a href="${link}">Continue</a></p><p>Or enter this code: ${email_data.token}</p>`,
      },
      // One token, one email. Supabase retries the hook on a timeout; this key
      // makes the retry replay rather than send a second copy.
      { idempotencyKey: `supabase-auth/${email_data.token_hash}` },
    );
  } catch (err) {
    if (err instanceof AgentiSendError) {
      // Supabase surfaces this message to the end user's sign-up attempt, so
      // hand back the fix rather than a stack trace.
      return Response.json(
        { error: { http_code: err.status, message: err.fix } },
        { status: err.status },
      );
    }
    throw err;
  }

  return Response.json({});
}
examples/supabase-auth-hook/handler.ts, run by the test suite against a local AgentiSend server

The starter is Sign-in code.

The refusal

The same code text to the same address is held when it would be the 4th near-identical send inside 60 minutes. 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

A sign-in code is the same words every time, so the loop guard treats a retry storm as a repeat. Send a new code only when the person asked again, and reuse the idempotency key when the request is the same send.

Next