Sign in to fill these examples with your own keys. Sign in Start free trial

Switching from SparkPost

Move each SparkPost transmission to one PostShiba request per message, then validate delivery and inbound webhooks before cutover.

Move from SparkPost to PostShiba one slice of traffic at a time, with SparkPost ready as a fallback until you are sure. The biggest change is shape. A SparkPost transmission can hold many recipients, stored templates, and a schedule. A PostShiba send is one finished message, so recipient lists, scheduling, and most template work move into your app.

Before you start

  • A PostShiba team with a payment method. Request production approval early, because you can only send to your own team until it is approved.
  • Access to DNS for each sending and receiving domain.
  • An HTTPS endpoint that can receive webhooks.

Take an inventory

Write these down before you change anything.

  • Every caller of the Transmissions API and every SMTP client, with its retries and peak send rate.
  • Stored recipient lists, substitution data, stored templates, and scheduled sends.
  • Subaccounts, with their domains, credentials, suppressions, and ip_pool choices.
  • The metadata you attach, and any of it that is nested, secret, or long.
  • Event webhook and relay webhook handlers, the keys you deduplicate on, inbound domains, reply addresses, and MX TTLs.

How SparkPost concepts map to PostShiba

SparkPost PostShiba
Account and API key Team and API key
API key with only Transmissions: Read/Write Send only API key
Subaccount Tenant. Keep a map of subaccount ids to tenant slugs.
Sending domain Verified sending domain under a tenant, with its own DKIM and return-path records
Transmission with many recipients One POST /api/v1/emails request per finished message
Transmission and recipient metadata unique_args with plain values. Keep the full record in your app.
Stored recipient list Your own recipient list, expanded into send jobs
Stored template and substitution data Hosted Liquid templates, or HTML you render yourself
options.start_time Your own scheduler
options.ip_pool IP pools of your own dedicated IPs, picked per message with X-Capsule-Pool.
SMTP with AUTH LOGIN SMTP credential for one tenant, with AUTH PLAIN
Event webhook with msys objects Delivery webhook with a SendGrid-style event array
Inbound domain and relay webhook Inbox and receiving webhook

Set up PostShiba

  1. Create a cluster. Choose transactional or bulk to match the mail you are moving, and size the plan for your busiest hour at full volume.
  2. Create a Send only API key for each app and environment, and store it on your server.
  3. For each subaccount, create a tenant and save its id and slug. With one account, use the default tenant. See Setting up PostShiba for multi-tenant apps.
  4. Add each sending domain under the right tenant.
  5. Issue an SMTP credential on the cluster for each extra tenant. The default tenant gets one when you create the cluster. REST sends need a live credential for the tenant too.
  6. Wait until the cluster is sending_ready and approval is complete before sending real traffic.

Keep the key safe

A Send only key is PostShiba's version of a SparkPost API key with only Transmissions: Read/Write. It can call POST /api/v1/emails, the cluster's POST /sends, and GET /api/v1/users/me. Every other request returns 403. If your app used a key with more permissions, the migration is a good time to drop to Send only.

Adding domains, creating webhooks, and importing suppressions need a Full access key. Use it from a script or the CLI, and do not deploy it with your app. Never put either key in browser or mobile code. A leaked Send only key can still send mail from your verified domains, so delete it on API keys if it leaks.

Publish DNS records

PostShiba uses its own DKIM selector and return-path host, so its records sit next to SparkPost's.

  1. Open the domain in PostShiba and copy the records it shows.

  2. Publish them at your DNS provider.

    Record Host Type
    DKIM ps1._domainkey.mail.example.com CNAME
    Return-path rp.mail.example.com CNAME
  3. Click Check DNS and wait for the domain to verify.

  4. Leave SparkPost's DKIM, bounce domain, and tracking records in place. You need them if you roll back.

If SparkPost already uses ps1._domainkey or rp. on that domain, the records collide. Send from a new subdomain on PostShiba instead. If a record will not verify, see What if my domain is not verifying?.

