Dedicated IPs

A sending address of your own, the 17-day warmup it goes through, and the two things you can change about it.

By default your mail goes out through shared pools, which is the right answer for almost everybody: a pool carries enough steady volume to hold a reputation, and yours joins it. A dedicated address is the other trade. The reputation is entirely yours, which cuts both ways.

POST /v1/ips takes one out of inventory, answering 201 with the address. It reads {"mail_class": "transactional" | "marketing"} and an optional region, and the class travels with the address for life: an address that spent three weeks warming on newsletters carries a newsletter reputation, so the two never share one. Warmup starts immediately and cannot be skipped. It answers 402 plan_required on the free plan, and 503 no_address_available when inventory is empty, which is a retry rather than a mistake. DELETE /v1/ips/:id hands the address back; sends fall back to the shared pool for their class immediately, so releasing one does not stop mail, but the warmup it had accumulated stops being yours. Pause while you are investigating a reputation problem, release when you are finished with it.

What you have

GET /v1/ips
curl https://emails.sh/v1/ips \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
200 OK
{
  "default_region": "eu",
  "residency": { "sending": "eu-west-1", "storage": "eu", "compute": "eu" },
  "ips": [
    {
      "id": "5c1a7e04-8b39-4f62-a0d7-1e94b3c76085",
      "ip": "203.0.113.42",
      "region": "eu",
      "region_label": "Europe (Ireland)",
      "mail_class": "marketing",
      "pool_name": "acme-marketing",
      "warmup_state": "warming",
      "warmup_day": 6,
      "daily_cap": 10000,
      "sent_today": 4102,
      "day": "2026-07-28",
      "assigned_at": "2026-07-22T09:00:00.000Z",
      "warmup_started_at": "2026-07-23T00:00:00.000Z",
      "warmup_completed_at": null
    }
  ],
  "shared_pools": [
    { "mail_class": "transactional", "label": "Transactional", "pool_name": "shared-tx-eu" },
    { "mail_class": "marketing", "label": "Marketing", "pool_name": "shared-mkt-eu" }
  ]
}

mail_class is derived from the pool name rather than set separately, and it is transactional, marketing, or null. Keeping the two classes on separate addresses is the whole reason to have more than one: a campaign that gets complaints then cannot damage the deliverability of your password resets.

Warmup

A new address has no history, and an address with no history that sends a hundred thousand messages on its first day looks exactly like a spammer, because usually it is one. So a new address ramps, with a cap that rises each day for 17 days.

DayDaily cap
150
2100
3500
41000
55000
610000
720000
840000
970000
10100000
11150000
12200000
13300000
14400000
15500000
16750000
171000000

Past day 17 the address is ready and the schedule stops applying. Overflow above the cap goes out through the shared pool rather than being refused, so warmup slows an address down without stopping your mail.

warmup_stateWhat it means
pendingAssigned and not started. Nothing is going through it yet.
warmingOn the schedule above. warmup_day says where.
readyWarmed. It carries whatever you send it.
pausedYou paused it, or we did. Mail routes through the shared pool instead.
retiredOut of service. It refuses a PATCH.

The two things you can change

paused and daily_cap. That is the whole of PATCH /v1/ips/:id, and anything else in the body is refused. daily_cap must be a non-negative integer or null, and it can only lower the allowance: a number above the warmup cap for the day does not raise it.

PATCH /v1/ips/:id
# Hold this address at 5000 a day, below whatever warmup would allow
curl -X PATCH https://emails.sh/v1/ips/5c1a7e04-8b39-4f62-a0d7-1e94b3c76085 \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"daily_cap": 5000}'

# Take it out of rotation without giving it up
curl -X PATCH https://emails.sh/v1/ips/5c1a7e04-8b39-4f62-a0d7-1e94b3c76085 \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paused": true}'

Reverse DNS

GET on one address runs a live PTR check and tells you what the record should say, what it does say, whether a forward lookup confirms it, and whether the whole thing is in order. A dedicated address whose reverse DNS does not forward-confirm is one several large receivers treat with suspicion, so this is the field to watch after provisioning.

GET /v1/ips/:id
{
  "id": "5c1a7e04-8b39-4f62-a0d7-1e94b3c76085",
  "ip": "203.0.113.42",
  "reverse_dns": {
    "ip": "203.0.113.42",
    "expected": "a42.out.emails.sh",
    "found": "a42.out.emails.sh",
    "forwardConfirmed": true,
    "ok": true
  }
}

Do you want one

Under a few hundred thousand a month, probably not
A dedicated address needs steady volume to hold a reputation. Send too little and receivers have nothing recent to judge it by, which is worse than being in a healthy pool.
Separating marketing from transactional, yes
This is the strongest reason. A campaign and a password reset should not share a reputation, and separate addresses are how that is enforced rather than hoped for.
To escape a bad shared reputation, no
A dedicated address starts with no reputation at all, and 17 days of warmup, and every complaint after that is yours alone. If your mail is getting filtered, fix the mail.
A compliance requirement, sometimes
Some contracts specify a dedicated sending address. That is a real reason and it is fine.
GET /v1/ips

Dedicated addresses, shared pools, the default region, and data residency.

GET /v1/ips/:id

One address, with a live reverse DNS check.

PATCH /v1/ips/:id

{ paused?, daily_cap? } and nothing else.

ips.list

Dedicated addresses on the workspace, the shared pools anything else goes through, the default region, and where sending, storage, and compute physically happen.

Takes no arguments.

Returns{ default_region, residency, ips[], shared_pools[] }

ips.get

One address, plus a live reverse DNS check: what the PTR should say, what it says, and whether the forward lookup confirms it.

Arguments

id string required

ReturnsIp & { reverse_dns }

ips.update

The only two things about an address you can change. POST /v1/ips takes one out of inventory and DELETE /v1/ips/:id hands it back; this route is for pausing one and lowering its cap.

Arguments

id string required
paused boolean
Stop routing mail through it without giving it up.
daily_cap number | null
A non-negative integer, or null for no cap of your own. It can lower the warmup allowance and never raise it.

ReturnsIp