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.
Invented tenure/career claim with no signal. Redraft with Pattern B.
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.