Send your application's own email - a support reply, a sign-in code - on behalf of a subscribing workspace, through Connect's sending identity and its bounce and complaint suppression list.
Send your application's own email - a helpdesk reply, a portal sign-in code - for a workspace that subscribes to you, without an email provider account of your own.
Who it is from
By default every email leaves from Connect's sending address, support@cosmoops.com, under a display name: the fromName you send, or the workspace's name when you send none. Put your inbound address in replyTo so the customer's answer reaches you.
With the email:send_as scope you may name fromAddress instead - on your application's hosted domain (set by CosmoOps in Console), or on one of the workspace's verified sending domains. Anything else is refused with sending_domain_unverified and nothing is sent: Connect never quietly swaps the From, so the fallback is yours to choose. A domain verified in the last 7 days sends at most 500 emails a day while it warms up. The response's from names the address that was used.
Threads
Mail clients thread on In-Reply-To and References and nothing else. Every send answers the messageId the email actually left under - Connect's mail provider replaces any Message-ID you propose, so store the one in the response, not your own. Reply with headers.inReplyTo set to the message you are answering and headers.references to the thread so far.
Addresses that cannot receive
Connect keeps one suppression list for the whole platform: an address that hard-bounced or marked mail as spam is never sent to again until an operator clears it. Your mail shares that list and that reputation. A suppressed recipient is skipped, the others are sent, and the skipped ones come back in suppressed - show the address as undeliverable rather than retrying, because a retry is skipped the same way.
Budgets
Per key, and per key and recipient - a ticket thread is a few replies an hour. Send an Idempotency-Key with every call: a retried send without one is a second copy in somebody's inbox.
Endpoints
POST /api/v1/application/email/send
Send an email on a workspace's behalf
Scope email:send · Rate limit 120 requests / min per key, 30 requests / hour per key+recipient
Sends your application's own email - a support reply, a portal sign-in code - for a workspace that subscribes to your application. Server-to-server only: a request carrying a member's session token is refused.
Implementation
1. Prerequisite - an API key scoped to email:send.
2. Send - html and text are both required (256 KB each); to and cc take up to 10 addresses each:
curl -X POST https://connect.cosmoops.com/api/v1/application/email/send \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-1042-reply-3" \
-d '{"workspaceId":"WORKSPACE_ID","to":["priya@example.com"],"subject":"Re: Refund for order 1042","html":"<p>Done - it is on its way.</p>","text":"Done - it is on its way.","replyTo":"t-1042@in.example.com","fromName":"Acme Support","headers":{"inReplyTo":"<CAF0x9@mail.gmail.com>","references":"<CAF0x9@mail.gmail.com>"}}'3. The sender. Without fromAddress, the email leaves from Connect's address (support@cosmoops.com) with fromName as the display name - the workspace's name when you send none. Put your own inbound address in replyTo to receive the answer.
With the email:send_as scope, fromAddress names the sender: any address on your application's hosted domain, or one on the workspace's verified sending domains (/email/domains). An unverified domain is refused with sending_domain_unverified and nothing is sent - re-send without fromAddress if you want Connect's address instead. The response's from is the address actually used.
4. Threading. Store the response's messageId: it is the Message-ID the email actually left under (Connect's mail provider replaces any headers.messageId you send), and a customer's reply names it in In-Reply-To. Send the next reply with headers.inReplyTo and headers.references set from the thread. Header values may not contain line breaks.
5. Suppression. An address that hard-bounced or marked Connect's mail as spam is skipped and listed in suppressed; the rest are sent. When every recipient is suppressed nothing is sent and the call still answers 201 - show the customer's address as undeliverable rather than retrying.
Errors
validation_error (400 - bad body, too many recipients, an oversize body, or a line break in a header value), forbidden (403 - key missing email:send, a session token was forwarded, or your application is not installed in that workspace), from_address_not_allowed (403 - fromAddress without the email:send_as scope), idempotency_key_reused/request_in_progress (409), sending_domain_unverified (409 - fromAddress is not on your hosted domain or a verified sending domain of this workspace; nothing was sent), rate_limited (429 - per key, and per key and recipient), domain_warming_up (429 - a domain verified in the last 7 days has sent its 500 for today; honour Retry-After), delivery_failed (502 - retry).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
to | array | yes | |
cc | array | no | |
subject | string | yes | |
html | string | yes | |
text | string | yes | |
replyTo | email | no | |
fromAddress | email | no | |
fromName | string | no | |
headers | object | no |
Responses
| Status | Meaning |
|---|---|
201 | Sent (or every recipient suppressed - see suppressed) |
400 | validation_error - bad body, recipient cap, oversize body, or a line break in a header |
403 | forbidden - key missing email:send, a forwarded session, or not installed in that workspace; from_address_not_allowed - fromAddress without email:send_as |
409 | idempotency_key_reused / request_in_progress; sending_domain_unverified - fromAddress is on no verified sending domain, nothing sent |
429 | rate_limited; domain_warming_up - a newly verified domain's daily cap |
502 | delivery_failed - the mail provider refused the message; retry |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
messageId | string | yes | |
suppressed | array | yes | |
from | string | yes |
GET /api/v1/application/email/domains
List a workspace's sending domains
Scope email:domains · Rate limit 60 requests / min per key
Every domain the workspace has registered for sending, each with the DNS records it must publish and whether each one is in place as of the last check. A sending domain belongs to the workspace, not to your application - every application with email:domains sees the same list. Server-to-server only.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | query | yes | A workspace subscribed to your application. |
Responses
| Status | Meaning |
|---|---|
200 | The workspace's sending domains |
400 | validation_error - workspaceId missing |
403 | forbidden - key missing email:domains, a forwarded session, or not installed in that workspace |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
domains | array | yes |
POST /api/v1/application/email/domains
Add a sending domain
Scope email:domains · Rate limit 60 requests / min per key
Registers the workspace's own domain (acme.com) so applications may send From addresses on it, and answers the records to publish. Idempotent: adding a domain the workspace already holds answers the existing one, with its unchanged token.
Records
| Purpose | Name | Type | Required |
|---|---|---|---|
ownership | _cosmoops.<domain> | TXT cosmoops-verification=<token> | yes |
dkim ×3 | <token>._domainkey.<domain> | CNAME <token>.dkim.amazonses.com | yes |
mail_from_mx | bounce.<domain> | MX 10 feedback-smtp.<region>.amazonses.com | recommended |
mail_from_spf | bounce.<domain> | TXT v=spf1 include:amazonses.com ~all | recommended |
dmarc | _dmarc.<domain> | TXT v=DMARC1; p=none; | only listed when the domain has none |
The domain is verified once the ownership TXT and DKIM both pass. Connect re-checks pending domains every ten minutes for 72 hours and verified ones hourly; a verified domain whose records break is failing (it still sends) and, after 72 hours, failed. The workspace's admins are emailed on each change.
Errors
validation_error (400 - not a domain name), forbidden (403), domain_claimed (409 - another workspace has verified it), conflict (409 - the workspace holds the maximum number of domains), domain_not_allowed (422 - a free-mail provider, a public suffix, or a CosmoOps domain).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
domain | string | yes |
Responses
| Status | Meaning |
|---|---|
201 | The domain, pending, with the records to publish |
400 | validation_error - not a domain name |
403 | forbidden - key missing email:domains, a forwarded session, or not installed in that workspace |
409 | domain_claimed - verified by another workspace; conflict - domain limit reached |
422 | domain_not_allowed - free mail, a public suffix, or a CosmoOps domain |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
domain | object | yes |
GET /api/v1/application/email/domains/{domain}
Get a sending domain
Scope email:domains · Rate limit 60 requests / min per key
One domain as of its last check - no DNS lookup is made. Use POST /email/domains/{domain}/check to re-check now.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
domain | path | yes | The domain, e.g. acme.com. |
workspaceId | query | yes | A workspace subscribed to your application. |
Responses
| Status | Meaning |
|---|---|
200 | The domain |
403 | forbidden |
404 | not_found - the workspace holds no such domain |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
domain | object | yes |
POST /api/v1/application/email/domains/{domain}/check
Re-check a sending domain
Scope email:domains · Rate limit 60 requests / min per key
Checks SES and DNS now and answers the updated domain, with a status on every record (ok, missing, mismatch, unknown). Connect also does this on its own, so there is no need to poll.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
domain | path | yes | The domain, e.g. acme.com. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | The domain, freshly checked |
403 | forbidden |
404 | not_found - the workspace holds no such domain |
503 | unavailable - SES could not be reached; retry |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
domain | object | yes |
DELETE /api/v1/application/email/domains/{domain}
Remove a sending domain
Scope email:domains · Rate limit 60 requests / min per key
Removes the domain from the workspace; mail can no longer be sent From it. 204 on success.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
domain | path | yes | The domain, e.g. acme.com. |
workspaceId | query | yes | A workspace subscribed to your application. |
Responses
| Status | Meaning |
|---|---|
204 | Removed |
403 | forbidden |
404 | not_found - the workspace holds no such domain |
Send your application's own Meta-approved WhatsApp templates to a workspace's customers, through Connect's WhatsApp sender - and check whether a template is live before offering it.
Push notifications
Put a notification on a member's lock screen. Connect holds the VAPID keys, the devices and the member's own preferences - your application supplies a title, a body and a path.