Messages
Send an email to a person who applied, with the unsubscribe link and a daily cap.
One endpoint sends an email to one person who applied to your jobs. It exists so that an integration can write to a candidate from inside your own tool, with the same safeguards as the Tahoe product: the unsubscribe link, the opt-out lists and a daily cap.
POST/messagesmessages:send
Sends one email and returns 200 once the mail provider has accepted it. The call needs an API key that belongs to a Tahoe user. A Sign in with Tahoe token gets 403 write_requires_api_key, unless writes for connected apps are switched on. messages:send is one of the paid plan scopes.
| Field | Type | Notes |
|---|---|---|
to | object, required | Exactly one of application_id (an app_ handle) or applicant_id (an apl_ handle). Both or neither is 400 invalid_request. There is no field for an email address, and none is accepted: the address is read from Tahoe’s own record of that person, in your workspace. |
subject | string, required | 1 to 200 characters, on one line, and not blank. |
body_text | string, required | 1 to 20,000 characters, not blank. It is the text part of the email and the fallback for mail clients that do not show HTML. |
body_html | string | Optional, up to 50,000 characters. Tahoe removes everything except basic formatting before sending. |
reply_to_user | boolean | Optional. Accepted and ignored: replies always go to the Tahoe user the key acts for. |
curl -X POST https://tahoe.workonward.com/api/partner/v1/messages \
-H "Authorization: Bearer $TAHOE_API_KEY" \
-H "Idempotency-Key: msg-6Qm2-next-steps-0001" \
-H "Content-Type: application/json" \
-d '{
"to": { "application_id": "app_6Qm2xKd4Rp8v" },
"subject": "Next steps for the Backend Engineer role",
"body_text": "Hi Jane,\n\nThanks for applying. Are you free for a call on Thursday?\n\nRecruiting team",
"body_html": "<p>Hi Jane,</p><p>Thanks for applying. Are you free for a call on Thursday?</p>"
}'{
"id": "msg_Zm9vQm1hcjR3",
"status": "sent",
"to": { "application_id": "app_6Qm2xKd4Rp8v" },
"sent_at": "2026-10-08T14:03:11.482913Z"
}idis amsg_handle for the message. Store it and do not parse it.statusissentwhen the mail provider accepted the message, andqueuedwhen an earlier request with the same key is still being delivered.sentdoes not mean the person read it, or that it reached their inbox.torepeats the kind of handle you sent, in its canonical form. The response holds no email address, name or message content.sent_atis when the provider accepted it, in UTC, andnullwhilestatusisqueued.- The call is in the
expensiverate-limit tier. It emits no event, because the product emits none when a recruiter sends a message.
What the person receives
- The email comes from Tahoe’s shared sending address, not from your domain, as every email the Tahoe product sends.
Reply-Tois the email of the Tahoe user who created the key, so replies land in that person’s own mailbox, and it cannot be changed per request. Tahoe has no inbox and does not read replies. - Every message ends with Tahoe’s unsubscribe footer and carries the one-click
List-Unsubscribeheader. They cannot be switched off or edited. The footer link unsubscribes the person from email sent through Tahoe for that workspace. - Delivery is handed to the mail provider before the response returns. A reserved test address, such as one at
example.com, is never mailed and returns409 recipient_unsubscribed.
What your team sees
The message is stored with its subject, text, cleaned HTML, recipient, sender and status, and it appears in the person’s sent-email list in Tahoe as if the key’s user had sent it. An email_sent entry appears on the application’s timeline, or in the workspace activity when you named the recipient by applicant. It records the recipient address, the subject and the delivery status, attributed to the person who created the key.
Who can be emailed
Only people who applied to the key’s workspace: applicants and their applications. A sourced profile that has not applied cannot be emailed through this endpoint. Three records are checked, and any one of them stops the send:
- the person’s own unsubscribe in the workspace;
- the workspace’s outreach opt-out list;
- Tahoe’s suppression and erasure records, which are kept after a person’s data is deleted and apply across workspaces.
If any of these cannot be read, nothing is sent. A refused send returns 409 recipient_unsubscribed, and you should not retry it.
{
"detail": {
"code": "recipient_unsubscribed",
"type": "conflict",
"message": "This person cannot be emailed. Nothing was sent.",
"param": null
}
}The body_html field
Tahoe’s own send path does not clean HTML, because only a signed-in recruiter can reach it. This endpoint cleans it, because the caller is a program.
- Kept:
a(onlyhttp,httpsormailtolinks, rewritten withrel="noopener noreferrer nofollow"),b,strong,i,em,u,p,br,hr,div,span,ul,ol,li,blockquote,h1toh4,codeandpre. Every attribute except the link target is removed. - Removed with their content:
script,style,iframe,object,embed,svg,math,formand form controls. All other tags are removed and their text is kept. - Images are not allowed. If nothing is left after cleaning, the text part alone is used for the HTML view.
Limits
A key may send at most 200 messages per UTC day by default. This is separate from the per-minute request limits. Past it, the call returns 429 message_quota_exceeded, and Retry-After is the number of seconds to UTC midnight. A request that fails does not use up allowance.
Retries
- The key is scoped to the credential, the workspace and the path, and its record lasts 24 hours. Same key and same body: you get the first answer back, with
Idempotent-Replayed: true, and no second email is sent. Same key and a different body:409 idempotency_key_reused. - The key is also written onto Tahoe’s record of the sent email. If the stored answer is gone (for example, the 24 hour record expired) and the request is repeated, Tahoe finds the earlier send, returns it, sends nothing and does not use up allowance. This check runs before the unsubscribe check on purpose: a person who unsubscribed after the first attempt is told the message was sent, not that it was refused.
- A request that fails with an error releases its key, so you can retry with the same key once the cause is fixed. The exception is
delivery_failed: the same key reports the same failure again, by design, so a failed send is never repeated by accident. Retry with a new key.
Errors
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_request | The body is malformed, a field is too long, the subject has a line break, both or neither handle was sent, or a field the endpoint does not accept is present, such as email or reply_to. The errors list names the fields. |
| 400 | idempotency_key_required | Add an Idempotency-Key header. |
| 400 | invalid_idempotency_key | The key is not 8 to 255 allowed characters. |
| 403 | insufficient_scope | The key lacks messages:send. |
| 403 | write_requires_api_key | The credential is a Sign in with Tahoe token, or is not tied to a Tahoe user. |
| 404 | not_found | The application or applicant is not in the key’s workspace. A handle from another workspace, one of the wrong type and a malformed one all give this answer. |
| 409 | recipient_unsubscribed | The person unsubscribed, was suppressed or erased, or has an address that cannot receive mail. Nothing was sent. Do not retry. |
| 409 | sender_email_missing | The Tahoe user the key acts for has no email address, so replies would have nowhere to go. |
| 409 | idempotency_key_reused | The key was used before with a different body. |
| 409 | idempotency_in_flight | A request with this key is still running. Retry in a moment. |
| 429 | rate_limit_exceeded | Over the per-minute limit. Wait for Retry-After. |
| 429 | message_quota_exceeded | The key has sent its daily maximum. Wait for Retry-After. |
| 429 | limiter_unavailable | The counters that enforce the limits could not answer. Nothing was sent. Retry after Retry-After. |
| 429 | datastore_unavailable | The database or the unsubscribe records could not answer. Nothing was sent. Retry after Retry-After. |
| 429 | delivery_failed | The mail provider refused or failed. Nothing reached the person. Retry with a new Idempotency-Key. |
The last three have type: "unavailable" and use 429 rather than 503, as explained under A 429 is not always a rate limit. Branch on detail.code.