Create a key to copy examples filled with your team.

Send basic transactional emails with PostShiba

Verify a domain and send your first production message.

You will create a shared-IP cluster, verify a sending domain, add the SMTP credential the cluster needs, and send one message through the REST API.

PostShiba reviews new senders before they can create a cluster. Finish that review first. Until your team is approved, cluster creation returns 403 {"error":"kyc_required"}.

What you need

  • A PostShiba account with an approved team
  • A domain whose DNS records you can edit
  • A platform application token from Developers → Platform applications
  • A server-side application that can keep that token secret

The examples use these placeholders:

  • CAPSULE_API_KEY is your platform application token
  • Team 1, cluster 4, and domain 8 stand in for IDs from your own responses
  • mail.example.com is the domain you will verify

Set a few shell variables so the commands are easier to read.

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

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

1. Create a cluster

A cluster is your sending environment. Customer-created clusters use PostShiba's shared IP pool.

create-cluster.sh
1 curl -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 }
12 }'

Keep the returned id. Wait until sending_ready is true before you send. A cluster that is still starting, or one you suspended, returns cluster_not_ready.

2. Add a sending domain

Use a subdomain such as mail.example.com. It keeps your normal mailbox MX records separate from the records PostShiba needs.

create-domain.sh
1 curl -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 }'

The response includes the exact DNS values to publish:

domain-response.json
1 {
2 "id": 8,
3 "name": "mail.example.com",
4 "tenant_id": 12,
5 "tenant": "default",
6 "dkim_status": "pending",
7 "spf_status": "pending",
8 "return_path_status": "pending",
9 "dkim_cname_host": "s1._domainkey.mail.example.com",
10 "dkim_cname_target": "1-8.dkim.postshiba.com",
11 "spf_record": "v=spf1 include:spf.postshiba.com ~all",
12 "return_path_host": "rp.mail.example.com",
13 "return_path_target": "rp.postshiba.com",
14 "suspended": false
15 }

Publish the DKIM CNAME and return-path CNAME exactly as returned. The SPF include is optional. PostShiba blocks sending until both dkim_status and return_path_status are verified.

Ask PostShiba to check again after you publish DNS:

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

DNS changes are not instant. Read the domain again and wait for the two required statuses instead of sleeping for a fixed amount of time.

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

Once the domain verifies, you can send from any address at that domain. There is no separate sender identity to create.

3. Issue an SMTP credential

PostShiba injects REST API messages into the cluster through an active SMTP credential. Create the credential before calling /sends.

create-credential.sh
1 curl -sS \
2 --request POST \
3 --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/clusters/$CLUSTER_ID/smtp_credentials" \
4 --header "Authorization: Bearer $CAPSULE_API_KEY" \
5 --header "Content-Type: application/json" \
6 --data '{"smtp_credential":{}}'

The REST API returns the SMTP password on credential creation. Save it only if you also plan to send over SMTP. Authorized PostShiba dashboard users can currently reveal the encrypted password again. REST sends do not put that password in your application code.

4. Send a message

Start small. A from, at least one recipient, and a body are enough.

send.sh
1 curl -sS \
2 --request POST \
3 --url "$CAPSULE_URL/api/v1/teams/$TEAM_ID/clusters/$CLUSTER_ID/sends" \
4 --header "Authorization: Bearer $CAPSULE_API_KEY" \
5 --header "Content-Type: application/json" \
6 --data '{
7 "send": {
8 "from": "Acme Receipts <receipts@mail.example.com>",
9 "to": ["customer@example.net"],
10 "reply_to": "support@example.com",
11 "subject": "We received your order",
12 "text": "Thanks for your order. We will send tracking details soon.",
13 "html": "<p>Thanks for your order. We will send tracking details soon.</p>",
14 "unique_args": {
15 "order_id": "ord_123",
16 "kind": "receipt"
17 }
18 }
19 }'

A successful request returns 201:

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

Check that the response has "queued": true. HTTP 201 can also carry queued: false when the injector accepts no recipients. queued: true means at least one recipient queued, not that every recipient did. Send one recipient per request when you need per-recipient certainty.

Queued mail has not reached the recipient's server yet. Use a delivery webhook or the cluster event API to learn what happened next.

Add the fields your application needs

The send API also accepts:

  • cc and bcc
  • headers for custom headers
  • attachments with filename, content_type, and base64 content
  • tenant when you send for one of your own customers

When both text and html are present, PostShiba builds a multipart message. Keep the text part. It gives clients and accessibility tools a useful fallback.

This example adds a PDF attachment:

send-with-attachment.json
1 {
2 "send": {
3 "from": "Acme Billing <billing@mail.example.com>",
4 "to": ["customer@example.net"],
5 "subject": "Your invoice",
6 "text": "Your invoice is attached.",
7 "attachments": [
8 {
9 "filename": "invoice.pdf",
10 "content_type": "application/pdf",
11 "content": "BASE64_FILE_CONTENT"
12 }
13 ]
14 }
15 }

Do not put passwords, access tokens, or message contents in unique_args. PostShiba copies those values into delivery events. Good keys are IDs you already expose in logs, such as order_id, user_id, or notification_type.

Watch delivery

Create one webhook endpoint for delivery events:

create-webhook.sh
1 curl -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 ]
17 }
18 }'

Store the returned secret. Verify X-Capsule-Signature before you trust a webhook. PostShiba signs the exact body with HMAC-SHA256 over {timestamp}.{body}.

PostShiba emits processed, delivered, deferred, bounce, dropped, and spamreport. It does not emit open or click events.

Handle send errors

The send endpoint returns these error strings:

  • cluster_not_ready means the cluster cannot send yet
  • domain_unverified means the From domain is not ready or is suspended
  • credential_missing means the cluster has no active credential for this tenant
  • suppressed means at least one recipient is on the tenant's suppression list
  • invalid with HTTP 422 means from or all recipients are missing
  • HTTP 502 means the cluster did not accept the inject request; its error may be inject_failed or an upstream error string

PostShiba automatically suppresses hard bounces and spam reports. Treat a suppressed response as final until you review and remove that suppression.

PostShiba does not provide an idempotency key on this endpoint. If your connection drops after a request, you may not know whether the message queued. Put your own notification ID in unique_args, record the returned message_id, and make retry decisions in your application.

Where to go next

About

PostShiba is the transactional email platform that powers Bento behind the scenes. You can build your own products, like Bento, on top of it.

© 2026 PostShiba by Backpack Internet Pty. Ltd. All rights reserved.

The same policies that govern Bento are applied to PostShiba Privacy | Terms | Security