Bring your suppressions

Import your SparkPost suppression list before the first real send. Every address you miss is a bounce or complaint on your new cluster.

  1. Export the suppression list from SparkPost, or pull it from its suppression list API. Export each subaccount's list separately.
  2. Import each list into the matching tenant. Upload the CSV on the Suppressions page, or call the import API.
import-suppressions.sh
1curl --fail-with-body -sS -X POST \ 2 "https://app.postshiba.com/api/v1/teams/KjkAJW/suppressions/import" \ 3 -H "Authorization: Bearer YOUR_API_KEY" \ 4 -H "Content-Type: application/json" \ 5 -d '{ 6 "emails": ["unsubscribed@example.com", "bounced@example.com"], 7 "tenant_id": "WbLcFd" 8 }'
import-response.json
1{"imported": 2, "existing": 0, "invalid": 0}

Each request takes up to 10,000 addresses, and a CSV upload can be up to 2 MB. Imported rows have the reason Manual. Leave tenant_id out to import into the default tenant.

While both providers run, write new unsubscribes and complaints to a store that both send paths check.

Switch one send path

Turn each transmission into jobs your app controls.

  1. Expand recipient lists into one job per message.
  2. Give each job your own id, such as message_job_id, before calling either provider. A SparkPost transmission id arrives after submission and can cover many recipients, so it cannot identify one message across providers.
  3. Render the content, or send with a PostShiba template.
  4. Release scheduled jobs from your own scheduler when they are due.
  5. Send one request per job.
send-receipt.sh
1curl --fail-with-body -sS -X POST \ 2 "https://app.postshiba.com/api/v1/emails" \ 3 -H "Authorization: Bearer YOUR_API_KEY" \ 4 -H "Content-Type: application/json" \ 5 -d '{ 6 "from": "receipts@mail.example.com", 7 "to": ["you@example.com"], 8 "subject": "Receipt 1042", 9 "text": "Thanks for order 1042.", 10 "html": "<p>Thanks for order 1042.</p>", 11 "tenant": "merchant-42", 12 "unique_args": {"message_job_id": "job_1042", "campaign_id": "receipts"} 13 }'

A 201 response has queued: true and a message_id. Store it on the job, so you never resend a message PostShiba already accepted. message_job_id comes back on every delivery event.

A few more changes:

  • Metadata. Put only short, plain, non-secret values in unique_args, up to 32 keys. Keep the rest in your app under message_job_id. See Unique args.
  • Retries. If your retries need to be safe, send on the cluster's POST /sends with an Idempotency-Key header, such as the job id. See Sending.
  • Volume. The REST send API allows 300 requests a minute per API key. For more than that, use SMTP or HTTP inject.
  • Tenants. An unknown tenant is rejected. See Errors.

SMTP. Replace the SMTP_Injection username and API key password with your PostShiba SMTP credential, on port 587 with STARTTLS and AUTH PLAIN. AUTH LOGIN does not work. Put tracking fields in the X-Capsule-Unique-Args header as a JSON object. See Send emails with SMTP.

Replace delivery webhooks

  1. Create a delivery webhook and store its secret.
  2. Stop reading msys.message_event. Parse a JSON array of SendGrid-style event objects instead.
  3. Verify the raw body before you parse it. See Verify webhook requests.
  4. Deduplicate on sg_event_id, and return a 2xx status once you have stored the event.
  5. Remove anything that waits for opens or clicks.
SparkPost event PostShiba event
injection processed
delivery delivered
delay deferred
bounce, out_of_band bounce
policy_rejection dropped
spam_complaint spamreport
list_unsubscribe, link_unsubscribe unsubscribe
generation_failure, generation_rejection No equivalent. Handle rendering errors in your app.
open, initial_open, click Not sent

The full field list is on Event types.

Bring your dedicated IPs

Skip this section if you send on SparkPost's shared IPs.

