<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cendyandreoli</title>
    <description>The latest articles on DEV Community by cendyandreoli (@cendyandreoli).</description>
    <link>https://dev.to/cendyandreoli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F267229%2F46284243-4553-471a-bfc7-2e42f97244f2.jpeg</url>
      <title>DEV Community: cendyandreoli</title>
      <link>https://dev.to/cendyandreoli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cendyandreoli"/>
    <language>en</language>
    <item>
      <title>Quanto custa vender a prazo para o varejo? Uma conta aberta</title>
      <dc:creator>cendyandreoli</dc:creator>
      <pubDate>Mon, 09 Nov 2026 20:43:58 +0000</pubDate>
      <link>https://dev.to/cendyandreoli/quanto-custa-vender-a-prazo-para-o-varejo-uma-conta-aberta-10c4</link>
      <guid>https://dev.to/cendyandreoli/quanto-custa-vender-a-prazo-para-o-varejo-uma-conta-aberta-10c4</guid>
      <description>&lt;p&gt;Quem vende a prazo para o varejo costuma olhar só para o atraso. Mas o prazo em si já custa, mesmo quando o lojista paga em dia. Segue uma conta com números de demonstração (não é resultado de cliente).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cenário:&lt;/strong&gt; venda de R$ 100.000, prazo de 60 dias, custo do dinheiro de 2% ao mês, 10% das vendas pagas com atraso médio de 15 dias, perda definitiva de 1%.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parte&lt;/th&gt;
&lt;th&gt;Conta&lt;/th&gt;
&lt;th&gt;Valor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prazo&lt;/td&gt;
&lt;td&gt;R$ 100.000 x (1,02² − 1)&lt;/td&gt;
&lt;td&gt;R$ 4.040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atraso&lt;/td&gt;
&lt;td&gt;R$ 100.000 x 10% x (1,02^0,5 − 1)&lt;/td&gt;
&lt;td&gt;R$ 99,50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perda&lt;/td&gt;
&lt;td&gt;R$ 100.000 x 1%&lt;/td&gt;
&lt;td&gt;R$ 1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;soma&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R$ 5.139,50 (5,14% da venda)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  O que se aprende com a conta
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;O maior pedaço é o prazo, não a inadimplência. Reduzir calote não zera o custo de vender a prazo.&lt;/li&gt;
&lt;li&gt;Uma garantia atua sobre a linha da perda. O custo do prazo continua. A pergunta útil é se o custo da garantia fica abaixo da perda que ela evita.&lt;/li&gt;
&lt;li&gt;Como régua de mercado, o Banco Central publica a inadimplência da carteira de crédito de pessoas jurídicas: 4,98% em setembro de 2026 (série SGS 21082). Ela mede crédito bancário a empresas, não venda a prazo entre indústria e varejo; serve só de referência.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Uma forma de garantia que existe hoje
&lt;/h2&gt;

&lt;p&gt;Recebíveis de cartão do lojista, já registrados em registradora, podem servir de lastro para a venda feita a ele. O registro é regulado pelo Banco Central (Resolução BCB nº 264, de 25/11/2022). Não é antecipação: o recebível continua sendo do lojista, que é o comprador.&lt;/p&gt;

&lt;p&gt;Quer refazer a conta com os seus números? A &lt;a href="https://arcuspay.com.br/calculadora-venda-a-prazo" rel="noopener noreferrer"&gt;calculadora é aberta&lt;/a&gt;.&lt;br&gt;
Explicação completa, com comparação entre garantia, duplicata, seguro e factoring: &lt;a href="https://arcuspay.com.br/garantia-venda-a-prazo" rel="noopener noreferrer"&gt;garantia para venda a prazo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Texto da equipe do ArcusPay. O ArcusPay não é banco e trabalha com parceiros regulados pelo Banco Central.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Issue Brazilian invoices from Claude with an MCP server (and a human approval step)</title>
      <dc:creator>cendyandreoli</dc:creator>
      <pubDate>Mon, 09 Nov 2026 20:19:34 +0000</pubDate>
      <link>https://dev.to/cendyandreoli/issue-brazilian-invoices-from-claude-with-an-mcp-server-and-a-human-approval-step-edi</link>
      <guid>https://dev.to/cendyandreoli/issue-brazilian-invoices-from-claude-with-an-mcp-server-and-a-human-approval-step-edi</guid>
      <description>&lt;p&gt;Brazilian service invoices (NFS-e) are issued per city or through a national system, and every field is checked by tax authorities. SendNF exposes this as an API and as an MCP server, so an AI agent can look up a company by CNPJ, preview an invoice and issue it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you need&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An API key. Keys that start with &lt;code&gt;nt_test_&lt;/code&gt; are sandbox keys (no tax value). &lt;code&gt;nt_live_&lt;/code&gt; keys issue real invoices.&lt;/li&gt;
