[{"data":1,"prerenderedAt":2261},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api":3,"surround:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api":2229,"ancestors:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api":2237,"children:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api":2242,"related:\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api":2243},{"id":4,"title":5,"body":6,"dateModified":2196,"datePublished":2196,"description":2197,"extension":2198,"faq":2199,"keyword":2208,"kind":2209,"meta":2210,"navigation":340,"path":2219,"seo":2220,"slug":2223,"stem":2224,"topics":2225,"type":2227,"__hash__":2228},"docs\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api\u002Findex.md","Send an Excel Report with the SendGrid API",{"type":7,"value":8,"toc":2182},"minimark",[9,24,29,60,81,85,96,255,259,270,604,618,622,632,675,691,820,824,839,1217,1228,1232,1247,1391,1402,1406,1416,1784,1799,1968,1972,1979,2047,2055,2059,2096,2100,2117,2121,2127,2133,2139,2145,2149,2178],[10,11,12,13,17,18,23],"p",{},"SMTP works for sending reports, but many organisations route automated email through a delivery service so they get bounce handling, suppression lists, sender authentication and a log of every message. Twilio SendGrid is one of the most common, and its Web API is often easier to use from a scheduled job than SMTP: a single HTTPS request with an API key, no ports to open, and a clear JSON error when something is wrong. This page sends an Excel report with the official ",[14,15,16],"code",{},"sendgrid"," Python library — HTML and text bodies, the workbook as an attachment, one personalised message per recipient, an optional dynamic template — and covers testing without sending, error handling and the size limits. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install sendgrid pandas openpyxl\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," sendgrid",[39,55,56],{"class":49}," pandas",[39,58,59],{"class":49}," openpyxl\n",[10,61,62,63,67,68,71,72,76,77,80],{},"You need a SendGrid account, a verified sender (a single sender address or an authenticated domain) and an API key with the ",[64,65,66],"em",{},"Mail Send"," permission. Store the key in the ",[14,69,70],{},"SENDGRID_API_KEY"," environment variable rather than in the script — see ",[19,73,75],{"href":74},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002F","Store Credentials for Excel Report Scripts Safely",". ",[14,78,79],{},"sales_report.xlsx"," is the report to send. The outputs on this page were produced against a local stand-in for the API, so no real email was sent; the requests are the same.",[25,82,84],{"id":83},"smtp-relay-or-web-api","SMTP relay or Web API?",[10,86,87,88,91,92,95],{},"SendGrid also offers an SMTP relay, so an existing ",[14,89,90],{},"smtplib"," script can switch to it by changing the host, port and credentials (the username is the literal string ",[14,93,94],{},"apikey"," and the password is the API key). That is the quickest migration. The Web API is the better long-term choice for a scheduled job: it runs over HTTPS on port 443, which corporate firewalls rarely block, it validates the whole message in one request and answers with a specific error, and it supports personalizations, templates and sandbox mode directly.",[97,98,106,107,106,111,106,115,106,123,106,133,106,144,106,151,106,157,106,163,106,171,106,175,106,182,106,186,106,193,106,197,106,202,106,206,106,213,106,218,106,222,106,226,106,231,106,234,106,239,106,242,106,247,106,250],"svg",{"viewBox":99,"role":100,"ariaLabelledBy":101,"xmlns":104,"style":105},"4 0 752 233","img",[102,103],"sg-routes-t","sg-routes-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[108,109,110],"title",{"id":102},"Two ways to send through SendGrid",[112,113,114],"desc",{"id":103},"The SMTP relay reuses an existing smtplib script by changing host and credentials, but needs an outbound SMTP port and gives less specific errors. The Web API uses HTTPS, returns JSON errors, and supports personalizations, dynamic templates and sandbox mode.",[116,117],"rect",{"x":118,"y":119,"width":120,"height":121,"fill":122},"4","0","752","233","#ffffff",[116,124],{"x":125,"y":126,"width":127,"height":128,"rx":129,"fill":130,"stroke":131,"style":132},"20.0","16.0","351.0","201.0","14","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[134,135,137],"g",{"transform":136},"translate(182.5,29.0) scale(1.083)",[138,139],"path",{"d":140,"fill":141,"stroke":142,"style":143},"M3 6h18v12H3z M3 7l9 6 9-6","none","var(--text,#172033)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[145,146,150],"text",{"x":147,"y":148,"style":149},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","SMTP relay",[41,152],{"x1":153,"y1":154,"x2":155,"y2":154,"stroke":131,"style":156},"36.0","91.0","355.0","stroke-width:1px",[145,158,162],{"x":159,"y":160,"style":161},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","reuse the smtplib script",[134,164,166],{"transform":165},"translate(38.5,99.5) scale(0.625)",[138,167],{"d":168,"fill":141,"stroke":169,"style":170},"M5 12.5l4.5 4.5L19 7","var(--teal,#0f9488)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[145,172,174],{"x":159,"y":173,"style":161},"137.0","user 'apikey', password = key",[134,176,178],{"transform":177},"translate(38.5,125.5) scale(0.625)",[138,179],{"d":180,"fill":141,"stroke":181,"style":170},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold,#b4740a)",[145,183,185],{"x":159,"y":184,"style":161},"163.0","needs port 587 outbound",[134,187,189],{"transform":188},"translate(38.5,151.5) scale(0.625)",[138,190],{"d":191,"fill":141,"stroke":192,"style":170},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)",[145,194,196],{"x":159,"y":195,"style":161},"189.0","terse SMTP errors",[134,198,200],{"transform":199},"translate(38.5,177.5) scale(0.625)",[138,201],{"d":191,"fill":141,"stroke":192,"style":170},[116,203],{"x":204,"y":126,"width":127,"height":128,"rx":129,"fill":205,"stroke":169,"style":132},"389.0","#d9f4f1",[134,207,209],{"transform":208},"translate(551.5,29.0) scale(1.083)",[138,210],{"d":211,"fill":141,"stroke":212,"style":143},"M8 7l-5 5 5 5 M16 7l5 5-5 5 M14 4l-4 16","var(--teal-ink,#0b6157)",[145,214,217],{"x":215,"y":148,"style":216},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Web API (this page)",[41,219],{"x1":220,"y1":154,"x2":221,"y2":154,"stroke":169,"style":156},"405.0","724.0",[145,223,225],{"x":224,"y":160,"style":161},"429.0","HTTPS on port 443",[134,227,229],{"transform":228},"translate(407.5,99.5) scale(0.625)",[138,230],{"d":168,"fill":141,"stroke":169,"style":170},[145,232,233],{"x":224,"y":173,"style":161},"JSON errors per field",[134,235,237],{"transform":236},"translate(407.5,125.5) scale(0.625)",[138,238],{"d":168,"fill":141,"stroke":169,"style":170},[145,240,241],{"x":224,"y":184,"style":161},"personalizations, templates",[134,243,245],{"transform":244},"translate(407.5,151.5) scale(0.625)",[138,246],{"d":168,"fill":141,"stroke":169,"style":170},[145,248,249],{"x":224,"y":195,"style":161},"sandbox mode for tests",[134,251,253],{"transform":252},"translate(407.5,177.5) scale(0.625)",[138,254],{"d":168,"fill":141,"stroke":169,"style":170},[25,256,258],{"id":257},"step-1-build-the-message-and-attach-the-workbook","Step 1 — Build the message and attach the workbook",[10,260,261,262,265,266,269],{},"A ",[14,263,264],{},"Mail"," object holds sender, recipients, subject and bodies. The workbook goes in as an ",[14,267,268],{},"Attachment"," whose content is base64-encoded text:",[30,271,275],{"className":272,"code":273,"language":274,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import base64\nimport os\nfrom pathlib import Path\nfrom sendgrid import SendGridAPIClient\nfrom sendgrid.helpers.mail import Attachment, Disposition, FileContent, FileName, FileType, Mail\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ndef excel_attachment(path):\n    data = base64.b64encode(Path(path).read_bytes()).decode(\"ascii\")\n    return Attachment(FileContent(data), FileName(Path(path).name), FileType(XLSX), Disposition(\"attachment\"))\n\nmessage = Mail(\n    from_email=(\"reports@example.com\", \"Reporting\"),\n    to_emails=\"finance@example.com\",\n    subject=\"Sales report — September 2026\",\n    plain_text_content=\"The September sales report is attached. Total revenue: 40,300.\",\n    html_content=\"\u003Cp>The September sales report is attached.\u003C\u002Fp>\u003Cp>\u003Cb>Total revenue: 40,300\u003C\u002Fb>\u003C\u002Fp>\",\n)\nmessage.attachment = excel_attachment(\"sales_report.xlsx\")\npayload = message.get()\nprint(payload[\"subject\"], \"|\", payload[\"attachments\"][0][\"filename\"], \"|\", payload[\"attachments\"][0][\"type\"][:40] + \"…\")\n","python",[14,276,277,287,295,309,322,335,342,355,360,373,391,411,416,427,451,465,478,491,504,509,525,536],{"__ignoreMap":35},[39,278,279,283],{"class":41,"line":42},[39,280,282],{"class":281},"s-kum","import",[39,284,286],{"class":285},"skGVy"," base64\n",[39,288,290,292],{"class":41,"line":289},2,[39,291,282],{"class":281},[39,293,294],{"class":285}," os\n",[39,296,298,301,304,306],{"class":41,"line":297},3,[39,299,300],{"class":281},"from",[39,302,303],{"class":285}," pathlib ",[39,305,282],{"class":281},[39,307,308],{"class":285}," Path\n",[39,310,312,314,317,319],{"class":41,"line":311},4,[39,313,300],{"class":281},[39,315,316],{"class":285}," sendgrid ",[39,318,282],{"class":281},[39,320,321],{"class":285}," SendGridAPIClient\n",[39,323,325,327,330,332],{"class":41,"line":324},5,[39,326,300],{"class":281},[39,328,329],{"class":285}," sendgrid.helpers.mail ",[39,331,282],{"class":281},[39,333,334],{"class":285}," Attachment, Disposition, FileContent, FileName, FileType, Mail\n",[39,336,338],{"class":41,"line":337},6,[39,339,341],{"emptyLinePlaceholder":340},true,"\n",[39,343,345,349,352],{"class":41,"line":344},7,[39,346,348],{"class":347},"sP0c6","XLSX",[39,350,351],{"class":281}," =",[39,353,354],{"class":49}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[39,356,358],{"class":41,"line":357},8,[39,359,341],{"emptyLinePlaceholder":340},[39,361,363,366,370],{"class":41,"line":362},9,[39,364,365],{"class":281},"def",[39,367,369],{"class":368},"s_Opv"," excel_attachment",[39,371,372],{"class":285},"(path):\n",[39,374,376,379,382,385,388],{"class":41,"line":375},10,[39,377,378],{"class":285},"    data ",[39,380,381],{"class":281},"=",[39,383,384],{"class":285}," base64.b64encode(Path(path).read_bytes()).decode(",[39,386,387],{"class":49},"\"ascii\"",[39,389,390],{"class":285},")\n",[39,392,394,397,400,402,405,408],{"class":41,"line":393},11,[39,395,396],{"class":281},"    return",[39,398,399],{"class":285}," Attachment(FileContent(data), FileName(Path(path).name), FileType(",[39,401,348],{"class":347},[39,403,404],{"class":285},"), Disposition(",[39,406,407],{"class":49},"\"attachment\"",[39,409,410],{"class":285},"))\n",[39,412,414],{"class":41,"line":413},12,[39,415,341],{"emptyLinePlaceholder":340},[39,417,419,422,424],{"class":41,"line":418},13,[39,420,421],{"class":285},"message ",[39,423,381],{"class":281},[39,425,426],{"class":285}," Mail(\n",[39,428,430,434,436,439,442,445,448],{"class":41,"line":429},14,[39,431,433],{"class":432},"sa561","    from_email",[39,435,381],{"class":281},[39,437,438],{"class":285},"(",[39,440,441],{"class":49},"\"reports@example.com\"",[39,443,444],{"class":285},", ",[39,446,447],{"class":49},"\"Reporting\"",[39,449,450],{"class":285},"),\n",[39,452,454,457,459,462],{"class":41,"line":453},15,[39,455,456],{"class":432},"    to_emails",[39,458,381],{"class":281},[39,460,461],{"class":49},"\"finance@example.com\"",[39,463,464],{"class":285},",\n",[39,466,468,471,473,476],{"class":41,"line":467},16,[39,469,470],{"class":432},"    subject",[39,472,381],{"class":281},[39,474,475],{"class":49},"\"Sales report — September 2026\"",[39,477,464],{"class":285},[39,479,481,484,486,489],{"class":41,"line":480},17,[39,482,483],{"class":432},"    plain_text_content",[39,485,381],{"class":281},[39,487,488],{"class":49},"\"The September sales report is attached. Total revenue: 40,300.\"",[39,490,464],{"class":285},[39,492,494,497,499,502],{"class":41,"line":493},18,[39,495,496],{"class":432},"    html_content",[39,498,381],{"class":281},[39,500,501],{"class":49},"\"\u003Cp>The September sales report is attached.\u003C\u002Fp>\u003Cp>\u003Cb>Total revenue: 40,300\u003C\u002Fb>\u003C\u002Fp>\"",[39,503,464],{"class":285},[39,505,507],{"class":41,"line":506},19,[39,508,390],{"class":285},[39,510,512,515,517,520,523],{"class":41,"line":511},20,[39,513,514],{"class":285},"message.attachment ",[39,516,381],{"class":281},[39,518,519],{"class":285}," excel_attachment(",[39,521,522],{"class":49},"\"sales_report.xlsx\"",[39,524,390],{"class":285},[39,526,528,531,533],{"class":41,"line":527},21,[39,529,530],{"class":285},"payload ",[39,532,381],{"class":281},[39,534,535],{"class":285}," message.get()\n",[39,537,539,542,545,548,551,554,557,560,563,565,567,570,572,574,576,578,580,582,584,587,590,593,596,599,602],{"class":41,"line":538},22,[39,540,541],{"class":347},"print",[39,543,544],{"class":285},"(payload[",[39,546,547],{"class":49},"\"subject\"",[39,549,550],{"class":285},"], ",[39,552,553],{"class":49},"\"|\"",[39,555,556],{"class":285},", payload[",[39,558,559],{"class":49},"\"attachments\"",[39,561,562],{"class":285},"][",[39,564,119],{"class":347},[39,566,562],{"class":285},[39,568,569],{"class":49},"\"filename\"",[39,571,550],{"class":285},[39,573,553],{"class":49},[39,575,556],{"class":285},[39,577,559],{"class":49},[39,579,562],{"class":285},[39,581,119],{"class":347},[39,583,562],{"class":285},[39,585,586],{"class":49},"\"type\"",[39,588,589],{"class":285},"][:",[39,591,592],{"class":347},"40",[39,594,595],{"class":285},"] ",[39,597,598],{"class":281},"+",[39,600,601],{"class":49}," \"…\"",[39,603,390],{"class":285},[10,605,606,609,610,613,614,617],{},[14,607,608],{},"from_email"," accepts a tuple of address and display name. Giving both a plain-text and an HTML body lets every mail client show something sensible. The MIME type tells the recipient's client to open the attachment with Excel; using ",[14,611,612],{},"application\u002Foctet-stream"," instead works, but some clients then show a generic file icon or warn about it. ",[14,615,616],{},"message.get()"," returns the JSON body the library will send — useful for checking the message before anything leaves.",[25,619,621],{"id":620},"step-2-send-it-and-read-the-response","Step 2 — Send it and read the response",[10,623,624,627,628,631],{},[14,625,626],{},"SendGridAPIClient.send()"," posts the message to the ",[14,629,630],{},"\u002Fv3\u002Fmail\u002Fsend"," endpoint:",[30,633,635],{"className":272,"code":634,"language":274,"meta":35,"style":35},"sg = SendGridAPIClient(os.environ[\"SENDGRID_API_KEY\"])\nresponse = sg.send(message)\nprint(response.status_code, response.headers.get(\"X-Message-Id\"))\n",[14,636,637,653,663],{"__ignoreMap":35},[39,638,639,642,644,647,650],{"class":41,"line":42},[39,640,641],{"class":285},"sg ",[39,643,381],{"class":281},[39,645,646],{"class":285}," SendGridAPIClient(os.environ[",[39,648,649],{"class":49},"\"SENDGRID_API_KEY\"",[39,651,652],{"class":285},"])\n",[39,654,655,658,660],{"class":41,"line":289},[39,656,657],{"class":285},"response ",[39,659,381],{"class":281},[39,661,662],{"class":285}," sg.send(message)\n",[39,664,665,667,670,673],{"class":41,"line":297},[39,666,541],{"class":347},[39,668,669],{"class":285},"(response.status_code, response.headers.get(",[39,671,672],{"class":49},"\"X-Message-Id\"",[39,674,410],{"class":285},[10,676,677,678,682,683,686,687,690],{},"A status of ",[679,680,681],"strong",{},"202 Accepted"," means SendGrid has queued the message for delivery — not that it has been delivered. The ",[14,684,685],{},"X-Message-Id"," header identifies the message in SendGrid's activity feed and event webhooks, so log it alongside the report run; if a recipient says they never received the report, that ID is how you find out whether it bounced, was blocked or went to spam. For EU accounts with data residency, call ",[14,688,689],{},"sg.set_sendgrid_data_residency(\"eu\")"," before sending so requests go to the EU endpoint.",[97,692,106,697,106,700,106,703,106,706,106,713,106,719,106,722,106,727,106,730,106,734,106,739,106,741,106,743,106,748,106,753,106,761,106,767,106,772,106,775,106,780,106,785,106,790,106,793,106,798,106,803,106,806,106,809,106,815],{"viewBox":693,"role":100,"ariaLabelledBy":694,"xmlns":104,"style":105},"4 0 752 292",[695,696],"sg-seq-t","sg-seq-d",[108,698,699],{"id":695},"What happens when the job sends a report",[112,701,702],{"id":696},"The report job posts the message with its API key to SendGrid's mail send endpoint and gets 202 Accepted with a message id. SendGrid then delivers the email and reports delivered, bounced or opened events that can be looked up by the message id.",[116,704],{"x":118,"y":119,"width":120,"height":705,"fill":122},"292",[116,707],{"x":125,"y":126,"width":708,"height":709,"rx":710,"fill":711,"stroke":712,"style":132},"180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)",[145,714,718],{"x":715,"y":716,"style":717},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Report job",[116,720],{"x":721,"y":126,"width":708,"height":709,"rx":710,"fill":130,"stroke":131,"style":132},"290.0",[145,723,726],{"x":724,"y":716,"style":725},"380.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","SendGrid API",[116,728],{"x":729,"y":126,"width":708,"height":709,"rx":710,"fill":130,"stroke":131,"style":132},"560.0",[145,731,733],{"x":732,"y":716,"style":725},"650.0","Recipient",[41,735],{"x1":715,"y1":736,"x2":715,"y2":737,"stroke":131,"style":738},"64.0","276.0","stroke-width:1.5px;stroke-dasharray:5 4",[41,740],{"x1":724,"y1":736,"x2":724,"y2":737,"stroke":131,"style":738},[41,742],{"x1":732,"y1":736,"x2":732,"y2":737,"stroke":131,"style":738},[41,744],{"x1":745,"y1":746,"x2":747,"y2":746,"stroke":712,"style":132},"115.0","104.0","371.0",[749,750],"polygon",{"points":751,"fill":752},"375.0,104.0 366.0,109.0 366.0,99.0","#5b5cf0",[116,754],{"x":755,"y":756,"width":757,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"170.6","75.0","148.8","21.0","10","stroke-width:1.2px",[145,762,766],{"x":763,"y":764,"style":765},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. POST \u002Fv3\u002Fmail\u002Fsend",[41,768],{"x1":769,"y1":770,"x2":771,"y2":770,"stroke":712,"style":132},"375.0","152.0","119.0",[749,773],{"points":774,"fill":752},"115.0,152.0 124.0,147.0 124.0,157.0",[116,776],{"x":777,"y":778,"width":779,"height":758,"rx":759,"fill":205,"stroke":169,"style":760},"169.7","123.0","150.6",[145,781,784],{"x":763,"y":782,"style":783},"137.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","2. 202 · X-Message-Id",[41,786],{"x1":787,"y1":788,"x2":789,"y2":788,"stroke":712,"style":132},"385.0","200.0","641.0",[749,791],{"points":792,"fill":752},"645.0,200.0 636.0,205.0 636.0,195.0",[116,794],{"x":795,"y":796,"width":797,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"446.6","171.0","136.8",[145,799,802],{"x":800,"y":801,"style":765},"515.0","185.5","3. delivers the email",[41,804],{"x1":769,"y1":805,"x2":771,"y2":805,"stroke":712,"style":132},"248.0",[749,807],{"points":808,"fill":752},"115.0,248.0 124.0,243.0 124.0,253.0",[116,810],{"x":811,"y":812,"width":813,"height":758,"rx":759,"fill":814,"stroke":181,"style":760},"169.5","219.0","151.1","#fdefd8",[145,816,819],{"x":763,"y":817,"style":818},"233.5","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","4. events via webhook",[25,821,823],{"id":822},"step-3-one-personalised-email-per-recipient","Step 3 — One personalised email per recipient",[10,825,826,827,830,831,834,835,838],{},"Sending to several people with one ",[14,828,829],{},"to_emails"," list puts everyone in the same ",[14,832,833],{},"To:"," header, so recipients see each other's addresses. ",[64,836,837],{},"Personalizations"," send a separate copy to each recipient in a single API call, each with its own substitutions:",[30,840,842],{"className":272,"code":841,"language":274,"meta":35,"style":35},"from sendgrid.helpers.mail import Personalization, Substitution, To\n\nmanagers = [\n    {\"email\": \"north@example.com\", \"name\": \"Ana\", \"region\": \"North\", \"revenue\": 18400},\n    {\"email\": \"south@example.com\", \"name\": \"Ben\", \"region\": \"South\", \"revenue\": 12100},\n]\n\nmessage = Mail(from_email=(\"reports@example.com\", \"Reporting\"), subject=\"Your region: September 2026\",\n               html_content=\"\u003Cp>Hi -name-,\u003C\u002Fp>\u003Cp>-region- revenue for September: \u003Cb>-revenue-\u003C\u002Fb>.\u003C\u002Fp>\")\nfor m in managers:\n    p = Personalization()\n    p.add_to(To(m[\"email\"], m[\"name\"]))\n    p.add_substitution(Substitution(\"-name-\", m[\"name\"]))\n    p.add_substitution(Substitution(\"-region-\", m[\"region\"]))\n    p.add_substitution(Substitution(\"-revenue-\", f\"{m['revenue']:,}\"))\n    message.add_personalization(p)\nmessage.attachment = excel_attachment(\"sales_report.xlsx\")\n\nbody = message.get()\nprint(len(body[\"personalizations\"]), \"copies:\", [p[\"to\"][0][\"email\"] for p in body[\"personalizations\"]])\nprint(body[\"personalizations\"][0][\"substitutions\"])\n",[14,843,844,855,859,869,916,956,961,965,999,1011,1025,1035,1050,1065,1078,1116,1121,1133,1137,1146,1198],{"__ignoreMap":35},[39,845,846,848,850,852],{"class":41,"line":42},[39,847,300],{"class":281},[39,849,329],{"class":285},[39,851,282],{"class":281},[39,853,854],{"class":285}," Personalization, Substitution, To\n",[39,856,857],{"class":41,"line":289},[39,858,341],{"emptyLinePlaceholder":340},[39,860,861,864,866],{"class":41,"line":297},[39,862,863],{"class":285},"managers ",[39,865,381],{"class":281},[39,867,868],{"class":285}," [\n",[39,870,871,874,877,880,883,885,888,890,893,895,898,900,903,905,908,910,913],{"class":41,"line":311},[39,872,873],{"class":285},"    {",[39,875,876],{"class":49},"\"email\"",[39,878,879],{"class":285},": ",[39,881,882],{"class":49},"\"north@example.com\"",[39,884,444],{"class":285},[39,886,887],{"class":49},"\"name\"",[39,889,879],{"class":285},[39,891,892],{"class":49},"\"Ana\"",[39,894,444],{"class":285},[39,896,897],{"class":49},"\"region\"",[39,899,879],{"class":285},[39,901,902],{"class":49},"\"North\"",[39,904,444],{"class":285},[39,906,907],{"class":49},"\"revenue\"",[39,909,879],{"class":285},[39,911,912],{"class":347},"18400",[39,914,915],{"class":285},"},\n",[39,917,918,920,922,924,927,929,931,933,936,938,940,942,945,947,949,951,954],{"class":41,"line":324},[39,919,873],{"class":285},[39,921,876],{"class":49},[39,923,879],{"class":285},[39,925,926],{"class":49},"\"south@example.com\"",[39,928,444],{"class":285},[39,930,887],{"class":49},[39,932,879],{"class":285},[39,934,935],{"class":49},"\"Ben\"",[39,937,444],{"class":285},[39,939,897],{"class":49},[39,941,879],{"class":285},[39,943,944],{"class":49},"\"South\"",[39,946,444],{"class":285},[39,948,907],{"class":49},[39,950,879],{"class":285},[39,952,953],{"class":347},"12100",[39,955,915],{"class":285},[39,957,958],{"class":41,"line":337},[39,959,960],{"class":285},"]\n",[39,962,963],{"class":41,"line":344},[39,964,341],{"emptyLinePlaceholder":340},[39,966,967,969,971,974,976,978,980,982,984,986,989,992,994,997],{"class":41,"line":357},[39,968,421],{"class":285},[39,970,381],{"class":281},[39,972,973],{"class":285}," Mail(",[39,975,608],{"class":432},[39,977,381],{"class":281},[39,979,438],{"class":285},[39,981,441],{"class":49},[39,983,444],{"class":285},[39,985,447],{"class":49},[39,987,988],{"class":285},"), ",[39,990,991],{"class":432},"subject",[39,993,381],{"class":281},[39,995,996],{"class":49},"\"Your region: September 2026\"",[39,998,464],{"class":285},[39,1000,1001,1004,1006,1009],{"class":41,"line":362},[39,1002,1003],{"class":432},"               html_content",[39,1005,381],{"class":281},[39,1007,1008],{"class":49},"\"\u003Cp>Hi -name-,\u003C\u002Fp>\u003Cp>-region- revenue for September: \u003Cb>-revenue-\u003C\u002Fb>.\u003C\u002Fp>\"",[39,1010,390],{"class":285},[39,1012,1013,1016,1019,1022],{"class":41,"line":375},[39,1014,1015],{"class":281},"for",[39,1017,1018],{"class":285}," m ",[39,1020,1021],{"class":281},"in",[39,1023,1024],{"class":285}," managers:\n",[39,1026,1027,1030,1032],{"class":41,"line":393},[39,1028,1029],{"class":285},"    p ",[39,1031,381],{"class":281},[39,1033,1034],{"class":285}," Personalization()\n",[39,1036,1037,1040,1042,1045,1047],{"class":41,"line":413},[39,1038,1039],{"class":285},"    p.add_to(To(m[",[39,1041,876],{"class":49},[39,1043,1044],{"class":285},"], m[",[39,1046,887],{"class":49},[39,1048,1049],{"class":285},"]))\n",[39,1051,1052,1055,1058,1061,1063],{"class":41,"line":418},[39,1053,1054],{"class":285},"    p.add_substitution(Substitution(",[39,1056,1057],{"class":49},"\"-name-\"",[39,1059,1060],{"class":285},", m[",[39,1062,887],{"class":49},[39,1064,1049],{"class":285},[39,1066,1067,1069,1072,1074,1076],{"class":41,"line":429},[39,1068,1054],{"class":285},[39,1070,1071],{"class":49},"\"-region-\"",[39,1073,1060],{"class":285},[39,1075,897],{"class":49},[39,1077,1049],{"class":285},[39,1079,1080,1082,1085,1087,1090,1093,1097,1100,1103,1106,1109,1112,1114],{"class":41,"line":453},[39,1081,1054],{"class":285},[39,1083,1084],{"class":49},"\"-revenue-\"",[39,1086,444],{"class":285},[39,1088,1089],{"class":281},"f",[39,1091,1092],{"class":49},"\"",[39,1094,1096],{"class":1095},"sSjpA","{",[39,1098,1099],{"class":285},"m[",[39,1101,1102],{"class":49},"'revenue'",[39,1104,1105],{"class":285},"]",[39,1107,1108],{"class":281},":,",[39,1110,1111],{"class":1095},"}",[39,1113,1092],{"class":49},[39,1115,410],{"class":285},[39,1117,1118],{"class":41,"line":467},[39,1119,1120],{"class":285},"    message.add_personalization(p)\n",[39,1122,1123,1125,1127,1129,1131],{"class":41,"line":480},[39,1124,514],{"class":285},[39,1126,381],{"class":281},[39,1128,519],{"class":285},[39,1130,522],{"class":49},[39,1132,390],{"class":285},[39,1134,1135],{"class":41,"line":493},[39,1136,341],{"emptyLinePlaceholder":340},[39,1138,1139,1142,1144],{"class":41,"line":506},[39,1140,1141],{"class":285},"body ",[39,1143,381],{"class":281},[39,1145,535],{"class":285},[39,1147,1148,1150,1152,1155,1158,1161,1164,1167,1170,1173,1175,1177,1179,1181,1183,1185,1188,1190,1193,1195],{"class":41,"line":511},[39,1149,541],{"class":347},[39,1151,438],{"class":285},[39,1153,1154],{"class":347},"len",[39,1156,1157],{"class":285},"(body[",[39,1159,1160],{"class":49},"\"personalizations\"",[39,1162,1163],{"class":285},"]), ",[39,1165,1166],{"class":49},"\"copies:\"",[39,1168,1169],{"class":285},", [p[",[39,1171,1172],{"class":49},"\"to\"",[39,1174,562],{"class":285},[39,1176,119],{"class":347},[39,1178,562],{"class":285},[39,1180,876],{"class":49},[39,1182,595],{"class":285},[39,1184,1015],{"class":281},[39,1186,1187],{"class":285}," p ",[39,1189,1021],{"class":281},[39,1191,1192],{"class":285}," body[",[39,1194,1160],{"class":49},[39,1196,1197],{"class":285},"]])\n",[39,1199,1200,1202,1204,1206,1208,1210,1212,1215],{"class":41,"line":527},[39,1201,541],{"class":347},[39,1203,1157],{"class":285},[39,1205,1160],{"class":49},[39,1207,562],{"class":285},[39,1209,119],{"class":347},[39,1211,562],{"class":285},[39,1213,1214],{"class":49},"\"substitutions\"",[39,1216,652],{"class":285},[10,1218,1219,1220,1223,1224,23],{},"Each personalization becomes its own email: Ana sees only her address and \"North revenue for September: 18,400\". Up to 1,000 personalizations fit in one request. (The library inserts each new personalization at the front of the list, which is why South is listed first; the order makes no difference to delivery.) The attachment is shared by all copies; if each manager should get ",[64,1221,1222],{},"their own"," workbook, send one message per manager instead, as in ",[19,1225,1227],{"href":1226},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report",[25,1229,1231],{"id":1230},"step-4-use-a-dynamic-template","Step 4 — Use a dynamic template",[10,1233,1234,1235,1238,1239,1242,1243,1246],{},"For a report email whose layout marketing or finance want to edit without touching code, create a ",[64,1236,1237],{},"dynamic template"," in the SendGrid dashboard using Handlebars placeholders such as ",[14,1240,1241],{},"{{region}}"," and ",[14,1244,1245],{},"{{revenue}}",", then send only the data:",[30,1248,1250],{"className":272,"code":1249,"language":274,"meta":35,"style":35},"message = Mail(from_email=(\"reports@example.com\", \"Reporting\"), to_emails=\"north@example.com\")\nmessage.template_id = \"d-0123456789abcdef0123456789abcdef\"\nmessage.dynamic_template_data = {\"name\": \"Ana\", \"region\": \"North\", \"revenue\": \"18,400\", \"period\": \"September 2026\"}\nmessage.attachment = excel_attachment(\"sales_report.xlsx\")\nbody = message.get()\nprint(body[\"template_id\"], \"|\", body[\"personalizations\"][0][\"dynamic_template_data\"][\"region\"])\n",[14,1251,1252,1282,1292,1338,1350,1358],{"__ignoreMap":35},[39,1253,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276,1278,1280],{"class":41,"line":42},[39,1255,421],{"class":285},[39,1257,381],{"class":281},[39,1259,973],{"class":285},[39,1261,608],{"class":432},[39,1263,381],{"class":281},[39,1265,438],{"class":285},[39,1267,441],{"class":49},[39,1269,444],{"class":285},[39,1271,447],{"class":49},[39,1273,988],{"class":285},[39,1275,829],{"class":432},[39,1277,381],{"class":281},[39,1279,882],{"class":49},[39,1281,390],{"class":285},[39,1283,1284,1287,1289],{"class":41,"line":289},[39,1285,1286],{"class":285},"message.template_id ",[39,1288,381],{"class":281},[39,1290,1291],{"class":49}," \"d-0123456789abcdef0123456789abcdef\"\n",[39,1293,1294,1297,1299,1302,1304,1306,1308,1310,1312,1314,1316,1318,1320,1322,1325,1327,1330,1332,1335],{"class":41,"line":297},[39,1295,1296],{"class":285},"message.dynamic_template_data ",[39,1298,381],{"class":281},[39,1300,1301],{"class":285}," {",[39,1303,887],{"class":49},[39,1305,879],{"class":285},[39,1307,892],{"class":49},[39,1309,444],{"class":285},[39,1311,897],{"class":49},[39,1313,879],{"class":285},[39,1315,902],{"class":49},[39,1317,444],{"class":285},[39,1319,907],{"class":49},[39,1321,879],{"class":285},[39,1323,1324],{"class":49},"\"18,400\"",[39,1326,444],{"class":285},[39,1328,1329],{"class":49},"\"period\"",[39,1331,879],{"class":285},[39,1333,1334],{"class":49},"\"September 2026\"",[39,1336,1337],{"class":285},"}\n",[39,1339,1340,1342,1344,1346,1348],{"class":41,"line":311},[39,1341,514],{"class":285},[39,1343,381],{"class":281},[39,1345,519],{"class":285},[39,1347,522],{"class":49},[39,1349,390],{"class":285},[39,1351,1352,1354,1356],{"class":41,"line":324},[39,1353,1141],{"class":285},[39,1355,381],{"class":281},[39,1357,535],{"class":285},[39,1359,1360,1362,1364,1367,1369,1371,1374,1376,1378,1380,1382,1385,1387,1389],{"class":41,"line":337},[39,1361,541],{"class":347},[39,1363,1157],{"class":285},[39,1365,1366],{"class":49},"\"template_id\"",[39,1368,550],{"class":285},[39,1370,553],{"class":49},[39,1372,1373],{"class":285},", body[",[39,1375,1160],{"class":49},[39,1377,562],{"class":285},[39,1379,119],{"class":347},[39,1381,562],{"class":285},[39,1383,1384],{"class":49},"\"dynamic_template_data\"",[39,1386,562],{"class":285},[39,1388,897],{"class":49},[39,1390,652],{"class":285},[10,1392,1393,1394,1397,1398,1401],{},"The template ID starts with ",[14,1395,1396],{},"d-"," and is copied from the dashboard. The subject can be part of the template too. Dynamic template data is per personalization, so the per-recipient pattern from Step 3 works the same way with ",[14,1399,1400],{},"p.dynamic_template_data = {...}",". Keep numbers formatted in Python — Handlebars in SendGrid templates has only basic helpers.",[25,1403,1405],{"id":1404},"step-5-test-without-sending-and-handle-errors","Step 5 — Test without sending, and handle errors",[10,1407,1408,1411,1412,1415],{},[64,1409,1410],{},"Sandbox mode"," makes SendGrid validate the request completely — authentication, addresses, attachment encoding — and return ",[679,1413,1414],{},"200"," without delivering anything. Turn it on in tests and in a dry-run flag:",[30,1417,1419],{"className":272,"code":1418,"language":274,"meta":35,"style":35},"from python_http_client.exceptions import HTTPError\nfrom sendgrid.helpers.mail import MailSettings, SandBoxMode\n\ndef send_report(message, dry_run=False, api_key=None):\n    if dry_run:\n        message.mail_settings = MailSettings(sandbox_mode=SandBoxMode(True))\n    try:\n        resp = SendGridAPIClient(api_key or os.environ[\"SENDGRID_API_KEY\"]).send(message)\n        return resp.status_code, resp.headers.get(\"X-Message-Id\")\n    except HTTPError as err:\n        details = [e[\"message\"] for e in err.to_dict.get(\"errors\", [])]\n        raise RuntimeError(f\"SendGrid rejected the report email ({err.status_code}): {details}\") from err\n\nmsg = Mail(from_email=\"reports@example.com\", to_emails=\"finance@example.com\",\n           subject=\"Dry run\", plain_text_content=\"Test\")\nprint(\"sandbox:\", send_report(msg, dry_run=True))\ntry:\n    send_report(Mail(from_email=\"reports@example.com\", to_emails=\"finance@example.com\",\n                     subject=\"x\", plain_text_content=\"x\"), api_key=\"SG.revoked\")\nexcept RuntimeError as e:\n    print(e)\n",[14,1420,1421,1433,1444,1448,1474,1482,1505,1513,1534,1546,1560,1591,1633,1637,1662,1684,1705,1712,1733,1763,1776],{"__ignoreMap":35},[39,1422,1423,1425,1428,1430],{"class":41,"line":42},[39,1424,300],{"class":281},[39,1426,1427],{"class":285}," python_http_client.exceptions ",[39,1429,282],{"class":281},[39,1431,1432],{"class":285}," HTTPError\n",[39,1434,1435,1437,1439,1441],{"class":41,"line":289},[39,1436,300],{"class":281},[39,1438,329],{"class":285},[39,1440,282],{"class":281},[39,1442,1443],{"class":285}," MailSettings, SandBoxMode\n",[39,1445,1446],{"class":41,"line":297},[39,1447,341],{"emptyLinePlaceholder":340},[39,1449,1450,1452,1455,1458,1460,1463,1466,1468,1471],{"class":41,"line":311},[39,1451,365],{"class":281},[39,1453,1454],{"class":368}," send_report",[39,1456,1457],{"class":285},"(message, dry_run",[39,1459,381],{"class":281},[39,1461,1462],{"class":347},"False",[39,1464,1465],{"class":285},", api_key",[39,1467,381],{"class":281},[39,1469,1470],{"class":347},"None",[39,1472,1473],{"class":285},"):\n",[39,1475,1476,1479],{"class":41,"line":324},[39,1477,1478],{"class":281},"    if",[39,1480,1481],{"class":285}," dry_run:\n",[39,1483,1484,1487,1489,1492,1495,1497,1500,1503],{"class":41,"line":337},[39,1485,1486],{"class":285},"        message.mail_settings ",[39,1488,381],{"class":281},[39,1490,1491],{"class":285}," MailSettings(",[39,1493,1494],{"class":432},"sandbox_mode",[39,1496,381],{"class":281},[39,1498,1499],{"class":285},"SandBoxMode(",[39,1501,1502],{"class":347},"True",[39,1504,410],{"class":285},[39,1506,1507,1510],{"class":41,"line":344},[39,1508,1509],{"class":281},"    try",[39,1511,1512],{"class":285},":\n",[39,1514,1515,1518,1520,1523,1526,1529,1531],{"class":41,"line":357},[39,1516,1517],{"class":285},"        resp ",[39,1519,381],{"class":281},[39,1521,1522],{"class":285}," SendGridAPIClient(api_key ",[39,1524,1525],{"class":281},"or",[39,1527,1528],{"class":285}," os.environ[",[39,1530,649],{"class":49},[39,1532,1533],{"class":285},"]).send(message)\n",[39,1535,1536,1539,1542,1544],{"class":41,"line":362},[39,1537,1538],{"class":281},"        return",[39,1540,1541],{"class":285}," resp.status_code, resp.headers.get(",[39,1543,672],{"class":49},[39,1545,390],{"class":285},[39,1547,1548,1551,1554,1557],{"class":41,"line":375},[39,1549,1550],{"class":281},"    except",[39,1552,1553],{"class":285}," HTTPError ",[39,1555,1556],{"class":281},"as",[39,1558,1559],{"class":285}," err:\n",[39,1561,1562,1565,1567,1570,1573,1575,1577,1580,1582,1585,1588],{"class":41,"line":393},[39,1563,1564],{"class":285},"        details ",[39,1566,381],{"class":281},[39,1568,1569],{"class":285}," [e[",[39,1571,1572],{"class":49},"\"message\"",[39,1574,595],{"class":285},[39,1576,1015],{"class":281},[39,1578,1579],{"class":285}," e ",[39,1581,1021],{"class":281},[39,1583,1584],{"class":285}," err.to_dict.get(",[39,1586,1587],{"class":49},"\"errors\"",[39,1589,1590],{"class":285},", [])]\n",[39,1592,1593,1596,1599,1601,1603,1606,1608,1611,1613,1616,1618,1621,1623,1625,1628,1630],{"class":41,"line":413},[39,1594,1595],{"class":281},"        raise",[39,1597,1598],{"class":347}," RuntimeError",[39,1600,438],{"class":285},[39,1602,1089],{"class":281},[39,1604,1605],{"class":49},"\"SendGrid rejected the report email (",[39,1607,1096],{"class":1095},[39,1609,1610],{"class":285},"err.status_code",[39,1612,1111],{"class":1095},[39,1614,1615],{"class":49},"): ",[39,1617,1096],{"class":1095},[39,1619,1620],{"class":285},"details",[39,1622,1111],{"class":1095},[39,1624,1092],{"class":49},[39,1626,1627],{"class":285},") ",[39,1629,300],{"class":281},[39,1631,1632],{"class":285}," err\n",[39,1634,1635],{"class":41,"line":418},[39,1636,341],{"emptyLinePlaceholder":340},[39,1638,1639,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660],{"class":41,"line":429},[39,1640,1641],{"class":285},"msg ",[39,1643,381],{"class":281},[39,1645,973],{"class":285},[39,1647,608],{"class":432},[39,1649,381],{"class":281},[39,1651,441],{"class":49},[39,1653,444],{"class":285},[39,1655,829],{"class":432},[39,1657,381],{"class":281},[39,1659,461],{"class":49},[39,1661,464],{"class":285},[39,1663,1664,1667,1669,1672,1674,1677,1679,1682],{"class":41,"line":453},[39,1665,1666],{"class":432},"           subject",[39,1668,381],{"class":281},[39,1670,1671],{"class":49},"\"Dry run\"",[39,1673,444],{"class":285},[39,1675,1676],{"class":432},"plain_text_content",[39,1678,381],{"class":281},[39,1680,1681],{"class":49},"\"Test\"",[39,1683,390],{"class":285},[39,1685,1686,1688,1690,1693,1696,1699,1701,1703],{"class":41,"line":467},[39,1687,541],{"class":347},[39,1689,438],{"class":285},[39,1691,1692],{"class":49},"\"sandbox:\"",[39,1694,1695],{"class":285},", send_report(msg, ",[39,1697,1698],{"class":432},"dry_run",[39,1700,381],{"class":281},[39,1702,1502],{"class":347},[39,1704,410],{"class":285},[39,1706,1707,1710],{"class":41,"line":480},[39,1708,1709],{"class":281},"try",[39,1711,1512],{"class":285},[39,1713,1714,1717,1719,1721,1723,1725,1727,1729,1731],{"class":41,"line":493},[39,1715,1716],{"class":285},"    send_report(Mail(",[39,1718,608],{"class":432},[39,1720,381],{"class":281},[39,1722,441],{"class":49},[39,1724,444],{"class":285},[39,1726,829],{"class":432},[39,1728,381],{"class":281},[39,1730,461],{"class":49},[39,1732,464],{"class":285},[39,1734,1735,1738,1740,1743,1745,1747,1749,1751,1753,1756,1758,1761],{"class":41,"line":506},[39,1736,1737],{"class":432},"                     subject",[39,1739,381],{"class":281},[39,1741,1742],{"class":49},"\"x\"",[39,1744,444],{"class":285},[39,1746,1676],{"class":432},[39,1748,381],{"class":281},[39,1750,1742],{"class":49},[39,1752,988],{"class":285},[39,1754,1755],{"class":432},"api_key",[39,1757,381],{"class":281},[39,1759,1760],{"class":49},"\"SG.revoked\"",[39,1762,390],{"class":285},[39,1764,1765,1768,1770,1773],{"class":41,"line":511},[39,1766,1767],{"class":281},"except",[39,1769,1598],{"class":347},[39,1771,1772],{"class":281}," as",[39,1774,1775],{"class":285}," e:\n",[39,1777,1778,1781],{"class":41,"line":527},[39,1779,1780],{"class":347},"    print",[39,1782,1783],{"class":285},"(e)\n",[10,1785,1786,1787,1790,1791,1794,1795,23],{},"The library raises ",[14,1788,1789],{},"HTTPError"," for any 4xx or 5xx response; ",[14,1792,1793],{},"err.to_dict"," holds SendGrid's JSON error list, which says exactly what is wrong — a revoked key, an unverified sender, a malformed address. Re-raising with that message makes the failure readable in the job log. Retry only on 429 (rate limited) and 5xx responses; a 400 or 401 will fail the same way every time. The general retry pattern is in ",[19,1796,1798],{"href":1797},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","Retry a Failed Excel Report Job in Python",[97,1800,106,1805,106,1808,106,1811,106,1814,106,1819,106,1827,106,1831,106,1835,106,1838,106,1841,106,1844,106,1849,106,1854,106,1859,106,1864,106,1869,106,1874,106,1877,106,1880,106,1884,106,1886,106,1889,106,1895,106,1899,106,1902,106,1905,106,1908,106,1912,106,1915,106,1920,106,1926,106,1931,106,1935,106,1939,106,1941,106,1944,106,1948,106,1951,106,1954,106,1961,106,1965],{"viewBox":1801,"role":100,"ariaLabelledBy":1802,"xmlns":104,"style":105},"4 0 752 290",[1803,1804],"sg-status-t","sg-status-d",[108,1806,1807],{"id":1803},"What to do with the response",[112,1809,1810],{"id":1804},"202 means queued for delivery, so log the message id. 200 comes back in sandbox mode with nothing sent. 400 or 401 point to a problem in the request or key and should fail the job without retrying. 429 and 5xx responses are temporary and worth retrying with backoff.",[116,1812],{"x":118,"y":119,"width":120,"height":1813,"fill":122},"290",[116,1815],{"x":1816,"y":126,"width":1817,"height":1818,"rx":710,"fill":711,"stroke":712,"style":132},"240.0","280.0","75.5",[134,1820,1822],{"transform":1821},"translate(368.0,29.0) scale(1.000)",[138,1823],{"d":1824,"fill":141,"stroke":1825,"style":1826},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[145,1828,1830],{"x":724,"y":1818,"style":1829},"font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Which status came back?",[41,1832],{"x1":724,"y1":1833,"x2":724,"y2":1834,"stroke":712,"style":132},"91.5","111.5",[41,1836],{"x1":746,"y1":1834,"x2":1837,"y2":1834,"stroke":712,"style":132},"656.0",[41,1839],{"x1":746,"y1":1834,"x2":746,"y2":1840,"stroke":712,"style":132},"157.5",[749,1842],{"points":1843,"fill":752},"104.0,161.5 99.0,152.5 109.0,152.5",[116,1845],{"x":1846,"y":1847,"width":1848,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"83.3","121.5","41.4",[145,1850,1853],{"x":746,"y":1851,"style":1852},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","202",[116,1855],{"x":125,"y":1856,"width":1857,"height":1858,"rx":710,"fill":205,"stroke":169,"style":132},"163.5","168.0","110.5",[134,1860,1862],{"transform":1861},"translate(92.0,185.2) scale(1.000)",[138,1863],{"d":168,"fill":141,"stroke":212,"style":1826},[145,1865,1868],{"x":746,"y":1866,"style":1867},"231.8","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","queued for delivery",[145,1870,1873],{"x":746,"y":1871,"style":1872},"251.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","log X-Message-Id",[41,1875],{"x1":1876,"y1":1834,"x2":1876,"y2":1840,"stroke":712,"style":132},"288.0",[749,1878],{"points":1879,"fill":752},"288.0,161.5 283.0,152.5 293.0,152.5",[116,1881],{"x":1882,"y":1847,"width":1883,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"267.1","41.9",[145,1885,1414],{"x":1876,"y":1851,"style":1852},[116,1887],{"x":1888,"y":1856,"width":1857,"height":1858,"rx":710,"fill":130,"stroke":131,"style":132},"204.0",[134,1890,1892],{"transform":1891},"translate(276.0,185.2) scale(1.000)",[138,1893],{"d":1894,"fill":141,"stroke":142,"style":1826},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z",[145,1896,1898],{"x":1876,"y":1866,"style":1897},"font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","sandbox only",[145,1900,1901],{"x":1876,"y":1871,"style":1872},"nothing was sent",[41,1903],{"x1":1904,"y1":1834,"x2":1904,"y2":1840,"stroke":712,"style":132},"472.0",[749,1906],{"points":1907,"fill":752},"472.0,161.5 467.0,152.5 477.0,152.5",[116,1909],{"x":1910,"y":1847,"width":1911,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"417.6","108.9",[145,1913,1914],{"x":1904,"y":1851,"style":1852},"400 \u002F 401 \u002F 403",[116,1916],{"x":1917,"y":1856,"width":1857,"height":1858,"rx":710,"fill":1918,"stroke":1919,"style":132},"388.0","#fce9e9","var(--danger,#dc2626)",[134,1921,1923],{"transform":1922},"translate(460.0,176.5) scale(1.000)",[138,1924],{"d":191,"fill":141,"stroke":1925,"style":1826},"var(--accent-ink,#be185d)",[145,1927,1930],{"x":1904,"y":1928,"style":1929},"223.0","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","fix the request or",[145,1932,1934],{"x":1904,"y":1933,"style":1929},"240.5","key",[145,1936,1938],{"x":1904,"y":1937,"style":1872},"260.0","do not retry",[41,1940],{"x1":1837,"y1":1834,"x2":1837,"y2":1840,"stroke":712,"style":132},[749,1942],{"points":1943,"fill":752},"656.0,161.5 651.0,152.5 661.0,152.5",[116,1945],{"x":1946,"y":1847,"width":1947,"height":758,"rx":759,"fill":122,"stroke":131,"style":760},"619.2","73.5",[145,1949,1950],{"x":1837,"y":1851,"style":1852},"429 \u002F 5xx",[116,1952],{"x":1953,"y":1856,"width":1857,"height":1858,"rx":710,"fill":814,"stroke":181,"style":132},"572.0",[134,1955,1957],{"transform":1956},"translate(644.0,185.2) scale(1.000)",[138,1958],{"d":1959,"fill":141,"stroke":1960,"style":1826},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--gold-ink,#7a4e06)",[145,1962,1964],{"x":1837,"y":1866,"style":1963},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","temporary",[145,1966,1967],{"x":1837,"y":1871,"style":1872},"retry with backoff",[25,1969,1971],{"id":1970},"size-limits","Size limits",[10,1973,1974,1975,1978],{},"SendGrid accepts messages up to ",[679,1976,1977],{},"30 MB in total",", including all attachments after base64 encoding, which inflates files by about a third — so a workbook much over 20 MB will not fit, and many receiving servers reject far smaller messages. Check the size before building the message:",[30,1980,1982],{"className":272,"code":1981,"language":274,"meta":35,"style":35},"size_mb = Path(\"sales_report.xlsx\").stat().st_size * 4 \u002F 3 \u002F 1024 \u002F 1024\nprint(f\"encoded attachment: {size_mb:.3f} MB of 30 MB\")\n",[14,1983,1984,2021],{"__ignoreMap":35},[39,1985,1986,1989,1991,1994,1996,1999,2002,2005,2008,2011,2013,2016,2018],{"class":41,"line":42},[39,1987,1988],{"class":285},"size_mb ",[39,1990,381],{"class":281},[39,1992,1993],{"class":285}," Path(",[39,1995,522],{"class":49},[39,1997,1998],{"class":285},").stat().st_size ",[39,2000,2001],{"class":281},"*",[39,2003,2004],{"class":347}," 4",[39,2006,2007],{"class":281}," \u002F",[39,2009,2010],{"class":347}," 3",[39,2012,2007],{"class":281},[39,2014,2015],{"class":347}," 1024",[39,2017,2007],{"class":281},[39,2019,2020],{"class":347}," 1024\n",[39,2022,2023,2025,2027,2029,2032,2034,2037,2040,2042,2045],{"class":41,"line":289},[39,2024,541],{"class":347},[39,2026,438],{"class":285},[39,2028,1089],{"class":281},[39,2030,2031],{"class":49},"\"encoded attachment: ",[39,2033,1096],{"class":1095},[39,2035,2036],{"class":285},"size_mb",[39,2038,2039],{"class":281},":.3f",[39,2041,1111],{"class":1095},[39,2043,2044],{"class":49}," MB of 30 MB\"",[39,2046,390],{"class":285},[10,2048,2049,2050,2054],{},"For large reports, zip the workbook — see ",[19,2051,2053],{"href":2052},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fzip-large-excel-attachments-before-emailing\u002F","Zip Large Excel Attachments Before Emailing"," — or upload it to cloud storage and email a link.",[25,2056,2058],{"id":2057},"common-pitfalls","Common pitfalls",[2060,2061,2062,2069,2078,2084,2090],"ul",{},[2063,2064,2065,2068],"li",{},[679,2066,2067],{},"Treating 202 as delivered."," It means queued; check events or the activity feed for delivery.",[2063,2070,2071,2077],{},[679,2072,2073,2074,2076],{},"All recipients in one ",[14,2075,829],{}," list."," Everyone sees everyone; use personalizations.",[2063,2079,2080,2083],{},[679,2081,2082],{},"Unverified sender."," The API returns 403 until the from address or domain is verified.",[2063,2085,2086,2089],{},[679,2087,2088],{},"API key in the script."," Read it from the environment or a secret store.",[2063,2091,2092,2095],{},[679,2093,2094],{},"Retrying 4xx errors."," They will not succeed on retry; fix the request.",[25,2097,2099],{"id":2098},"conclusion","Conclusion",[10,2101,2102,2103,2105,2106,2108,2109,2112,2113,2116],{},"The ",[14,2104,16],{}," library turns sending an Excel report into building a ",[14,2107,264],{},", attaching the workbook as base64 with the xlsx MIME type, and calling ",[14,2110,2111],{},"send()",". Use personalizations for per-recipient copies, a dynamic template when others own the email layout, sandbox mode for dry runs and tests, and ",[14,2114,2115],{},"HTTPError.to_dict"," for readable failures. Log the message ID from every 202 response, and keep attachments well under the 30 MB limit.",[25,2118,2120],{"id":2119},"frequently-asked-questions","Frequently asked questions",[10,2122,2123,2126],{},[679,2124,2125],{},"How do I attach an Excel file with the SendGrid Python library?","\nBase64-encode the file bytes, then create an Attachment with FileContent, FileName, FileType set to the xlsx MIME type and Disposition attachment, and assign it to message.attachment.",[10,2128,2129,2132],{},[679,2130,2131],{},"What does a 202 response from SendGrid mean?","\nThe message was accepted and queued for delivery. It does not confirm delivery; use the X-Message-Id header to look the message up in the activity feed or event webhooks.",[10,2134,2135,2138],{},[679,2136,2137],{},"How do I test SendGrid emails without sending them?","\nEnable sandbox mode in the mail settings. SendGrid validates the whole request and returns 200 without delivering the email.",[10,2140,2141,2144],{},[679,2142,2143],{},"How do I send the same report to several people privately?","\nAdd one Personalization per recipient to the Mail. Each becomes a separate email with only that recipient in the To header.",[25,2146,2148],{"id":2147},"related","Related",[2060,2150,2151,2157,2164,2171],{},[2063,2152,2153,2154,2156],{},"Up one level: ",[19,2155,22],{"href":21}," — sending reports by email.",[2063,2158,2159,2163],{},[19,2160,2162],{"href":2161},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses\u002F","Send an Excel Report with Amazon SES"," — another delivery service.",[2063,2165,2166,2170],{},[19,2167,2169],{"href":2168},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api\u002F","Send an Excel Report with the Microsoft Graph API"," — sending from a Microsoft 365 mailbox.",[2063,2172,2173,2177],{},[19,2174,2176],{"href":2175},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email\u002F","Embed a Chart Image in the Body of a Report Email"," — a chart inside the message.",[2179,2180,2181],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":289,"depth":289,"links":2183},[2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195],{"id":27,"depth":289,"text":28},{"id":83,"depth":289,"text":84},{"id":257,"depth":289,"text":258},{"id":620,"depth":289,"text":621},{"id":822,"depth":289,"text":823},{"id":1230,"depth":289,"text":1231},{"id":1404,"depth":289,"text":1405},{"id":1970,"depth":289,"text":1971},{"id":2057,"depth":289,"text":2058},{"id":2098,"depth":289,"text":2099},{"id":2119,"depth":289,"text":2120},{"id":2147,"depth":289,"text":2148},"2026-10-02","Email an Excel report through Twilio SendGrid's Web API instead of SMTP: build a Mail with HTML and text bodies, attach the workbook as base64 with the right MIME type, send one personalised email per recipient, use a dynamic template, test safely with sandbox mode, handle API errors and size limits, and check the request before it leaves.","md",[2200,2202,2204,2206],{"q":2125,"a":2201},"Base64-encode the file bytes, then create an Attachment with FileContent, FileName, FileType set to the xlsx MIME type and Disposition attachment, and assign it to message.attachment.",{"q":2131,"a":2203},"The message was accepted and queued for delivery. It does not confirm delivery; use the X-Message-Id header to look the message up in the activity feed or event webhooks.",{"q":2137,"a":2205},"Enable sandbox mode in the mail settings. SendGrid validates the whole request and returns 200 without delivering the email.",{"q":2143,"a":2207},"Add one Personalization per recipient to the Mail. Each becomes a separate email with only that recipient in the To header.","send an excel report with the sendgrid api",null,{"breadcrumb":2211},[2212,2215,2218],{"name":2213,"item":2214},"Home","\u002F",{"name":2216,"item":2217},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api",{"title":2221,"description":2222},"Send an Excel Report by Email with the SendGrid API in Python","Send Excel reports with the sendgrid Python library: Mail, base64 Attachment with the xlsx MIME type, personalizations, dynamic templates, sandbox mode, HTTPError handling and the 30 MB message limit.","send-an-excel-report-with-the-sendgrid-api","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-the-sendgrid-api\u002Findex",[2226],"email-delivery","how-to","Y68xnftQ3Tfw6P5svBqFdqOs49JRJ1_PPdDoeQoUYbo",[2230,2233],{"title":2169,"path":2231,"stem":2232,"children":-1},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api\u002Findex",{"title":2234,"path":2235,"stem":2236,"children":-1},"Send Each Recipient Their Own Excel Report with Python","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002Findex",[2238,2240],{"path":2239,"title":22},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib",{"path":2241,"title":2216},"\u002Fautomating-reporting-workflows",[],[2244,2247,2250,2253,2256,2259],{"path":2245,"title":2246,"kind":2227},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-credit-control-dunning-letter-run","Build a Credit Control Dunning Letter Run",{"path":2248,"title":2249,"kind":2227},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python","Attach Multiple Excel Files to One Email in Python",{"path":2251,"title":2252,"kind":2227},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fdownload-excel-attachments-from-an-email-inbox","Download Excel Attachments from an Email Inbox with Python",{"path":2254,"title":2255,"kind":2227},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-a-download-link-instead-of-an-excel-attachment","Email a Download Link Instead of an Excel Attachment",{"path":2257,"title":2258,"kind":2227},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Femail-an-excel-report-with-an-html-summary-body","Email an Excel Report with an HTML Summary Body",{"path":2260,"title":2176,"kind":2227},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email",1790972129877]