{"openapi":"3.1.0","info":{"title":"BobtailMail API","version":"1","description":"Transactional email: REST and MCP call the same code and share the same contracts. Full reference: https://bobtailmail.com/docs"},"servers":[{"url":"https://bobtailmail.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"bm_live_…"}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["type","message"],"properties":{"type":{"type":"string","enum":["invalid_request","unauthorized","not_found","send_rejected","no_active_plan","plan_limit_reached","rate_limited","upstream_unavailable","internal_error"]},"message":{"type":"string"},"details":{}}}}},"Message":{"type":"object","required":["id","channel","status","from","to","cc","bcc","replyTo","subject","text","html","error","createdAt","updatedAt"],"properties":{"id":{"type":"string","pattern":"^msg_[0-9a-z]{26}$"},"channel":{"type":"string"},"status":{"type":"string","enum":["accepted","sent","delivered","bounced","complained","rejected","failed"]},"from":{"type":"string"},"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"replyTo":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"text":{"type":["string","null"]},"html":{"type":["string","null"]},"error":{"type":["object","null"],"properties":{"code":{"type":["string","null"]},"message":{"type":["string","null"]}}},"carrier":{"type":"object","properties":{"latencyMs":{"type":"integer"},"costMicros":{"type":["integer","null"]}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"data":{},"createdAt":{"type":"string","format":"date-time"}}}}}},"Domain":{"type":"object","required":["id","name","mailFromSubdomain","verified","dkimVerified","mailFromVerified","dnsRecords","lastCheckedAt","createdAt"],"properties":{"id":{"type":"string","pattern":"^dom_[0-9a-z]{26}$"},"name":{"type":"string"},"mailFromSubdomain":{"type":"string"},"verified":{"type":"boolean"},"dkimVerified":{"type":"boolean"},"mailFromVerified":{"type":"boolean"},"dnsRecords":{"type":"array","items":{"type":"object","required":["kind","name","type","value","priority","verified"],"properties":{"kind":{"type":"string","enum":["dkim","spf","dmarc"]},"name":{"type":"string"},"type":{"type":"string","enum":["CNAME","MX","TXT"]},"value":{"type":"string"},"priority":{"type":["integer","null"]},"verified":{"type":["boolean","null"]}}}},"lastCheckedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"WebhookEndpoint":{"type":"object","required":["id","url","createdAt","revokedAt"],"properties":{"id":{"type":"string","pattern":"^whe_[0-9a-z]{26}$"},"url":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"revokedAt":{"type":["string","null"],"format":"date-time"},"secret":{"type":"string","pattern":"^whsec_"}}}}},"paths":{"/v1/messages":{"post":{"summary":"Send a message","operationId":"sendMessage","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Binds for 24h — same key, same message, no double send."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","maxLength":420},"to":{"anyOf":[{"type":"string"},{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string"}}]},"cc":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"bcc":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"replyTo":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"subject":{"type":"string","minLength":1,"maxLength":998},"text":{"type":"string","minLength":1,"maxLength":1000000},"html":{"type":"string","minLength":1,"maxLength":2000000}},"required":["from","to","subject"],"additionalProperties":false}}}},"responses":{"200":{"description":"Idempotent replay of a prior send","headers":{"Idempotent-Replay":{"schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"201":{"description":"Accepted and recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"402":{"description":"No active plan or monthly cap reached"},"422":{"description":"Refused before or by the carrier (send_rejected)"},"429":{"description":"Per-workspace rate limit"},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"List messages","operationId":"listMessages","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"before","in":"query","schema":{"type":"string","pattern":"^msg_[0-9a-z]{26}$"}}],"responses":{"200":{"description":"A page of messages, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}}}}}}}}},"/v1/messages/batch":{"post":{"summary":"Send a batch of messages (M9)","operationId":"sendMessageBatch","description":"N independent one-to-one deliveries in one call, up to 100 — never one message to many recipients. Every item carries exactly one `to` address; a per-recipient RFC 8058 List-Unsubscribe/List-Unsubscribe-Post is attached automatically. Always 200: every item gets its own outcome, positionally matched to the request array, so a rate limit or plan cap hit partway through never fails the whole batch.","requestBody":{"required":true,"content":{"application/json":{"schema":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"object","properties":{"from":{"type":"string","maxLength":420},"to":{"anyOf":[{"type":"string"},{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string"}}]},"cc":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"bcc":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"replyTo":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"subject":{"type":"string","minLength":1,"maxLength":998},"text":{"type":"string","minLength":1,"maxLength":1000000},"html":{"type":"string","minLength":1,"maxLength":2000000},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255}},"required":["from","to","subject"],"additionalProperties":false}}}}},"responses":{"200":{"description":"Every item's own outcome, positionally matched to the request array","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","required":["index","status"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["sent","rejected","upstream_unavailable","rate_limited","no_active_plan","plan_limit_reached"]},"message":{"$ref":"#/components/schemas/Message"},"limit":{"type":"integer"},"retryAfterSeconds":{"type":"integer"},"count":{"type":"integer"},"resetAt":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"Malformed request — more than one `to` recipient in an item, more than 100 items, etc."},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/messages/{id}":{"get":{"summary":"Get a message","operationId":"getMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The message, with its current status and event trail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"404":{"description":"No message with that id in this workspace"}}}},"/v1/domains":{"post":{"summary":"Register a sending domain","operationId":"createDomain","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":253,"pattern":"^(?!-)[a-z0-9-]{1,63}(?<!-)(\\.(?!-)[a-z0-9-]{1,63}(?<!-))+$"}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Registered, with the DNS records to set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}}}},"get":{"summary":"List domains","operationId":"listDomains","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"before","in":"query","schema":{"type":"string","pattern":"^dom_[0-9a-z]{26}$"}}],"responses":{"200":{"description":"A page of domains","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}}}}}}}}}},"/v1/domains/{id}/verify":{"post":{"summary":"Poll the carrier for a fresh verification result","operationId":"verifyDomain","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current verification state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}}}}},"/v1/webhook-endpoints":{"post":{"summary":"Register a webhook endpoint","operationId":"createWebhookEndpoint","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048}},"required":["url"],"additionalProperties":false}}}},"responses":{"201":{"description":"Registered — the signing secret is shown only in this response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}},"get":{"summary":"List webhook endpoints","operationId":"listWebhookEndpoints","responses":{"200":{"description":"Your endpoints (secret omitted)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}}}}}},"/v1/webhook-endpoints/{id}":{"delete":{"summary":"Revoke a webhook endpoint","operationId":"revokeWebhookEndpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked"}}}}}}