SparkPost options.ip_pool maps to a PostShiba IP pool. You pick the pool per message with the X-Capsule-Pool header.

  1. Count how many dedicated IPs and pools you use, and which streams use each. Keep transactional and bulk apart.
  2. Buy the same number of dedicated IPs in PostShiba. Create one pool per SparkPost pool, using the same name where you can. Add the IPs, then attach each pool to the matching cluster.
  3. Send each stream with X-Capsule-Pool: <name> where you used to set options.ip_pool. For example, options.ip_pool: "transactional" becomes the header X-Capsule-Pool: transactional.
  4. Do this at least 14 days before stage 2, so the parallel run below doubles as the warm-up.

The IP addresses themselves change, so update any allowlists your recipients keep. See Set up IP pools and warm up dedicated IPs for the full schedule and how to watch it.

Run both providers in parallel

Route each new message to exactly one provider. Pick a stable slice, such as a set of subaccounts or one message type, and record which provider sent each job. Retries and rollback then stay on the same provider. Never send the same message through both.

Stage Days Share on PostShiba Move on when
1 1 to 3 Test sends only Content, DKIM, return-path, and webhooks check out
2 4 to 10 About 10% Bounce, deferral, and complaint rates look like SparkPost's
3 11 to 17 About 25% The same checks still hold
4 18 to 24 About 50% The same checks still hold
5 25 to 30 100% Stay here through your rollback window

Adjust the schedule for your volume and risk. Keep SparkPost as primary while you ramp. Raise PostShiba's share only while deferrals stay down at Gmail, Yahoo, and Outlook. On shared IPs, this ramp tests your integration and builds your domain's reputation with mailbox providers. If you send through an IP pool, the same ramp is your IP warm-up, so start the pool's IPs at least 14 days before stage 2 and check the per-provider numbers before each stage. While approval is pending, stage 1 can only send to your own team.

Move inbound mail

Move receiving separately, after outbound is stable. MX records cannot split mail by percentage.

  1. Create an inbox.
  2. Pick the webhook format. The default posts one JSON object per message. Choose sendgrid_parse if you want the raw message in an email field. See Receiving webhooks.
  3. Test on a hosted inbox or a new subdomain first, including signatures and attachments.
  4. Lower the TTL on the current MX record, and wait for the old TTL to pass.
  5. Point the MX record at the PostShiba inbound host shown on the inbox, and update Reply-To addresses if they change.

Receive on a subdomain, not on a host that already has mailboxes. See How do I avoid MX conflicts?.

Roll back

  • Outbound. Stop new PostShiba submissions and move only jobs that were never sent back to SparkPost. Never resend a job that already has a PostShiba message_id, because it was accepted and may still be delivered. Keep the PostShiba webhook running so late events still arrive.
  • Inbound. Restore SparkPost's MX record and the old Reply-To addresses. Keep both inbound handlers running until the old TTL has passed, because some senders cache MX records.

Keep SparkPost keys, webhooks, relay webhooks, and DNS records until your rollback window closes.

Finish the cutover

After you have run at 100% for your whole rollback window with healthy checks:

  1. Revoke your SparkPost API keys.
  2. Remove SparkPost event and relay webhooks.
  3. Delete SparkPost's DNS records. Keep the PostShiba DKIM, return-path, and MX records.

Final checklist

  • The cluster is sending_ready and production approval is complete.
  • Every subaccount maps to a tenant with a verified domain and a live credential.
  • Suppressions are imported into the right tenants.
  • Recipient expansion, rendering, scheduling, and retries run in your app.
  • unique_args hold only short, non-secret values.
  • Delivery events pass verification and are deduplicated on sg_event_id.
  • The new inbox receives mail and its webhook verifies.
  • Each job stays on one provider through retries and rollback.
  • Dedicated IPs are in pools and finished their 14-day warm-up.
  • SparkPost stays ready until the rollback window closes.

Next steps