Short answer: you double-encoded the file for the API. Use the original contentBytes for both the email attachment and the API, or convert to binary when posting raw.
What’s happening
items('Apply_to_each')?['contentBytes'] is already Base64.
Your Compose uses base64( … ), which Base64-encodes that string again. Result = Base64 of a Base64 string (wrong for most APIs).
The Outlook “Send an email (V2)” action is tolerant. It accepts Base64 and handles MIME wrapping, so your attachment arrives fine. Your API just receives the double-encoded string and flags it invalid.
Fix
Pick one of these patterns and keep it consistent.
A) Send Base64 inside JSON
No conversions. Use the original field.
// remove the Compose
invoice64 : @{ items('Apply_to_each')?['contentBytes'] }
If you must keep the Compose, set it to:
@{ items('Apply_to_each')?['contentBytes'] } // not base64(...)
B) Send raw binary body (no JSON)
Some APIs want the file bytes as the request body.
Body: @{ base64ToBinary(items('Apply_to_each')?['contentBytes']) }
Headers:
Content-Type: application/pdf // or the actual MIME type
C) Normalize line breaks (rarely needed)
If an API rejects CR/LF inserted by upstream systems:
@{ replace(replace(items('Apply_to_each')?['contentBytes'], '\r',''), '\n','') }
For the email attachment
Use the same original contentBytes:
Attachments Content → @{ items('Apply_to_each')?['contentBytes'] }
Attachments Name → your filename
Summary
Do not call base64() on contentBytes.
Use contentBytes as Base64 for JSON, or base64ToBinary(contentBytes) for raw uploads.
Outlook’s action does MIME/Base64 wrapping; your API won’t.