# Set up IP pools and warm up dedicated IPs

> Choose shared or dedicated IPs, group them into pools, and warm new IPs without losing mail.

This guide helps you decide which mail should leave from dedicated IPs, puts those IPs in IP pools, and walks a new IP from its first message to full volume. It works the same for one product sending its own mail and for a platform sending for its customers.

Every step ends with **Save** (what to store) and **Done when** (how to know it worked). A person or an agent can run each block as written.

## Shared or dedicated

Every cluster starts on shared IPs. PostShiba keeps them warm with steady traffic from many senders, so a new sender gets good delivery from the first message. Dedicated IPs give you a reputation of your own, which helps only if you send enough steady mail to earn it.

| Setup | Use it when | Watch out for |
| --- | --- | --- |
| Shared IPs | You send under about 50,000 messages a month, or your volume comes in bursts. This is right for most senders. | Nothing to set up. Your domain's reputation still matters. |
| A stream pool | One stream, such as newsletters, should never affect another, such as password resets. | Transactional and marketing always go on separate clusters and pools. |
| A tier pool | You run a platform and several steady, well-behaved customers can share a reputation, for example `high-volume`. | One bad customer hurts everyone in the tier. Move them out quickly. |
| A customer pool | One customer sends a lot, wants a reputation of their own, or needs a fixed IP their recipients can allowlist. | Each IP needs that customer's steady daily volume to stay warm. |

Some rules of thumb:

- **Stay shared until volume is steady.** A dedicated IP that sends a few hundred messages one week and nothing the next looks like a spammer to mailbox providers.
- **Put two IPs in a pool you depend on.** If one IP has trouble at a provider, the other keeps the pool sending.
- **Don't spread thin.** Each IP should carry real daily volume. Add IPs as volume grows, not before.
- **Keep risky mail apart.** Put a new customer, or a list you haven't mailed in months, on shared IPs or in its own pool, never in a pool other mail depends on.
- **Use a pool for more than one IP.** A cluster can hold one dedicated IP of its own. To send on more, put them in a pool.

## How pools work

An IP pool is a named set of your dedicated IPs. Mail opts in one message at a time with the `X-Capsule-Pool` header. Mail without the header keeps using the cluster's own IPs.

- A pool is transactional or bulk. That can't change, and it only takes IPs and clusters of the same type.
- Each IP has a weight from 1 to 100. Weights are relative: two IPs at 100 and 50 take two thirds and one third of the pool's mail.
- One pool can be attached to several of your shared clusters, and one IP can sit in several pools.
- A pool name that doesn't exist, isn't attached to the cluster, or has no live IPs is refused. That mail never falls back to the shared IPs.

## Before you start

- A Full access API key and your team id, both from [API keys](/docs/create-an-api-key). Changing **Network** in the dashboard needs the Team Administrator role.
- A shared cluster of the right type, transactional or bulk. See [Clusters](/docs/dashboard/clusters/introduction).
- A payment method on the team. Dedicated IPs are $50 a month each.
- Your suppression list imported. Every bounce from an address you already knew was bad counts against a new IP. See [How to manage suppressions](/docs/dashboard/emails/email-suppressions).

Set these once in your shell. Every block below uses them.

```bash setup.sh
export CAPSULE_URL="https://app.postshiba.com"
export CAPSULE_API_KEY="YOUR_API_KEY"
export TEAM_ID="KjkAJW"
export CLUSTER_ID="NmQpXr"
```

## Steps at a glance

| # | Step | Call | Run |
| --- | --- | --- | --- |
| 1 | Buy dedicated IPs | `POST /api/v1/teams/:team_id/network` | Once per IP |
| 2 | Create the pool | `POST /api/v1/teams/:team_id/ip_pools` | Once per pool |
| 3 | Add the IPs | `POST /api/v1/ip_pools/:id/add_ip` | Once per IP |
| 4 | Attach the pool | `POST /api/v1/ip_pools/:id/attach` | Once per cluster |
| 5 | Send through the pool | `X-Capsule-Pool` header | Every pooled message |
| 6 | Ramp up next to your current sender | Your code | Every message, for 4 to 6 weeks |
| 7 | Watch delivery | `GET /api/v1/teams/:team_id/analytics` | Daily during warm-up |
| 8 | Add or retire IPs | `add_ip`, `remove_ip` | As volume changes |