&lt;li&gt;An MCP client such as Claude or Cursor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sendnf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.sendnf.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer nt_test_YOUR_KEY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server uses Streamable HTTP, with no session. Without a key it answers 401.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the agent can do (44 tools)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read: &lt;code&gt;lookup_cnpj&lt;/code&gt; (public data from the tax authority, and which city system applies), &lt;code&gt;search_service_codes&lt;/code&gt; (national service list, LC 116, with NBS), &lt;code&gt;check_readiness&lt;/code&gt; (what is still missing for a company), &lt;code&gt;validate_dps&lt;/code&gt; (checks a DPS XML against the official layout; it does not guarantee acceptance by the city).&lt;/li&gt;
&lt;li&gt;Preview without sending: &lt;code&gt;preview_nfse&lt;/code&gt;, &lt;code&gt;preview_nfce&lt;/code&gt;, &lt;code&gt;preview_nfe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Issue: &lt;code&gt;issue_nfse&lt;/code&gt;, &lt;code&gt;issue_nfce&lt;/code&gt;, &lt;code&gt;issue_nfe&lt;/code&gt;, and &lt;code&gt;cancel_document&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The human approval step&lt;/strong&gt;&lt;br&gt;
By default, any key used through MCP requires approval. &lt;code&gt;create_note_draft&lt;/code&gt; builds the invoice, validates it and leaves it as &lt;code&gt;pending_approval&lt;/code&gt; (NFS-e drafts come with a PDF preview). Nothing goes to the tax authority until a person approves in the dashboard or through the approver link. Approval expires in 7 days, and the audit log records who approved, when and from which IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits&lt;/strong&gt;&lt;br&gt;
The repository &lt;a href="https://github.com/sendnf/sendnf-mcp" rel="noopener noreferrer"&gt;https://github.com/sendnf/sendnf-mcp&lt;/a&gt; has the manifest, examples and tool list; the product code is private. SendNF issues NFS-e (MEI, Simples Nacional, Lucro Presumido and Lucro Real), NFC-e and NF-e (model 55; MEI and Simples Nacional); it does not issue CT-e or MDF-e. Docs: &lt;a href="https://sendnf.com/mcp/" rel="noopener noreferrer"&gt;https://sendnf.com/mcp/&lt;/a&gt; . Plans and prices: &lt;a href="https://sendnf.com/precos/" rel="noopener noreferrer"&gt;https://sendnf.com/precos/&lt;/a&gt; .&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>claude</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Reading Brazil's credit card revolving rate from the central bank API (no key needed)</title>
      <dc:creator>cendyandreoli</dc:creator>
      <pubDate>Mon, 09 Nov 2026 20:17:57 +0000</pubDate>
      <link>https://dev.to/cendyandreoli/reading-brazils-credit-card-revolving-rate-from-the-central-bank-api-no-key-needed-27cd</link>
      <guid>https://dev.to/cendyandreoli/reading-brazils-credit-card-revolving-rate-from-the-central-bank-api-no-key-needed-27cd</guid>
      <description>&lt;p&gt;Brazil's central bank (Banco Central do Brasil) publishes the average revolving credit card rate for individuals as series 25477 in its SGS time-series service. The API is public, needs no key and returns JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.bcb.gov.br/dados/serie/bcdata.sgs.25477/dados?formato=json&amp;amp;dataInicial=01/09/2025&amp;amp;dataFinal=30/09/2026"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row looks like &lt;code&gt;{"data":"01/09/2026","valor":"15.29"}&lt;/code&gt;. The value is the monthly rate in percent, as a string, and the date is always day 01 of the month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;September 2026: 15.29% per month.&lt;/li&gt;
