message_partially_sent

Some of the channel messages a send produced reached the guest and some did not. `parts` says which; resend only the failed ones.

HTTP 422The request was understood but refused as sent. Change it before retrying.

When it fires

Airbnb carries one file per message and no text beside it, so a send with attachments goes out as several Airbnb messages: each file in order, then the text. If Airbnb refuses a later message after earlier ones were delivered, the send is neither a success nor a clean failure. The guest already has part of it.

parts lists every message the send attempted, in order. Each part says which files it carried (attachmentIndexes, indexes into your request's attachments), whether it carried the text (hasText), whether it arrived (sent) and, if not, why (error).

It comes from POST /v1/conversations/{id}/messages and from POST /v1/channels/airbnb/messaging/{threadId}/messages with mediaUrl, where the file can arrive and the text be refused. Booking.com sends everything as one message, so it never returns this code.

Response shape

Every Repull error follows the same envelope. The code is stable and safe to switch on.

{
  "error": {
    "code": "message_partially_sent",
    "message": "Airbnb rejected this image — try a different file or re-save it as JPEG",
    "fix": "Some parts reached the guest — see `parts` (`sent: true`). Do NOT resend the whole request; resend only the attachments whose part has `sent: false`, fixing what its `error` names.",
    "docs_url": "https://repull.dev/docs/errors/message_partially_sent",
    "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
    "channel": "airbnb",
    "parts": [
      { "attachmentIndexes": [0], "hasText": false, "sent": true, "messageId": "1847910", "externalMessageId": "32837172380", "error": null },
      { "attachmentIndexes": [1], "hasText": false, "sent": false, "messageId": "1847912", "externalMessageId": null, "error": "Airbnb rejected this image — try a different file or re-save it as JPEG" }
    ],
    "attachments": []
  }
}

How to fix

  1. Do not resend the whole request: the guest would receive the delivered parts twice.
  2. Find the part with `sent: false` and read its `error`. Fix what it names (for a refused image, re-save it as JPEG or pick another file).
  3. Work out what did not arrive: every file whose index is not in a `sent: true` part's `attachmentIndexes`, plus the text if no delivered part has `hasText: true`.
  4. Send a new request carrying only that. Use a new `Idempotency-Key`: it is a different request.

Common gotchas

  • The delivered parts are recorded in the conversation, so GET /v1/conversations/{id}/messages shows exactly what the guest has.
  • Sending stops at the first refusal. Files after the refused one, and the text, are never attempted and do not appear in parts at all, which is why you work out what to resend from what was delivered, not from the failed parts alone.

Examples

curl

# The first file arrived, the second did not: resend only the second, with the text
curl -X POST https://api.repull.dev/v1/conversations/164743/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Idempotency-Key: msg-164743-resend-1" \
  -H "Content-Type: application/json" \
  -d '{"message": "And here is the gate.", "attachments": [{"url": "https://cdn.example.com/gate.jpg"}]}'

TypeScript

const res = await fetch('https://api.repull.dev/v1/conversations/164743/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.REPULL_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': key,
  },
  body: JSON.stringify({ message, attachments }),
})

if (res.status === 422) {
  const { error } = await res.json()
  if (error.code === 'message_partially_sent') {
    const delivered = error.parts.filter((p: any) => p.sent)
    const deliveredIdx = new Set(delivered.flatMap((p: any) => p.attachmentIndexes))
    // Everything not delivered: the refused file, the files after it, and the text
    const retryFiles = attachments.filter((_: unknown, i: number) => !deliveredIdx.has(i))
    const retryText = delivered.some((p: any) => p.hasText) ? undefined : message
    const reason = error.parts.find((p: any) => !p.sent)?.error
    // Fix what `reason` names, then send only these, with a NEW Idempotency-Key
    return { retryFiles, retryText, reason }
  }
}

If you're an AI agent

Part of the send reached the guest. Never resend the whole request. Fix what the `sent: false` part's `error` names, then resend only the files not carried by a `sent: true` part (plus the text if no delivered part has hasText) with a new Idempotency-Key.

Hit an error that isn't covered? Email hello@repull.dev with the request id from the response headers.

AI