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

Send basic transactional emails with PostShiba

Verify a domain and send your first production message.

This guide creates a shared cluster, verifies mail.example.com, and queues one receipt. You will see sending_ready, sending_allowed, and queued along the way.

Before you start

  • A Full access key from API keys for the setup steps: cluster, domain, and webhook. Run them from your shell, not your app.
  • A Send only key for the app that sends the receipt in step 4.
  • A domain whose DNS you can edit. A subdomain keeps root MX free. See Subdomain or root domain?.
  • A payment method on the team. Cluster create returns 403 {error: payment_required} when billing is on and there is no card.

1. Set the shell

setup.sh
1export CAPSULE_URL="https://app.postshiba.com" 2export CAPSULE_API_KEY="YOUR_API_KEY" 3export TEAM_ID="KjkAJW"

Run those exports in the current shell. If you saved the block as setup.sh, load it with source setup.sh so later commands inherit the variables.

2. Create a cluster

A cluster is a named sending environment. Customer clusters are shared-IP. size is the node size. plan is the monthly capacity. Omit plan and create sets nano.

create-cluster.sh
1curl -sS \ 2 --request POST \ 3 --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/clusters" \ 4 --header "Authorization: Bearer $CAPSULE_API_KEY" \ 5 --header "Content-Type: application/json" \ 6 --data '{ 7 "cluster": { 8 "name": "production", 9 "size": "small", 10 "region": "manual", 11 "plan": "nano", 12 "classification": "transactional" 13 } 14 }'

A pending team can create one cluster. A second cluster returns 403 {error: kyc_required} until the team is approved. Create also issues a default SMTP credential. The password is not in this response.

cluster-response.json
1{ 2 "id": "NmQpXr", 3 "name": "production", 4 "region": "manual", 5 "size": "small", 6 "plan": "nano", 7 "ip_assignment": "shared", 8 "desired_status": "active", 9 "status": "active", 10 "sending_ready": true 11}

Save the cluster id. The full object also includes emails_per_hour, emails_per_day, smtp_endpoint, http_endpoint, classification, and sending_ips. If sending_ready is still false, GET the cluster again.

get-cluster.sh
1curl -sS \ 2 --url "$CAPSULE_URL/api/v1/clusters/NmQpXr" \ 3 --header "Authorization: Bearer $CAPSULE_API_KEY"

If the response is 403 kyc_required, the team is still pending. While the team is pending, you send only to member emails, live inbox addresses, and inbox forward-to addresses, and anything else returns 403 kyc_required. See Does PostShiba require production approval?. If a later send returns 403 cluster_not_ready, the cluster is not ready yet. See Errors.

3. Add a sending domain

create-domain.sh
1curl -sS \ 2 --request POST \ 3 --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/sending_domains" \ 4 --header "Authorization: Bearer $CAPSULE_API_KEY" \ 5 --header "Content-Type: application/json" \ 6 --data '{ 7 "sending_domain": { 8 "name": "mail.example.com" 9 } 10 }'

Create queues a DNS check. sending_allowed stays false until DKIM is verified and return-path is verified (or you imported a key with dkim_manual).