&lt;li&gt;September 2025: 15.23% per month.&lt;/li&gt;
&lt;li&gt;The monthly rate stayed between 14.90% (Jan 2026) and 15.44% (May 2025) over the last 24 months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Turning monthly into yearly
&lt;/h2&gt;

&lt;p&gt;The API gives monthly only. The yearly rate is &lt;code&gt;(1 + m)^12 - 1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1529&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 451.44
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R$ 1,000 left untouched for 12 months at that rate becomes about R$ 5,514 (&lt;code&gt;1000 * 1.1529 ** 12&lt;/code&gt;). In Brazil, Law 14.690/2023 caps interest and fees at 100% of the original debt, so the real ceiling is double.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;valor&lt;/code&gt; is a string with a dot, not a comma.&lt;/li&gt;
&lt;li&gt;Date params use &lt;code&gt;dd/mm/aaaa&lt;/code&gt;, not ISO.&lt;/li&gt;
&lt;li&gt;The series is the average reported by institutions, not the rate on your card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I put the full 24-month table and the method on a page (in Portuguese), with sentences ready to cite: &lt;a href="https://tobbofin.com/calculadoras/juros-do-cartao-banco-central/" rel="noopener noreferrer"&gt;https://tobbofin.com/calculadoras/juros-do-cartao-banco-central/&lt;/a&gt;. I work on Tobbo, a personal finance app for Android that connects to Brazilian banks through Open Finance. The page is ours.&lt;/p&gt;

&lt;p&gt;Source: Banco Central do Brasil, SGS series 25477, checked on 2026-11-09.&lt;/p&gt;

</description>
      <category>api</category>
      <category>data</category>
      <category>fintech</category>
    </item>
    <item>
      <title>VERP and Automated Bounce Handling: DSN Parsing Explained</title>
      <dc:creator>cendyandreoli</dc:creator>
      <pubDate>Mon, 09 Nov 2026 13:08:07 +0000</pubDate>
      <link>https://dev.to/cendyandreoli/verp-and-automated-bounce-handling-dsn-parsing-explained-48m</link>
      <guid>https://dev.to/cendyandreoli/verp-and-automated-bounce-handling-dsn-parsing-explained-48m</guid>
      <description>&lt;p&gt;VERP (Variable Envelope Return Path) is a bounce-handling technique in which every message is sent with a unique envelope sender, such as &lt;code&gt;bounce-7f3k9q@bounces.example.com&lt;/code&gt;, that encodes which recipient or message it belongs to. When a delivery failure comes back, the address it was sent to tells you exactly which recipient failed, even if the bounce body is malformed. A complete automated pipeline combines VERP for identification, RFC 3464 Delivery Status Notification (DSN) parsing for the reason, and filters that separate real bounces from auto-replies and other noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bounce handling is harder than it looks
&lt;/h2&gt;

&lt;p&gt;Synchronous failures are easy: your MTA receives &lt;code&gt;550 5.1.1 User unknown&lt;/code&gt; during the SMTP session and records it. The hard part is asynchronous bounces. A server accepts the message, then later finds it cannot deliver it and sends a new message back to the envelope sender.&lt;/p&gt;

&lt;p&gt;Those messages are inconsistent. Many follow RFC 3464, but some servers send free-text bounces, some rewrite the recipient after forwarding, and some report an internal alias rather than the address you sent to. If you rely on parsing the human-readable text to find the recipient, you will mismatch or miss bounces. VERP removes that guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  How VERP works
&lt;/h2&gt;

&lt;p&gt;Without VERP, all mail uses one return-path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAIL FROM:&amp;lt;bounces@example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With VERP, the local part becomes unique per recipient or per message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAIL FROM:&amp;lt;b-01HZX4Q7M2-jane=example.net@bounces.example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic VERP format, as introduced for qmail, embeds the recipient address with &lt;code&gt;@&lt;/code&gt; replaced by &lt;code&gt;=&lt;/code&gt;. Modern systems usually prefer an opaque token instead, which does not leak the recipient address and keeps the local part short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAIL FROM:&amp;lt;b-7f3k9q2xv1@bounces.example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token maps (in a database or through an HMAC-signed encoding) to the message ID, the recipient and the stream. When a bounce arrives at &lt;code&gt;b-7f3k9q2xv1@bounces.example.com&lt;/code&gt;, you look up the token and know exactly what failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS for the bounce domain
&lt;/h3&gt;

