SendpositoryDocs

Quotas & rate limits

How much you can send, how fast, what happens when you reach the limit, and how the daily and monthly caps interact.

Updated

Two separate limits apply. They fail differently, so handle them differently.

Monthly quota

Your plan includes a number of emails per calendar month (UTC). Usage is visible in the sidebar and on Metrics. Each plan's current monthly and daily limits are on the pricing page.

Usage and deliverability on the Metrics page

At the limit, sending stops:

{
  "error": {
    "type": "quota_exceeded",
    "message": "Monthly quota of 50,000 emails reached. Upgrade your plan or wait for the next billing period."
  }
}

Returned as HTTP 402. It's a hard stop - we never send past your quota and bill you for the overage. The counter resets at the start of the next month.

Reservations are atomic

Quota is reserved before the message is handed to the provider, in a single statement. Two simultaneous requests at your last remaining email can't both succeed. If the send then fails, the reservation is given back.

Rate limit

Requests per second, per API key, set by your plan. Over it:

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "Too many requests. Retry in 2s."
  }
}

Returned as HTTP 429 with a Retry-After header in seconds.

Wait for that long. Retrying immediately makes it worse - you consume the budget you're waiting for. A simple, correct handler:

async function send(payload: unknown, attempt = 0): Promise<{ id: string }> {
  const res = await fetch("https://api.sendpository.com/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDPOSITORY_API_KEY}`,
      "Content-Type": "application/json",
      // Same key across retries, so a timeout can never send twice.
      "Idempotency-Key": `welcome-${(payload as { userId: string }).userId}`,
    },
    body: JSON.stringify(payload),
  });

  if (res.status === 429 && attempt < 3) {
    const wait = Number(res.headers.get("retry-after") ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return send(payload, attempt + 1);
  }

  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.type}: ${data.error.message}`);
  return data;
}

Sending a lot at once?

Use batch - up to 100 messages in one request, which costs one rate-limit slot instead of a hundred.

Trials

A trial can carry its own, lower cap. While trialing, that cap applies instead of the plan's full quota - so a trial of a 50,000-email plan might allow 1,000.

When a trial ends without a plan becoming active:

{
  "error": {
    "type": "trial_expired",
    "message": "Your free trial has ended. Add a plan to keep sending."
  }
}

Also HTTP 402. Everything you've already sent stays readable; only sending stops.

On this page