Access is invite-only during beta. Once invited, create a key to personalize these examples. Production sending requires a separate review.
Switching from SparkPost
Move each SparkPost transmission to one PostShiba request per message, then validate delivery and inbound webhooks before cutover.
Run both providers during outbound migration
SparkPost and PostShiba can coexist during outbound migration. Route by a stable cohort, keep retries on the original provider, and never send the same production message through both.
Start with this adjustable 30-day ramp:
- Days 1 to 3. Run controlled tests.
- Days 4 to 9. Route about 10% through PostShiba.
- Days 10 to 15. Increase to about 25%.
- Days 16 to 22. Increase to about 50%.
- Days 23 to 30. Increase to 100%.
Adjust or reverse a stage when submission, delivery, bounce, complaint, or suppression checks exceed your limits. PostShiba uses shared IPs. This ramp tests the integration, not dedicated-IP warming.
Keep SparkPost credentials, webhooks, and DNS through the rollback window. Outbound records can coexist unless DKIM selectors or return-path hosts collide. For a collision, use a separate PostShiba sending subdomain.
Move inbound mail separately. MX routing cannot split traffic by percentage. Test with a PostShiba-hosted inbox or a new inbound subdomain before changing the production MX.
Inventory the SparkPost integration
Record:
- Transmissions API and SMTP callers, retries, and peak rate.
- Recipient lists, substitutions, templates, and schedules.
- Subaccounts, domains, credentials, suppressions, and
ip_poolchoices. - Metadata that is structured, secret, or long.
- Event and relay handlers, deduplication keys, inbound domains, reply addresses, and MX time-to-live values.
Map SparkPost to PostShiba
| SparkPost | PostShiba | Migration action |
|---|---|---|
| Primary account and API key | Team and API key | Use Bearer auth. |
| Subaccount | Tenant | Store an ID-to-slug map. |
| Sending domain | Tenant-owned sending domain | Publish new DKIM and return-path records. |
SMTP API with AUTH LOGIN |
Tenant SMTP credential with AUTH PLAIN |
Replace the connection and auth. |
| Transmission and recipients | POST /api/v1/emails |
Send one rendered message. |
| Transmission or recipient metadata | unique_args |
Send scalars; store the full record. |
| Stored recipient list | Application recipient store | Expand the list into jobs. |
| Stored template and substitution data | Application template renderer | Render the message before sending. |
options.start_time |
Application scheduler | Release jobs when due. |
options.ip_pool |
Shared PostShiba cluster | Remove source-IP selection. |
Event webhook msys objects |
SendGrid-style event array | Parse the array and verify HMAC. |
| Inbound domain and relay webhook | Inbox and receiving webhook | Move the address, MX, and parser. |
PostShiba has no recipient lists, schedules, or selectable IP pools. Keep those in your application. Substitution can move to hosted Liquid templates. Leave a stream on SparkPost if it requires a dedicated source IP.
Set up PostShiba in order
- Create a shared cluster, then request production approval. Wait for approval and
sending_ready: true. - Create an API key. Keep it server-side and test it with
GET /api/v1/users/me. - Use the default tenant for one account. For each SparkPost subaccount, create a tenant and save its ID and slug.
- Add each sending domain to the correct tenant. Publish the DKIM and return-path CNAME records. Wait for both to verify.
- Create a tenant SMTP credential for every tenant that sends. The HTTP send path also needs a live cluster credential for injection.
- Create a team delivery webhook. Store its secret.
Bring your suppressions
Import the SparkPost suppression list before you send. A missed row is a complaint on the new cluster.
Export suppressions from the SparkPost UI or GET /api/v1/suppression-list. Map subaccount lists to the matching tenant.
Upload the CSV on Suppressions, or post the addresses:
Use a column named email, address, or recipient, or one address per line. Existing rows are skipped. Max 10,000 addresses per request. See Suppressions.
Replace a transmission
Expand recipient lists into durable jobs. Port stored templates to PostShiba templates or render them in your application, and release scheduled jobs when due. Send one message per PostShiba request.
Send one rendered receipt for tenant merchant-42:
Expect 201 with queued: true, then save message_id. POST /api/v1/emails has no idempotency key. For automatic retries, use cluster-specific POST /sends with Idempotency-Key. Otherwise, block a second submission until you reconcile an ambiguous response.
Use an application-owned message_job_id that exists before either provider call. A SparkPost transmission ID is assigned after submission and can represent many recipients, so it is not a safe cross-provider key. Use only a confirmed tenant ID or slug because an unknown value is 422 invalid.
Put only scalar, non-secret metadata in unique_args. The limits are 32 keys, 64 bytes per key, and 256 bytes per value. Store the rest under message_job_id.
For SMTP, use the cluster endpoint on port 587 with STARTTLS. Replace SparkPost's SMTP_Injection user and API-key password with the issued smtp_... credential. Force AUTH PLAIN. AUTH LOGIN fails. Put correlation fields in X-Capsule-Unique-Args.
Replace event webhooks
Stop reading msys.message_event. PostShiba sends a SendGrid-style JSON array. Map SparkPost events as follows:
injectiontoprocesseddeliverytodelivereddelaytodeferredbounceandout_of_bandtobouncepolicy_rejectiontodroppedspam_complainttospamreportlist_unsubscribeandlink_unsubscribetounsubscribe
Handle SparkPost generation_failure and generation_rejection in your template renderer. PostShiba receives finished content, so it emits no equivalent event. It also emits no open or click events.
Verify the raw body before parsing. Check X-Capsule-Signature against sha256=HMAC_SHA256(secret, "{X-Capsule-Timestamp}.{raw_body}") with a timing-safe comparison. Deduplicate on sg_event_id, store the event, then return 2xx. See webhook verification.
Replace relay inbound
Create a PostShiba inbox. A domain-bound address uses local-part@inbound.{sending_domain}. Publish MX only on that inbound host.
The default webhook is one JSON object, not an msys.relay_message array. Read its message fields and attachments[].content_base64. For raw email_rfc822, choose sendgrid_parse; its email field contains raw MIME. Each inbox has its own HMAC secret.
Validate, cut over, and roll back
- While approval is pending, send only to a team member or a live PostShiba inbox.
- After approval, confirm
201,queued: true, a signed delivery event, andmessage_job_id. - Follow the outbound ramp. Adjust it from measured results.
- Test inbound on the separate address or subdomain, including signatures and attachments.
- Switch Reply-To and inbound MX in a separate cutover.
For outbound rollback, stop new PostShiba submissions and move only unsubmitted jobs to SparkPost. Never resend a job with queued: true and a PostShiba message_id. Keep the PostShiba webhook online for late events.
For inbound rollback, restore the old Reply-To and SparkPost MX records. Wait for the DNS time-to-live.
Final checklist
- Production approval is complete and the shared cluster is sending-ready.
- The platform token passes
/users/me. - Every subaccount maps to a tenant with a verified domain and live credential.
- Recipient expansion, template rendering, scheduling, and retry guards run in the application.
-
unique_argscontain only bounded, non-secret correlation values. - Delivery events pass HMAC verification and deduplicate on
sg_event_id. - The new inbox accepts mail and its receiving webhook passes HMAC verification.
- Suppression lists are imported per tenant.
- Each production message stays on one provider through retry and rollback.
- Inbound mail was tested and switched separately.