&lt;p&gt;The return-path domain must receive mail and authorize your sending IPs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bounces.example.com.  IN MX   10 inbound.example.com.
bounces.example.com.  IN TXT  "v=spf1 ip4:203.0.113.0/28 -all"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it is a subdomain of &lt;code&gt;example.com&lt;/code&gt;, SPF aligns with a &lt;code&gt;From: ...@example.com&lt;/code&gt; address under DMARC relaxed alignment (RFC 7489). VERP and DMARC work together without conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token design tips
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keep the local part under 64 octets (the RFC 5321 limit).&lt;/li&gt;
&lt;li&gt;Use only lowercase letters and digits; some systems lowercase or mangle case and punctuation.&lt;/li&gt;
&lt;li&gt;Sign tokens or make them unguessable, so attackers cannot forge bounces that unsubscribe your users.&lt;/li&gt;
&lt;li&gt;Keep the mapping for as long as bounces can arrive (days, not minutes).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anatomy of a DSN
&lt;/h2&gt;

&lt;p&gt;A standards-compliant bounce is a &lt;code&gt;multipart/report&lt;/code&gt; message (RFC 6522) with &lt;code&gt;report-type=delivery-status&lt;/code&gt;. It is sent with an empty envelope sender (&lt;code&gt;MAIL FROM:&amp;lt;&amp;gt;&lt;/code&gt;) so that bounces never generate more bounces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return-Path: &amp;lt;&amp;gt;
To: b-7f3k9q2xv1@bounces.example.com
Subject: Undelivered Mail Returned to Sender
Content-Type: multipart/report; report-type=delivery-status; boundary="XYZ"

--XYZ
Content-Type: text/plain

The following address failed: jane@example.net

--XYZ
Content-Type: message/delivery-status

Reporting-MTA: dns; mx1.example.net
Arrival-Date: Mon, 12 Oct 2026 14:03:20 +0000

Final-Recipient: rfc822; jane@example.net
Original-Recipient: rfc822; jane@example.net
Action: failed
Status: 5.1.1
Remote-MTA: dns; mailstore3.example.net
Diagnostic-Code: smtp; 550 5.1.1 &amp;lt;jane@example.net&amp;gt;: Recipient address rejected: User unknown

--XYZ
Content-Type: message/rfc822-headers

Message-ID: &amp;lt;01HZX4Q7M2@mail.example.com&amp;gt;
From: Example &amp;lt;hello@example.com&amp;gt;
--XYZ--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields defined by RFC 3464 that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;delayed&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;relayed&lt;/code&gt; or &lt;code&gt;expanded&lt;/code&gt;. Only &lt;code&gt;failed&lt;/code&gt; is a bounce. &lt;code&gt;delayed&lt;/code&gt; is a warning that the remote server is still retrying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status&lt;/strong&gt;: the enhanced status code (RFC 3463), for example &lt;code&gt;5.1.1&lt;/code&gt; or &lt;code&gt;4.2.2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic-Code&lt;/strong&gt;: the remote server's actual reply, useful for classification and debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final-Recipient&lt;/strong&gt; and &lt;strong&gt;Original-Recipient&lt;/strong&gt;: which address failed. With VERP you use these to cross-check, not as the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third part, either &lt;code&gt;message/rfc822&lt;/code&gt; or &lt;code&gt;message/rfc822-headers&lt;/code&gt;, contains the original message or its headers. Your own &lt;code&gt;Message-ID&lt;/code&gt; there is a second way to correlate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounces vs auto-replies vs everything else
&lt;/h2&gt;