## 1. Buy dedicated IPs

List the IPs you can buy for the type of mail you are moving.

```bash list-ips.sh
curl -sS \
  --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/network?classification=transactional" \
  --header "Authorization: Bearer $CAPSULE_API_KEY"
```

Pick rows with `"kind": "dedicated"` and `"available": true`, then buy each one. Leave out `cluster_id` so the IP stays free for a pool.

```bash buy-ip.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/network" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"ip_address_id": "QwErTy"}'
```

```json buy-ip-response.json
{
  "id": "QwErTy",
  "address": "198.51.100.10",
  "status": "reserved",
  "kind": "dedicated",
  "classification": "transactional",
  "owned": true,
  "cluster_ids": [],
  "ip_pool_ids": []
}
```

In the dashboard, open **Network** and select **Add IP**, then pick **Leave unassigned**. Buying from a pool page with **Get a dedicated IP** adds the IP to that pool for you.

**Save:** each IP's `id`.

**Done when:** each IP shows `"owned": true`. Its 14-day warm-up has not started yet. It starts when the IP joins a pool in step 3.

## 2. Create the pool

The name is the value you will send in `X-Capsule-Pool`. Use lowercase letters, numbers, dashes, and underscores. For a customer pool, the customer's tenant slug works well.

```bash create-pool.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/ip_pools" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"name": "receipts", "classification": "transactional", "warmup_overflow": true}'
```

```json pool-response.json
{
  "id": "PlMnBv",
  "name": "receipts",
  "classification": "transactional",
  "warmup_overflow": true,
  "ips": [],
  "cluster_ids": [],
  "created_at": "2026-10-12T00:00:00Z"
}
```

Keep `warmup_overflow` on. While every IP in the pool is warming, the mail they can't take yet goes out on the cluster's shared IPs, so nothing waits. Turn it off only if every pooled message must leave from the pool's IPs and you can accept delays during the first two weeks.

In the dashboard, open **Network**, select **New pool**, and fill in **Name** and **Email type**. **Use the cluster's IPs while warming up** is the same setting.

**Save:** the pool `id` as `POOL_ID`, and its name.

**Done when:** the response has the name you sent. A name you already use returns `422` `name_taken`.

## 3. Add the IPs

Add each IP you bought. New IPs that warm together should have the same weight.

```bash add-ip.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/ip_pools/$POOL_ID/add_ip" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"ip_address_id": "QwErTy", "weight": 100}'
```

```json pool-with-ip.json
{
  "id": "PlMnBv",
  "name": "receipts",
  "warmup_overflow": true,
  "ips": [
    {
      "id": "QwErTy",
      "address": "198.51.100.10",
      "status": "warming",
      "warmup_ends_at": "2026-10-26T00:00:00Z",
      "weight": 100
    }
  ],
  "cluster_ids": []
}
```

**Done when:** each IP shows `"status": "warming"` and a `warmup_ends_at` 14 days out.

## 4. Attach the pool

Attach the pool to every shared cluster that should send through it. The cluster's type must match the pool's.

```bash attach-pool.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/ip_pools/$POOL_ID/attach" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"cluster_id\": \"$CLUSTER_ID\"}"
```

On the pool page in the dashboard, the three steps **Add dedicated IPs**, **Attach to a cluster**, and **Send through the pool** walk you through the same setup. The pool shows **Ready** when it has an IP and a cluster.

**Done when:** `cluster_ids` includes your cluster.

## 5. Send through the pool

Add the header to every message that should use the pool. On `POST /api/v1/emails` and `POST /sends` it goes in the `headers` object. Over SMTP and HTTP inject it is a normal message header. The name is not case-sensitive, and the header is removed before the message leaves.

```bash send-pooled.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/emails" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "from": "Acme Receipts <receipts@mail.example.com>",
    "to": ["you@example.com"],
    "subject": "Receipt ord_123",
    "text": "Thanks for your order.",
    "headers": {"X-Capsule-Pool": "receipts"}
  }'
```

**Done when:** the message's events show a pool IP as `sending_ip`. In the first two weeks, some pooled mail shows a shared IP instead. That is overflow doing its job.

## 6. Ramp up next to your current sender

