Skip to main content
The request body, headers, or query parameters didn’t match the documented schema, or the request violated a batch-level rule (target count, mode conflict, account resolution). This is rejection at the route boundary — platform-specific preflight (which checks platform constraints) only runs after validation passes.

What triggers it

  • Required field missing (e.g. no targets[], no text).
  • Type mismatch (e.g. text: 42 instead of a string).
  • Out-of-range value (e.g. limit: -1 on a list endpoint).
  • Bad pattern match (e.g. mediaId: "foo" — must match ^med_[0-9A-Za-z]{22}$).
  • Constraint violation (e.g. scheduledAt parses but is in the past).
  • Batch-level rules listed below.

Batch and target rules

These rules fire on POST /v1/posts after schema parsing but before preflight. They map directly to the validation layer in apps/api/src/routes/posts.ts.
rulemeaningremediation
targets.requiredtargets[] was emptySend at least one target.
targets.maxtargets[] exceeded 25 entriesSplit the publish into batches of at most 25 targets.
target.account.not_connectedA target’s platform matched zero connected accounts in the org+profile scopeConnect the platform first, or pass an explicit accountId.
target.account.ambiguousA target’s platform matched 2+ connected accounts; we won’t guessPass an explicit accountId (the remediation field lists the candidate ids).
targets.options.platform_mismatchA target’s options.platform doesn’t match the resolved account’s platformDrop options, or set options.platform to match.
targets.account.platform_mismatchA target carried both accountId and platform and they disagreedDrop platform, or set it to match the account.
mode_conflictpublishNow: true AND scheduledAt both setDrop one of the two — they’re mutually exclusive.
scheduledAt.futurescheduledAt was less than 1 second in the futureSend a timestamp at least 1 second ahead.
scheduledAt.text_onlyScheduled post carried media or firstComment (not supported in v1)Publish synchronously, or drop media/firstComment.
body.jsonRequest body wasn’t valid JSONSend a JSON body with Content-Type: application/json.

Response shape

validation_failed.json
{
  "error": {
    "code": "validation_failed",
    "message": "A single request may not fan out to more than 25 targets.",
    "rule": "targets.max",
    "remediation": "Split the publish into batches of at most 25 targets.",
    "docUrl": "https://docs.letmepost.dev/errors/validation_failed",
    "requestId": "req_..."
  }
}
For pure schema failures the rule is the dot-joined Zod path and platformResponse carries the full Zod issues array — every issue, not just the first — so a UI can render them inline next to the right field:
zod-issues.json
{
  "error": {
    "code": "validation_failed",
    "message": "Required",
    "rule": "targets",
    "platformResponse": [
      { "code": "invalid_type", "expected": "array", "received": "undefined", "path": ["targets"], "message": "Required" }
    ],
    "remediation": "Check the request body matches the documented schema.",
    "docUrl": "https://docs.letmepost.dev/errors/validation_failed",
    "requestId": "req_..."
  }
}

Reproducing it

reproduce.sh
# Missing 'targets' — should fail with rule: "targets"
curl -X POST https://api.letmepost.dev/v1/posts \
  -H "Authorization: Bearer $LMP_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "shipped" }'

Remediation

Read the rule field and either fix the field at that path, or follow the batch-rule table above. For pure Zod failures, rule is the path of the first issue; check platformResponse for the full list when you’re rendering form errors to a user.