&lt;p&gt;The return-path address receives more than bounces. Classify every inbound message before acting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;How to recognize it&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DSN failure&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;multipart/report; report-type=delivery-status&lt;/code&gt;, &lt;code&gt;Action: failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Classify by Status, maybe suppress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSN delay&lt;/td&gt;
&lt;td&gt;Same, &lt;code&gt;Action: delayed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Record, do not suppress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-reply&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Auto-Submitted: auto-replied&lt;/code&gt; (RFC 3834), &lt;code&gt;X-Autoreply&lt;/code&gt;, Precedence &lt;code&gt;auto_reply&lt;/code&gt;, normal content type&lt;/td&gt;
&lt;td&gt;Ignore for suppression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Challenge-response&lt;/td&gt;
&lt;td&gt;Asks you to verify you are human&lt;/td&gt;
&lt;td&gt;Ignore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-standard bounce&lt;/td&gt;
&lt;td&gt;Plain text with phrases like "user unknown", from &lt;code&gt;MAILER-DAEMON&lt;/code&gt; or &lt;code&gt;postmaster&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Heuristic parsing, conservative actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complaint (ARF)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;multipart/report; report-type=feedback-report&lt;/code&gt; (RFC 5965)&lt;/td&gt;
&lt;td&gt;Route to complaint handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human reply&lt;/td&gt;
&lt;td&gt;Anything else&lt;/td&gt;
&lt;td&gt;Forward to a mailbox or drop by policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Never suppress an address because of an out-of-office reply. The mailbox exists, and the person will be back.&lt;/p&gt;

&lt;h2&gt;
  
  
  A processing pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inbound SMTP (bounces.example.com)
  -&amp;gt; accept; store raw message
  -&amp;gt; resolve VERP token -&amp;gt; (message_id, recipient, stream)
  -&amp;gt; classify: dsn | arf | auto-reply | unknown
  -&amp;gt; dsn: parse per-recipient fields; for each Action=failed
         -&amp;gt; classify Status: address | transient | policy
         -&amp;gt; address: add to suppression list (global)
         -&amp;gt; transient: count toward soft-bounce threshold
         -&amp;gt; policy: record + alert, no suppression
  -&amp;gt; emit event (bounced / delayed / complained) to webhooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementation notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store the raw message&lt;/strong&gt; before parsing, so you can reprocess when you improve the parser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make processing idempotent&lt;/strong&gt;: the same bounce can arrive twice, and retries happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle multiple recipients in one DSN&lt;/strong&gt;; each recipient has its own section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back gracefully&lt;/strong&gt; when the DSN is not standard: if the VERP token resolves but the body cannot be parsed, record an "unknown failure" rather than guessing a hard bounce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject unknown tokens&lt;/strong&gt; with a &lt;code&gt;550&lt;/code&gt; or accept and discard. Do not let your bounce domain become an open relay or backscatter target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One static return-path&lt;/strong&gt; and regex parsing of bounce bodies to find the recipient.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treating &lt;code&gt;Action: delayed&lt;/code&gt; as a bounce.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suppressing on auto-replies&lt;/strong&gt; or challenge-response messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return-path on a domain that does not align&lt;/strong&gt; with the From domain, which breaks SPF alignment for DMARC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgeable tokens&lt;/strong&gt; that let anyone suppress arbitrary recipients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short token retention&lt;/strong&gt;, so late bounces cannot be matched.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Unique, signed VERP token per message or recipient&lt;/li&gt;
&lt;li&gt;[ ] Bounce subdomain with MX and SPF, aligned with From&lt;/li&gt;
&lt;li&gt;[ ] RFC 3464 parser handling multiple recipients and all Action values&lt;/li&gt;
&lt;li&gt;[ ] Separate paths for DSNs, ARF reports and auto-replies&lt;/li&gt;
&lt;li&gt;[ ] Raw message storage and idempotent processing&lt;/li&gt;
&lt;li&gt;[ ] Address, transient and policy failures handled differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pidgely uses VERP on a custom return-path subdomain for every message, parses DSNs and ARF reports on its own inbound servers, and turns them into bounce and complaint events with automatic suppression.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://pidgely.com/blog/verp-bounce-handling/" rel="noopener noreferrer"&gt;pidgely.com&lt;/a&gt;. Pidgely is a transactional and marketing email API with built-in bounce handling: &lt;a href="https://pidgely.com" rel="noopener noreferrer"&gt;pidgely.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deliverability</category>
      <category>email</category>
      <category>smtp</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