domain-response.json
1{ 2 "id": "HsVtYk", 3 "name": "mail.example.com", 4 "tenant_id": "WbLcFd", 5 "tenant": "default", 6 "dkim_status": "pending", 7 "return_path_status": "pending", 8 "dmarc_status": "pending", 9 "sending_allowed": false, 10 "dkim_selector": "ps1", 11 "dkim_manual": false, 12 "dkim_txt": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA", 13 "dkim_cname_host": "ps1._domainkey.mail.example.com", 14 "dkim_cname_target": "HsVtYk.customers.postshiba.com", 15 "dmarc_host": "_dmarc.mail.example.com", 16 "dmarc_record": "v=DMARC1; p=none;", 17 "return_path_host": "rp.mail.example.com", 18 "return_path_target": "rp.postshiba.com", 19 "last_verification_error": null, 20 "suspended": false, 21 "primary": false 22}

Publish the DKIM CNAME and return-path CNAME exactly as returned. Then ask PostShiba to check DNS.

verify-domain.sh
1curl -sS \ 2 --request POST \ 3 --url "$CAPSULE_URL/api/v1/sending_domains/HsVtYk/verify" \ 4 --header "Authorization: Bearer $CAPSULE_API_KEY"

Read the domain again. Do not sleep for a fixed time.

get-domain.sh
1curl -sS \ 2 --url "$CAPSULE_URL/api/v1/sending_domains/HsVtYk" \ 3 --header "Authorization: Bearer $CAPSULE_API_KEY"

Wait until sending_allowed is true. See Add and verify a domain for the records and Verified domains for the statuses. If sending_allowed stays false, check last_verification_error.

4. Send a receipt

Install the Node client with npm install @postshiba/node. emails.send maps to POST /api/v1/emails. POST /api/v1/emails does not accept Idempotency-Key. We call POST /api/v1/teams/:team_id/clusters/:cluster_id/sends with Idempotency-Key when we want the first 201 replayed for 24 hours. A reused key with a different body returns 409 idempotency_conflict.

from is optional. A blank From is replaced with hello@ the first sending-ready domain.

Use your Send only key here, not the Full access key from the shell. It can send and nothing else, so a leaked app key cannot change your domains or webhooks. Keep it on your server, never in browser or mobile code.

send.js
1import { PostShiba } from "@postshiba/node" 2 3const postshiba = new PostShiba("YOUR_API_KEY", { teamId: "KjkAJW" }) 4 5await postshiba.emails.send({ 6 from: "Acme Receipts <receipts@mail.example.com>", 7 to: ["you@example.com"], 8 reply_to: "support@example.com", 9 subject: "We received your order", 10 text: "Thanks for your order. We will send tracking details soon.", 11 html: "<p>Thanks for your order. We will send tracking details soon.</p>", 12 unique_args: { 13 order_id: "ord_123", 14 kind: "receipt", 15 }, 16})

A successful request returns 201 with "queued": true.

send-response.json
1{ 2 "queued": true, 3 "message_id": "abc@postshiba.com", 4 "from": "Acme Receipts <receipts@mail.example.com>", 5 "to": ["you@example.com"], 6 "subject": "We received your order", 7 "unique_args": { 8 "order_id": "ord_123", 9 "kind": "receipt" 10 } 11}

If inject queued nobody, the response is 502 {error: inject_failed}. Other send errors are on Errors and Sending.

REST does not need the SMTP password in application code. Create another credential only when you send over SMTP. See SMTP credentials.

5. Watch delivery

Create one webhook endpoint for delivery events. Omit cluster_id and tenant_id to receive the whole team.

create-webhook.sh
1curl -sS \ 2 --request POST \ 3 --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/webhook_endpoints" \ 4 --header "Authorization: Bearer $CAPSULE_API_KEY" \ 5 --header "Content-Type: application/json" \ 6 --data '{ 7 "webhook_endpoint": { 8 "url": "https://app.example.com/webhooks/capsule", 9 "event_types": [ 10 "processed", 11 "delivered", 12 "deferred", 13 "bounce", 14 "dropped", 15 "spamreport", 16 "unsubscribe" 17 ] 18 } 19 }'

Store secret now. Lists omit it.

webhook-response.json
1{ 2 "id": "CdFgHj", 3 "url": "https://app.example.com/webhooks/capsule", 4 "enabled": true, 5 "cluster_id": null, 6 "tenant_id": null, 7 "event_types": [ 8 "processed", 9 "delivered", 10 "deferred", 11 "bounce", 12 "dropped", 13 "spamreport", 14 "unsubscribe" 15 ], 16 "secret": "hex-secret" 17}

The POST is a SendGrid Event Webhook array. Unique args you set at send time are flattened onto each event and also appear as unique_args. Verify X-Capsule-Signature before you trust a body. See Verify webhook requests and Event types.

You now have a shared cluster, a verified mail.example.com, a queued receipt, and a signed delivery webhook.

When to move to dedicated IPs

Receipts and password resets can stay on the cluster's shared IPs until you have steady volume. Shared IPs are the better default below about 50,000 messages a month or when traffic is spiky; dedicated IPs need that volume to keep a reputation. When you are ready, put dedicated IPs in a transactional pool and opt those messages in with "X-Capsule-Pool": "receipts" on the headers object. Keep marketing on a separate bulk cluster. A new dedicated IP warms up for 14 days; overflow keeps receipts flowing on shared IPs meanwhile. See Set up IP pools and warm up dedicated IPs.

Next steps