/api/qmail/net/messages/tell
GET POSTTell-only async endpoint. Loads the receipt for a given file_guid and fires Tell2 to each unique beacon RAIDA covering its recipient list. The recipients come from the receipt — request-side overrides are rejected with 409 per Q25(a).
The link above uses 00000000000000000000000000000000 as a placeholder. Replace it with a real file_guid returned by an earlier /upload call before clicking — otherwise the response is HTTP 404 Receipt not found.
Description
The /api/qmail/net/messages/tell endpoint is the second half of the two-step send flow. It takes a file_guid from a prior /upload call, rehydrates the upload artifacts from the receipt at <Mail>/Receipts/<file_guid>.json, rehydrates the sender's per-RAIDA encryption ANs from the Mail wallet, and fires a Tell2 to each unique beacon RAIDA in the recipient list.
Per Q19(a), receipts never contain bearer secrets — neither sender_ans nor enc_key.ans are ever serialized. /tell re-reads the per-RAIDA ANs from the Mail wallet at run time using the receipt's sender_serial_number. If the sender identity coin can no longer be located (wallet moved, file deleted), the task fails with "Sender identity coin not found in Mail wallet".
The handler claims the receipt synchronously before spawning the worker, so duplicate /tell calls and re-tells against finished receipts are deterministic. The rejection rules are:
tell.status == "in_progress"— another/tellworker holds the claim. Returns HTTP409with"Tell already in progress; poll /api/qmail/receipts for status".tell.status == "success"— every recipient has been delivered. Returns HTTP409with"Tell already complete for this email".- Every recipient already
"sent"(e.g. a re-tell on a partial that the retry worker has since drained). Returns HTTP409with"No recipients need Tell; all receipt rows are already sent". upload.status != "success"— upload is still in flight or failed;/tellwon't proceed. Returns HTTP409with the current upload status in the message.
Retrying is allowed from tell.status == "failed" or tell.status == "partial". The handler claims the receipt, marks every recipient that is not already "sent" as "sending", and the worker only targets those rows. Recipients that are already "sent" are never re-told.
For background retries the retry worker handles transient beacon failures automatically. See /api/qmail/db/tells/retry.
Supplying to, cc, or bcc on a /tell request is rejected with HTTP 409 Conflict. Recipients are sourced exclusively from the receipt's tell.recipients[], which is seeded by /upload when the caller passes recipient hints. If the receipt has no recipients, the call fails with HTTP 400; re-upload with hints, or use /upload_and_tell.
Parameters
Send parameters as form data (POST) or query string values (GET). Form-encoded POST bodies are merged into the params table, so either method works.
| Parameter | Type | Required | Description |
|---|---|---|---|
file_guid |
string (32 hex chars) | Yes | The GUID returned by a prior /upload or /upload_and_tell call. Returns HTTP 404 if no receipt exists for that GUID. |
wallet_path / wallet |
string | No | Wallet used to fund any pool work performed during Tell (the inbox-fee locker confirmation step). Defaults to Default. |
debug |
boolean | No | Set debug=true to enable per-request LOG_DEBUG verbosity for this one call. Useful for following the rehydrate→Tell flow in main.log. |
Per Q25(a), the following parameters are rejected with HTTP 409 if any are present: to, cc, bcc. Recipients come from the receipt only.
Response
The handler returns HTTP 200 OK immediately after spawning the Tell worker. Poll /api/system/tasks?id=<task_id> for status, or fetch the receipt at /api/qmail/receipts?guid=<file_guid> to see the per-recipient status (each row's status, attempts, last_error, timestamps).
Success Response Properties (immediate)
true when the Tell worker thread was spawned.qmail-tell.not_started receipt this equals the total recipient count; on a retry from failed or partial it is the count of rows that are not already "sent"."sent" in a previous Tell. Compare with recipient_count to see how many rows are being skipped on a retry.rid=<id>; grep "rid=<id>" main.log retrieves the full transaction.Success Response Example (immediate)
{
"success": true,
"command": "qmail-tell",
"request_id": "5a91c802deadbeef",
"task_id": "task_8c7d5e10",
"url": "/api/system/tasks?id=task_8c7d5e10",
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"recipient_count": 1,
"receipt_recipient_count": 1,
"message": "Tell started — poll /api/system/tasks for status, or GET /api/qmail/receipts?guid=... for receipt detail"
}
Task Poll Example (after Tell completes — all accepted)
{
"status": "success",
"task_id": "task_8c7d5e10",
"progress": 100,
"message": "Tell complete: all beacons accepted",
"data": {
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"beacons": 1,
"tell_successes": 1,
"tell_failures": 0,
"all_accepted": true
}
}
Task Poll Example (Tell completed — partial, retry queued)
{
"status": "success",
"task_id": "task_8c7d5e10",
"progress": 100,
"message": "Tell complete: some beacons failed (queued for retry)",
"data": {
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"beacons": 3,
"tell_successes": 2,
"tell_failures": 1,
"all_accepted": false
}
}
Error Response — Receipt Not Found (HTTP 404)
{
"error": true,
"message": "Receipt not found for that file_guid",
"code": 404
}
Error Response — Tell Already In Progress (HTTP 409)
{
"error": true,
"message": "Tell already in progress; poll /api/qmail/receipts for status",
"code": 409
}
Error Response — Tell Already Complete (HTTP 409)
{
"error": true,
"message": "Tell already complete for this email",
"code": 409
}
Error Response — All Recipients Already Sent (HTTP 409)
Returned on a re-tell against a partial receipt where the retry worker has since drained the queue.
{
"error": true,
"message": "No recipients need Tell; all receipt rows are already sent",
"code": 409
}
Error Response — Recipient Override Rejected (HTTP 409)
{
"error": true,
"message": "Recipient overrides are not allowed; recipients come from the receipt",
"code": 409
}
Interactive API Tester
Test this Endpoint
Examples
cURL — Two-Step Compose (upload + tell)
# Step 1 — stage the upload (records the receipt with recipient hints)
GUID=$(curl -s "http://localhost:8081/api/qmail/net/messages/upload?body=Hi&to=%40chariot.harbor.byte" | jq -r .file_guid)
# Step 2 — fire Tell using the GUID
curl "http://localhost:8081/api/qmail/net/messages/tell?file_guid=$GUID"
JavaScript (fetch)
// Fire Tell for a previously uploaded GUID
const fileGuid = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'; // from /upload
const tellUrl = `http://localhost:8081/api/qmail/net/messages/tell?file_guid=${fileGuid}`;
const tell = await fetch(tellUrl).then(r => r.json());
if (!tell.success) {
throw new Error(`Tell kickoff failed: ${tell.message} (HTTP ${tell.code ?? '?'})`);
}
console.log(`Tell task=${tell.task_id} (${tell.recipient_count} recipient${tell.recipient_count === 1 ? '' : 's'})`);
// Poll the task until status leaves "running"
async function pollTask(taskId) {
while (true) {
const r = await fetch(`/api/system/tasks?id=${encodeURIComponent(taskId)}`);
const t = await r.json();
if (t.status !== 'running') return t;
await new Promise(r => setTimeout(r, 1000));
}
}
const finalState = await pollTask(tell.task_id);
console.log('Tell result:', finalState);
Python
import time
import requests
BASE = 'http://localhost:8081/api'
file_guid = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6' # from /upload
tell = requests.get(f'{BASE}/qmail/net/messages/tell',
params={'file_guid': file_guid}).json()
if not tell.get('success'):
raise SystemExit(f"Tell kickoff failed: {tell.get('message')} (HTTP {tell.get('code')})")
task_id = tell['task_id']
print(f'task={task_id} recipients={tell["recipient_count"]}')
while True:
t = requests.get(f'{BASE}/system/tasks', params={'id': task_id}).json()
if t['status'] != 'running':
break
time.sleep(1)
print('Tell result:', t)
Important Notes
If /upload reserved an inbox-fee locker (i.e., recipient hints were supplied), /tell confirms it on success. If /tell fails before any beacon is accepted, the locker stays RESERVED for the (deferred) Phase 8 reclamation sweep.
Beacons that fail on the first attempt are queued for retry by qmail_tell_retry.c. Each retry updates the corresponding recipient row in the receipt (status, attempts, last_error, timestamps), and consults /api/qmail/db/tells/list_pending for queue inspection.
Per Phase 1 BUG-11, Tell2 cannot be sent unencrypted. If no encryption key is available at run time, the task fails with "No encryption key available for tell". The retry worker handles the same condition by deferring the row back to pending rather than sending a plaintext Tell.
Append &debug=true to enable LOG_DEBUG output for this single request. Particularly useful for verifying the rehydrate path (sender ANs, encryption key selection, beacon RAIDA dispatch).