A new IP has no history, so mailbox providers watch it closely. Start small, send the mail people want most, and grow while the numbers stay clean.

### What PostShiba does for you

- **Holds each new IP back for 14 days.** A warming IP takes about 100 messages a day. With overflow on, the rest goes out on the cluster's shared IPs, so your volume never waits on the warm-up.
- **Slows down when a provider pushes back.** When a mailbox provider defers mail because a sending IP is moving too fast, PostShiba slows delivery to that provider and keeps retrying. At Yahoo, if one tenant keeps hitting the limit, PostShiba pauses that tenant's Yahoo mail for an hour, and everyone else keeps sending.
- **Stops sending to bad addresses.** Permanent bounces, spam reports, and unsubscribes go on the suppression list automatically.

After day 14 the IP sends at full rate. From then on, the share of mail you send with the pool header decides how fast it grows.

### What you control

Keep your current provider, or the shared IPs, as the main path, and send a growing share of mail through the pool. Decide once per message and record the choice, so a retry never goes out twice.

```javascript route-message.js
import { createHash } from "node:crypto"

// Share of each mailbox provider's mail that goes through the pool today.
export const poolShare = { gmail: 0.05, yahoo: 0.05, outlook: 0.05, other: 0.05 }

function mailboxProvider(address) {
  const domain = address.split("@").pop().toLowerCase()
  if (["gmail.com", "googlemail.com"].includes(domain)) return "gmail"
  if (["yahoo.com", "aol.com", "ymail.com"].includes(domain)) return "yahoo"
  if (["outlook.com", "hotmail.com", "live.com", "msn.com"].includes(domain)) return "outlook"
  return "other"
}

export function usePool(message) {
  const hash = createHash("sha256").update(`${message.id}:${message.to}`).digest()
  const bucket = hash.readUInt32BE(0) / 2 ** 32
  return bucket < poolShare[mailboxProvider(message.to)]
}
```

Store the result with the message. If `usePool` is true, send with `X-Capsule-Pool`. Otherwise send the way you do today.

Raise each provider's share on its own, following this schedule. The numbers are messages per day to each large provider through the pool.

| Days | Per provider per day | What to send | IP state |
| --- | --- | --- | --- |
| 1 to 2 | 50 to 100 | Your most engaged recipients: receipts, replies, people who signed in this week | Warming. Overflow carries extra mail. |
| 3 to 4 | About 200 | The same | Warming |
| 5 to 7 | About 400 | Add recipients active in the last 60 days | Warming |
| 8 to 10 | 600 to 800 | Add recipients active in the last 90 days | Warming |
| 11 to 14 | 1,000 to 1,500 | Grow 30 to 50% a day, no longer doubling | Warming |
| 15 to 17 | 2,000 to 3,000 | Your regular mix | Active. The pool takes everything you send it. |
| 18 to 21 | 4,000 to 5,000 | Your regular mix | Active |
| 22 to 25 | 7,500 to 10,000 | Grow 20 to 30% a day | Active |
| 26 to 30 | Full volume | Everything for this stream | Active |

No schedule fits every sender. Only move to the next row when step 7 looks healthy, and go slower with a provider that pushes back. Most senders reach full volume in 3 to 6 weeks.

**Save:** today's `poolShare` and the day of the ramp.

**Done when:** `poolShare` is `1` for every provider, and step 7 has stayed healthy for a week.

## 7. Watch delivery

Check each mailbox provider before you raise its share. PostShiba does not track opens or clicks, so watch what providers do with your mail: deferrals, bounces, and spam reports.

```bash analytics-by-provider.sh
curl -sS \
  --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/analytics?dimension=mx_provider" \
  --header "Authorization: Bearer $CAPSULE_API_KEY"
```

```json analytics-response.json
{
  "dimension": "mx_provider",
  "rows": [
    { "key": "gmail", "processed": 1200, "delivered": 1188, "deferred": 9, "bounce": 3, "spamreport": 0 },
    { "key": "yahoo", "processed": 400, "delivered": 380, "deferred": 31, "bounce": 2, "spamreport": 1 }
  ]
}
```

Divide each count by `processed`, provider by provider.

