{"record":{"id":"e86e1f7a8f1ceb8f","repo":"zeroclaw-labs/zeroclaw","slug":"twitter-dm-send-failed-status-err","errorCode":null,"errorMessage":"Twitter DM send failed ({status}): {err}","messagePattern":"Twitter DM send failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/twitter.rs","lineNumber":170,"sourceCode":"    async fn send_dm(&self, recipient_id: &str, text: &str) -> anyhow::Result<()> {\n        let body = json!({\n            \"text\": text,\n        });\n\n        let resp = self\n            .http_client()\n            .post(format!(\n                \"{TWITTER_API_BASE}/dm_conversations/with/{recipient_id}/messages\"\n            ))\n            .bearer_auth(&self.bearer_token)\n            .json(&body)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let err = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"Twitter DM send failed ({status}): {err}\");\n        }\n\n        Ok(())\n    }\n}\n\nimpl ::zeroclaw_api::attribution::Attributable for TwitterChannel {\n    fn role(&self) -> ::zeroclaw_api::attribution::Role {\n        ::zeroclaw_api::attribution::Role::Channel(\n            ::zeroclaw_api::attribution::ChannelKind::Twitter,\n        )\n    }\n    fn alias(&self) -> &str {\n        &self.alias\n    }\n}\n\n#[async_trait]","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/twitter.rs#L152-L188","documentation":"Raised by TwitterChannel::send_dm when POST /2/dm_conversations/with/{recipient_id}/messages answers non-2xx. The recipient travels in the URL as a numeric user id (the channel routes recipients prefixed \"dm:\"), and the body carries only {text}; DMs allow up to 10000 chars per the channel's own routing note.","triggerScenarios":"403 when the app lacks dm.write/dm.read scopes, 403 when the recipient does not follow the account or has DMs closed, 404 for a malformed or nonexistent recipient id, and 429 on DM rate limits.","commonSituations":"The token covers tweeting but not DMs. End users who never enabled open DMs cannot be messaged, so the bot fails exactly on the DM path while tweets work. Passing an @handle instead of the numeric user id as the dm: recipient.","solutions":["Regenerate the token with dm.write (and dm.read) scopes.","Have the recipient follow the account or open DMs — X blocks DMs to closed recipients with 403.","Verify the recipient value after \"dm:\" is the numeric user id, not a handle.","For 429, respect the DM rate window before retrying."],"exampleFix":"// before — dm: recipient holds a handle, not the numeric id\nchannel.send(&SendMessage { recipient: \"dm:@alice\".into(), content: text.into() }).await?;\n\n// after — numeric user id resolved beforehand\nchannel.send(&SendMessage { recipient: \"dm:2244994945\".into(), content: text.into() }).await?;","handlingStrategy":"retry","validationCode":"// The dm: recipient must be a numeric user id.\nfn is_twitter_user_id(recipient: &str) -> bool {\n    !recipient.is_empty() && recipient.chars().all(|c| c.is_ascii_digit())\n}\n\nif let Some(id) = msg.recipient.strip_prefix(\"dm:\") {\n    assert!(is_twitter_user_id(id), \"dm recipient must be the numeric user id\");\n}","typeGuard":"fn is_numeric_user_id(v: &str) -> bool {\n    !v.is_empty() && v.chars().all(|c| c.is_ascii_digit())\n}","tryCatchPattern":"match twitter.send(&msg).await {\n    Ok(()) => Ok(()),\n    Err(err) => {\n        let t = err.to_string();\n        if t.contains(\"Twitter DM send failed (403)\") {\n            // closed DMs or missing dm.write scope — retrying cannot help\n            Err(err.context(\"recipient must follow the account / open DMs; token needs dm.write\"))\n        } else if t.contains(\"(429)\") {\n            tokio::time::sleep(Duration::from_secs(60)).await;\n            twitter.send(&msg).await\n        } else {\n            Err(err)\n        }\n    }\n}","preventionTips":["Request dm.write and dm.read scopes when the token is created.","Resolve handles to numeric user ids before constructing \"dm:\" recipients.","For bot use, ask users to follow the account or enable open DMs; closed-DM 403s are recipient-side and permanent."],"tags":["twitter","x-api","dm","http","auth"],"backgroundTag":"twitter-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}