Payment Failed Dunning
ivy.agent.payment-failed-dunning · v1.0.0
ivyx
Takes a failed-payment event, looks the customer up in the CRM for the account owner, mails the customer a reminder and flags the account through your API.
Steps
In the order the agent's file lists them, each with where its inputs come from and the values the file fixes. A branch or a loop is a step too.
- 01Ivy NodeFind the account ownerivy.node.http-request
Sends one HTTP request to a URL and returns the status, headers and body, parsed as JSON when the response is JSON.
- takes
urlfrom the agent's input,crm_urlparams.emailfrom the agent's input,payload.customer_email- set
methodGET- secret
tokenthe stored secret crm-api-key- returns
owner
- 02Ivy NodeWrite the reminderivy.node.template-render
Fills {{ name }} placeholders in a text template from a mapping, and reports which names were missing.
- takes
valuesfrom the agent's input,payload- set
templateHello {{ customer_name }}, Our attempt {{ attempt }} to charge {{ amount }} {{ currency }} for your account did not g...on_missingempty- returns
reminder
- 03Ivy NodeMail the customerivy.node.email-send
Sends a plain-text or HTML email through an SMTP server in one step and returns the recipients it went to.
- takes
bodyfrom step 2,texttofrom the agent's input,payload.customer_emailfrom_addrfrom the agent's input,usernameusernamefrom the agent's inputsmtp_hostfrom the agent's inputsmtp_portfrom the agent's inputuse_tlsfrom the agent's input,smtp_tls- set
smtp_hostsmtp.gmail.comsmtp_port587use_tlstruesubjectYour payment did not go through- secret
passwordthe stored secret mail-password- returns
sent
- 04Ivy NodeSay whyivy.node.template-render
Fills {{ name }} placeholders in a text template from a mapping, and reports which names were missing.
- takes
valuesfrom the agent's input,payload- set
templatePayment failed on attempt {{ attempt }} ({{ amount }} {{ currency }})on_missingempty
- 05Ivy NodeFlag the accountivy.node.http-request
Sends one HTTP request to a URL and returns the status, headers and body, parsed as JSON when the response is JSON.
- takes
urlfrom the agent's input,accounts_urljson.idfrom the agent's input,payload.account_idjson.ownerfrom step 1,json.owner_emailjson.reasonfrom step 4,text- set
methodPATCH- secret
tokenthe stored secret saas-api-key- returns
flagged
Nodes it brings
Adding this agent in IVYX Studio adds these nodes with it. A node your workspace already has is kept as it is, even at another version.
What it touches
Collected from what each of its nodes declares, plus the model call when a step is a model turn. A declaration is the author's statement, and it is what policy rules select on.
What it needs
- A stored secret named
crm-api-key. The agent's file carries the name, never the value. - A stored secret named
mail-password. The agent's file carries the name, never the value. - A stored secret named
saas-api-key. The agent's file carries the name, never the value.
Inputs
| Field | Type | Description |
|---|---|---|
| payloadrequired | object | The event your service posted: customer_email, customer_name, account_id, amount, currency and attempt of the failed charge. |
| crm_urlrequired | string | The CRM's contacts endpoint; a GET with ?email= returns the contact with its owner_email. The stand-in's address is used in the cases. |
| accounts_urlrequired | string | Your API's accounts endpoint; a PATCH with id, owner and reason flags the account. The stand-in's address is used in the cases. |
| usernamerequired | string | The sending address, which is the login name; its password is the stored secret mail-password. |
| smtp_host | string | The SMTP server; smtp.gmail.com when left out. |
| smtp_port | integer | The SMTP port; 587 when left out. |
| smtp_tls | boolean | Upgrade SMTP with STARTTLS; on when left out. |
Outputs
| Field | Type | Description |
|---|---|---|
| owner | string | The account owner's address, from the CRM. |
| sent | boolean | True once the server accepted the reminder. |
| flagged | boolean | True once your API accepted the flag. |
| reminder | string | The reminder's text. |
Tests
2 of 2 test cases passed on Oct 7, 2026, in the publisher's own environment, before this version was published. The registry keeps that record; it does not run the cases again.
Requires: python:3.9, the CRM's API key stored as the secret crm-api-key, your own API's key stored as the secret saas-api-key, an SMTP server (Gmail unless smtp_host says otherwise) with the sending account's password stored as the secret mail-password, for the cases, GreenMail on 127.0.0.1:3025 (mkdata-mail.py), for the cases, the stand-in API on 127.0.0.1:18910 (api-standin.py), which the secrets crm-1, gh-1, saas-1 and the stand-in's /slack/hook address satisfy
- second-attempt
The customer is mailed, the owner is read from the CRM and the account is flagged.
- given
smtp_host127.0.0.1smtp_port3025smtp_tlsfalsepayload{"customer_email":"ada@acme.io","customer_name":"Ada Lovelace","account_id":"A-77","amount":490,"currency":"USD","att...crm_urlhttp://127.0.0.1:18910/crm/contactsaccounts_urlhttp://127.0.0.1:18910/accountsusernameagent@ivyx.test- expects
ownerequals owner@acme.iosentequals trueflaggedequals trueremindermatches attempt 2 to charge 490 USD
- another-customer
Another customer's owner comes from that customer's domain.
- given
smtp_host127.0.0.1smtp_port3025smtp_tlsfalsepayload{"customer_email":"ops@globex.example","customer_name":"Globex Ops","account_id":"A-78","amount":1200,"currency":"EUR...crm_urlhttp://127.0.0.1:18910/crm/contactsaccounts_urlhttp://127.0.0.1:18910/accountsusernameagent@ivyx.test- expects
ownerequals owner@globex.examplesentequals trueflaggedequals trueremindermatches 1200 EUR