Errors and QA codes.
Two vocabularies an agent can act on without reading prose. Transport errors tell you the call went wrong. QA codes tell you a draft is not ready to send, and what would fix it.
Error shape
{
"ok": false,
"tool": "create_campaign",
"kind": "write",
"data": null,
"error": { "code": "invalid_input", "message": "conference_url must be an http(s) URL" }
}Error codes
| code | HTTP | What it means |
|---|---|---|
| unauthenticated | 401 | Missing or invalid API key. |
| forbidden_scope | 403 | Key lacks the required scope (read, write or send). |
| invalid_request | 400 | Arguments failed schema validation. |
| not_found | 404 | No such resource, or not owned by this account. |
| rate_limited | 429 | Too many calls. Back off and retry. |
| tool_error | 500 | The capability failed while running. |
QA hard-fail codes
A draft that trips any of these is held back from approval. Read them from get_draft_qa, or fetch this same list at runtime from GET /api/public/v1/qa-codes. Drafts scoring below 8 never bulk-approve.
Opener references an unnamed event. Use a named anchor or switch to Pattern B (role + industry).
Opener invents an insight. Anchor on an extracted signal or drop to Pattern B.
Remove presumed pain. State only what the research verified.
Strip speculative verbs (must be, probably, I imagine).
Add a concrete anchor: amount, product, partner, metric or talk title.
Pitch line does not match the detected persona. Re-pick from persona_pitch_map.
Opener contradicts itself. Keep one claim.
Remove AI-tell phrasing; open plain and human.
Replace appointment-speak CTA with a specific shared-context ask.
Replace the 'worth ___' CTA family with a concrete ask.
Remove the URL from M1.
M2/M3 duplicate M1. Reframe without recycling the M1 anchor.
Message references the recipient's own company as the signal. Use a different anchor.
Remove the banned phrase or unverified market-category noun.
Remove the campaign-banned phrase.
Strip ™/® symbols.
Strip emoji.
Locked CTA phrase missing from M1; reinsert verbatim.
Locked pitch phrase missing from M1; reinsert verbatim.
Required percentage figure missing or altered.
Add the configured booth CTA.
Mail-merge placeholder leaked; resolve {first_name}/{company} before send.
Draft was cut off by a token cap. Redraft with a higher cap.
Fragment sentence; redraft the affected slot.
Filler opener; anchor on the talk topic or a real signal.
M1 exceeds the hard word ceiling; cut to target length.
A session title exists — anchor the opener on it.
Opener ignores the talk title. Reference the actual session topic.
Wrong event year referenced; use the campaign's target year.
Talk topic is stale; use the current session.
Do not assert the recipient is attending; frame the CTA conditionally.
M3 opens by announcing itself ('I'll stop here', 'this is my last one'). Lead with the question; offer to close the loop at the end instead.
'is now not the time' tells you nothing whichever way they answer. Branch on fit ('not relevant to what you do') or offer to stop ('should I close the loop').
Tells them what their own news means, guesses at their work, or predicts their needs ('which usually means', 'which puts X on your desk', 'is probably the constraint', 'you're about to'). Keep the fact, delete the inference, go straight to what we do.
Pitches from the job title ('as a {title}, you probably...'). Replace with something observed about this specific person, or anchor on their talk or the conference.
Invented tenure/career claim with no signal. Redraft with Pattern B.
The sender's own name appears in the body as a third party (a customer, an investor, a co-founder, someone who 'said' something). The sender is the author. Delete every mention of the sender's name from the body; a trailing first-name sign-off is the only exception.
The draft states a funding round, a raise, or a named investor and nothing in this recipient's evidence mentions funding. Delete the claim and anchor on a fact that is in the evidence.
Proof point altered; restore verbatim from campaign config.
Body was fully lowercased; restore proper-noun casing.
Claim about the speaker is unverified; drop it or swap to a verified anchor.
Industry framing does not fit; use industry_challenge_map for this industry.
The usual repair loop is get_draft_qa(only_failing: true) then request_rewrite with the failing code as the reason, then re-read the QA state.