Switching from SendGrid
Keep the Event Webhook array and optional Inbound Parse fields. Replace send credentials and webhook verification.
Move from SendGrid to PostShiba one slice of traffic at a time, with SendGrid ready as a fallback until you are sure. Much of the SendGrid shape carries over. Delivery webhooks are still a JSON array of SendGrid-style events, SMTP still reads unique_args inside X-SMTPAPI, and inboxes can post Inbound Parse fields. The send API, keys, and webhook signatures change.
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 domain.
- An HTTPS endpoint that can receive webhooks.
- A list of everything that uses SendGrid today. See the inventory below.
Take an inventory
Write these down before you change anything.
- Every caller of
/v3/mail/sendand every SMTP client, with the key and sending domain each uses. - Subusers, with their domains, credentials, suppressions, and inbound routes.
- The
custom_argsandX-SMTPAPIunique_argsfields your event handlers read. - Event Webhook URLs, the event types you subscribe to, and how you verify them.
- Inbound Parse hostnames, MX records, destination URLs, and how you handle attachments.
- Anything that uses Dynamic Templates,
send_at, ASM groups, orip_pool_name. These need a replacement.
How SendGrid concepts map to PostShiba
| SendGrid | PostShiba |
|---|---|
Account and SG. API key |
Team and API key |
| Send Transactional Mail Only key, or Restricted Access with only Mail Send | Send only API key |
| Subuser | Tenant |
| Authenticated domain | Verified sending domain, with DKIM and return-path records |
POST /v3/mail/send |
POST /api/v1/emails with flat JSON |
smtp.sendgrid.net with username apikey |
Your cluster's SMTP host, with an issued smtp_ username |
custom_args and X-SMTPAPI unique_args |
unique_args, or the X-Capsule-Unique-Args header |
| Dynamic Templates | Hosted Liquid templates, or HTML you render yourself |
| Suppressions and ASM groups | Suppressions. Group preferences stay in your app. |
| IP pools | IP pools of your own dedicated IPs, picked per message with X-Capsule-Pool. |
send_at |
Your own job queue |
| Event Webhook | Delivery webhook with the same array shape and PostShiba signatures |
| Inbound Parse | Inbox with the sendgrid_parse webhook format |
Set up PostShiba
- 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.
- Create a Send only API key for each app and environment, and store it on your server.3. If you use subusers to separate customers, create a tenant for each. Otherwise use the default tenant. See Setting up PostShiba for multi-tenant apps.
- Add each sending domain under its tenant.
- 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.
- Wait until the cluster is
sending_readyand approval is complete before sending real traffic.
Keep the key safe
A Send only key is PostShiba's version of a SendGrid Send Transactional Mail Only key, or a Restricted Access key with only Mail Send. On PostShiba it is the default for new keys. 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 Full Access SG. key, 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 SendGrid's.
-
Open the domain in PostShiba and copy the records it shows.
-
Publish them at your DNS provider.
Record Host Type DKIM ps1._domainkey.mail.example.comCNAME Return-path rp.mail.example.comCNAME -
Click Check DNS and wait for the domain to verify.
-
Leave SendGrid's DKIM, link branding, and SPF records in place. You need them if you roll back.
If SendGrid 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 SendGrid suppressions before the first real send. Every address you miss is a bounce or complaint on your new cluster.
- Export bounces, spam reports, unsubscribes, global unsubscribes, and invalid emails from SendGrid, as CSV or through the Suppressions API.
- Import each list into the matching tenant. Upload the CSV on the Suppressions page, or call the import API.
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.
Do not import SendGrid Blocks. Those are usually temporary. Keep ASM group preferences in your app. While both providers run, write new unsubscribes and complaints to a store that both send paths check.
Switch one send path
Change one caller at a time. SendGrid's nested personalizations become a flat request.
- Combine
from.emailandfrom.nameinto onefromstring. - Turn recipient objects into address strings in
to,cc, andbcc. - Move
contentintotextandhtml. - Move
custom_argsintounique_args. - Send one request for each different message. There is no
personalizationsarray.
A 201 response has queued: true and a message_id. Store the message_id with the provider name, so you never resend a message PostShiba already accepted. To match delivery events to your own records, put your own id in unique_args, as order_id is here. It comes back on every event.
A few more changes:
- Templates. Render Dynamic Templates in your app, or port them to PostShiba templates.
- Retries. If your retries need to be safe, send on the cluster's
POST /sendswith anIdempotency-Keyheader. 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. Use a tenant slug or id you have confirmed. An unknown
tenantis rejected. See Errors. - Metadata limits.
unique_argstakes up to 32 keys of plain values. See Unique args.
SMTP. Replace username apikey and the SG. password with your PostShiba SMTP credential, on port 587 with STARTTLS and AUTH PLAIN. You can keep the X-SMTPAPI header for its unique_args, or switch to X-Capsule-Unique-Args. See Send emails with SMTP.
Replace delivery webhooks
- Create a delivery webhook and store its secret.
- Keep your array parser. The event objects use SendGrid field names such as
event,email,sg_event_id, andsg_message_id. - Replace SendGrid's signature check with PostShiba's. See Verify webhook requests.
- Remove anything that waits for
openorclick. PostShiba does not send them. - Send a test message and confirm your handler receives
order_idandsource.
| SendGrid event | PostShiba event |
|---|---|
processed |
processed |
delivered |
delivered |
deferred |
deferred |
bounce |
bounce |
dropped |
dropped |
spamreport |
spamreport |
unsubscribe |
unsubscribe |
open, click, group_unsubscribe, group_resubscribe |
Not sent |
The full field list is on Event types.
Bring your dedicated IPs
Skip this section if you send on SendGrid's shared IPs.
SendGrid IP pools and the ip_pool_name field map to a PostShiba IP pool. You pick the pool per message with the X-Capsule-Pool header.
- Count how many dedicated IPs and pools you use, and which streams use each. Keep transactional and bulk apart.
- Buy the same number of dedicated IPs in PostShiba. Create one pool per SendGrid pool, using the same name where you can. Add the IPs, then attach each pool to the matching cluster.
- Send each stream with
X-Capsule-Pool: <name>where you used to setip_pool_name. For example,ip_pool_name: "transactional"becomes the headerX-Capsule-Pool: transactional. - 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 tenants or one message type, and record which provider sent each message. 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 SendGrid'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 SendGrid 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.
- Create a domain-bound inbox and choose the
sendgrid_parsewebhook format. - Update your handler. PostShiba posts the Inbound Parse fields as URL-encoded form data, not multipart uploads, so test attachments. See Receiving webhooks.
- Test on a hosted inbox or a new subdomain first.
- Lower the TTL on the current MX record, and wait for the old TTL to pass.
- Point the MX record at the PostShiba inbound host shown on the inbox. Never leave SendGrid and PostShiba MX records at the same priority.
Receive on a subdomain, not on a host that already has mailboxes. See How do I avoid MX conflicts?.
Roll back
- Outbound. Point your router back at SendGrid for new messages only. Do not resend anything 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 the SendGrid MX record. Keep both inbound handlers running until the old TTL has passed, because some senders cache MX records.
Keep SendGrid keys, webhooks, Inbound Parse settings, 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:
- Revoke your SendGrid API keys and SMTP credentials.
- Remove the SendGrid Event Webhook and Inbound Parse settings.
- Delete SendGrid's DNS records. Keep the PostShiba DKIM, return-path, and MX records.
Final checklist
- The cluster is
sending_readyand production approval is complete. - Every subuser has a tenant with a verified domain and a live credential.
- Suppressions are imported into the right tenants.
- A real send produced delivery events with the right
unique_args. - Webhook verification uses the raw body and PostShiba's signature.
- Nothing waits for
openorclick. - Inbound Parse fields and attachments work on the new inbox.
- Your router sends each message through exactly one provider.
- Dedicated IPs are in pools and finished their 14-day warm-up.
- SendGrid stays ready until the rollback window closes.