| Signal | Healthy | If it rises |
| --- | --- | --- |
| Bounces | Under 2% | Hold the ramp. Check where the addresses came from and import any suppressions you missed. |
| Spam reports | Under 0.1%. Gmail and Yahoo act at 0.3%. | Cut that provider's share 25 to 30%. Find the stream or customer causing them and pause it. |
| Deferrals | About the same as the day before | Cut that provider's share 25 to 30% until it settles, then grow more slowly. |

In the example, Yahoo's deferrals are almost 8%, so hold Yahoo where it is while Gmail keeps growing.

To see one IP on its own, use `dimension=sending_ip`, or list its events with `GET /api/v1/teams/:team_id/message_events?sending_ip=198.51.100.10`. In the dashboard, **Analytics** groups by the same dimensions.

**Done when:** you check this every day of the ramp, before you change `poolShare`.

## 8. Add or retire IPs

**Add capacity to a warm pool.** Buy an IP and add it with a low weight. While it warms, it takes its small daily share and the warm IPs carry the rest. After day 14, raise its weight a step every few days, checking step 7 each time.

```bash raise-weight.sh
curl -sS \
  --request POST \
  --url "$CAPSULE_URL/api/v1/ip_pools/$POOL_ID/add_ip" \
  --header "Authorization: Bearer $CAPSULE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"ip_address_id": "ZxCvBn", "weight": 25}'
```

Calling `add_ip` again on an IP already in the pool only changes its weight. A path such as 10, 25, 50, then 100 works for most pools.

**Retire an IP.** Remove it from the pool with `POST /api/v1/ip_pools/:id/remove_ip`. Queued mail pinned to it goes out on the rest of the pool. You still own it and pay for it until you cancel it in **Network**. A cancelled IP rests for 30 days before another team can buy it, and the next owner warms it up from the start.

**Keep IPs warm.** An IP that stops sending loses its reputation over a few weeks. If a stream shrinks, take IPs out of the pool rather than spreading less mail over the same IPs.

## Troubleshooting

| What you see | Why | What to do |
| --- | --- | --- |
| Pooled mail leaves on a shared IP | Every pool IP is still warming and overflow is on | Expected for the first 14 days. |
| Pooled mail is slow in the first two weeks | Overflow is off, so extra mail waits for the warming IPs | Turn **Use the cluster's IPs while warming up** back on, or send less through the pool. |
| `502` `inject_failed` on a pooled send | The pool name is wrong, the pool isn't attached to this cluster, or it has no live IPs | Check the name and `cluster_ids`. |
| Deferrals climb at one provider | The IP is growing faster than that provider trusts it | Cut that provider's share 25 to 30%, wait a day, then grow more slowly. |
| Bounces mention a blocklist | The IP or domain was listed | Stop raising volume and [contact support](/docs/support/how-to-reach-us). Once delisted, restart the ramp lower. |

## Errors you will see

| Status and error | When | What to do |
| --- | --- | --- |
| `403` `payment_required` | Buying an IP with no card on the team | Add a payment method. |
| `422` `cooling_down` | The IP was cancelled less than 30 days ago | Pick another IP. |
| `422` `name_taken` | Creating or renaming to a name you already use | Pick another name. |
| `422` `class_mismatch` | The IP or cluster is a different type from the pool | Use a transactional IP and cluster for a transactional pool, and the same for bulk. |
| `422` `in_default_pool` | The IP already sends for a cluster | Unassign it from the cluster first. |
| `422` `weight` | The weight isn't a whole number from 1 to 100 | Fix the weight. |
| `422` `empty_pool` | Attaching a pool with no live IPs | Add an IP first. |
| `422` `dedicated_cluster` | Attaching to a cluster that is not shared | Attach to a shared cluster. |
| `422` `last_ip` | Removing the last IP of an attached pool | Detach the pool first. |
| `422` `pool_attached` | Deleting a pool that is still attached | Detach every cluster first. |

## Next steps

- [How do dedicated IPs work?](/docs/knowledge-base/how-do-dedicated-ips-work)
- [IP pools API](/docs/api-reference/ip-pools)
- [Network API](/docs/api-reference/network)
- [Build an ESP on PostShiba](/docs/guides/build-an-esp)
- [What happens at your cap](/docs/dashboard/clusters/what-happens-at-your-cap)
- [Why are my emails landing on the suppression list?](/docs/knowledge-base/why-are-my-emails-landing-on-the-suppression-list)
