# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: fintech.failed-transaction-support
title: Failed transaction support
sector: fintech
source:
  kind: synthesized
  notes: >-
    No fintech has built this with us yet. Written from the per-decision message pattern of the
    loan application follow-up (a trigger filter on one property picks the message) and the
    KYC nudge safety rules. Each failure reason gets the right reassurance; suspected fraud gets
    no detail by message, only a call-us line and a task for the fraud team.
derived_from: lending.loan-application-follow-up
vocabulary:
  person: customer
  people: customers
  group: team
summary: >-
  When a transaction fails, the customer gets a short message that fits the reason: try again,
  check the app, or how to raise a limit. A suspected-fraud failure sends only a call-us line
  and gives your fraud team a task due within the hour. Two failures in one day give your
  support team a task. No message shows an amount or an account number.
when_to_use:
  - You run a wallet, bank app or payments business, and failed transactions flood support with "where is my money?".
  - Your app or payment processor records each failure with a reason.
  - A fraud or risk team must hear about suspected fraud quickly, and customers shouldn't get details in a message.

requires:
  data_in:
    - how: app_events
      what: "Your app sends one event per failed transaction, with a reason and the channel it happened on."
    - how: webhook
      what: "No app changes? If your payment processor can post failure notices to a webhook source, map each to the same event name and properties."
  channels:
    - channel: sms
      for: Every message to customers
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events:
    - "{{ params.failed_event }}"
  traits:
    - fraud_case
    - repeat_failure_call

parameters:
  - key: org_name
    ask: true
    question: What should messages call your company?
    label: Company name
    type: string
    example: Northwind Pay
  - key: support_phone
    ask: true
    question: Which number should customers call for help? (the number printed on your card or shown in the app)
    label: Support phone
    type: string
    default: "{{ workspace.phone }}"
  - key: app_url
    ask: true
    question: Where should messages send people to try again? (your app or a deep link)
    label: App link
    type: url
    example: https://app.yourcompany.example
  - key: fraud_team
    ask: true
    question: Which team handles suspected fraud?
    label: Fraud team
    type: team_name
    default: Fraud team
  - key: support_team
    ask: true
    question: Which team helps customers whose transactions keep failing?
    label: Support team
    type: team_name
    default: Customer support
  - key: failed_event
    label: Failed transaction event
    type: event_name
    default: Transaction Failed
  - key: reversal_hours
    label: Hours a failed debit takes to be reversed
    type: hours
    default: 24
  - key: limit_steps
    label: Where in the app customers raise their limit
    type: string
    default: Profile, then Limits
  - key: fraud_due_hours
    label: Hours the fraud team has to call
    type: hours
    default: 1
  - key: fraud_escalation_team
    label: Who hears if the fraud call isn't made in time
    type: team_name
    default: Fraud leads
  - key: repeat_due_hours
    label: Hours support has to follow up repeated failures
    type: hours
    default: 4
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there

timings:
  - "A failure for the network or any other reason: an SMS straight away (at most one every 6 hours per customer)."
  - "Insufficient funds and limit failures: a matching SMS straight away (at most one every 6 hours)."
  - "Suspected fraud: a call-us SMS and a task for {{ params.fraud_team }}, due in {{ params.fraud_due_hours }} hours and escalated to {{ params.fraud_escalation_team }} at the due time."
  - "Two or more failures in the same day: a task for {{ params.support_team }}, due in {{ params.repeat_due_hours }} hours."

customer_must:
  - "Send {{ params.failed_event }} for every failed transaction with a `reason` property that is one of insufficient_funds, network, limit, suspected_fraud or other, and a `channel` property (card, transfer, wallet and so on). A failure with no reason sends nothing. Never put an amount or account number in the event's properties that your team doesn't need."
  - "Check the reversal wording. The network message says a failed debit is returned within {{ params.reversal_hours }} hours: change that hour count to match your rails."
  - "For suspected fraud, send no detail. The SMS only asks the customer to call the number on their card or in your app. Your fraud team must verify the customer with your own procedure, and never ask for a PIN, one-time code, password or card number."
  - Add people to {{ params.fraud_team }}, {{ params.fraud_escalation_team }} and {{ params.support_team }}. A task assigned to an empty team reaches nobody, and a fraud task must never go unseen.
  - Tell customers on your site that you never ask for a PIN, one-time code, password or card number by message or call.
  - Connect an SMS provider or finish business verification, switch the follow-ups on and test each reason with your own phone.

objects:
  - ref: team_fraud
    kind: team
    about: Calls customers about suspected-fraud failures.
    body: { name: "{{ params.fraud_team }}" }
  - ref: team_fraud_leads
    kind: team
    about: Hears about fraud calls not made in time.
    body: { name: "{{ params.fraud_escalation_team }}" }
  - ref: team_support
    kind: team
    about: Helps customers whose transactions keep failing.
    body: { name: "{{ params.support_team }}" }

  - ref: seg_repeat_failures
    kind: segment
    about: Two or more failed transactions in the last day.
    body:
      name: Repeated failed transactions
      description: "{{ params.failed_event }} at least twice in the last day."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: event, event_name: "{{ params.failed_event }}", cond_operator: count_gte, value: 2, time_window: 1d }

  - ref: tmpl_try_again
    kind: template
    about: Network or other failure. Reassures and says when a debit is reversed.
    body:
      name: Transaction failed, try again (SMS)
      channel: sms
      category: Support
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: your transaction didn't go through, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. Please try again in a few minutes. If any money left your account, it is returned within {{ params.reversal_hours }} hours. Need help? Call {{ params.support_phone }}. We will never ask for your PIN or OTP. Reply STOP to opt out."
  - ref: tmpl_funds
    kind: template
    about: Insufficient funds. Points to the app, never states a balance.
    body:
      name: Transaction failed, check your account (SMS)
      channel: sms
      category: Support
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: your transaction didn't go through, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. Please check your account in the app and try again: {{ params.app_url }} Need help? Call {{ params.support_phone }}. We will never ask for your PIN or OTP. Reply STOP to opt out."
  - ref: tmpl_limit
    kind: template
    about: Limit failure. How to raise the limit.
    body:
      name: Transaction over a limit (SMS)
      channel: sms
      category: Support
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: your transaction went over a limit on your account, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. You can raise your limit in the app ({{ params.limit_steps }}): {{ params.app_url }} Need help? Call {{ params.support_phone }}. We will never ask for your PIN or OTP. Reply STOP to opt out."
  - ref: tmpl_fraud
    kind: template
    about: Suspected fraud. No detail, only a call-us line.
    body:
      name: Please call us (SMS)
      channel: sms
      category: Support
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: we paused a recent transaction to keep your account safe. Please call us on the number in the app or on your card, or {{ params.support_phone }}. We will never ask for your PIN, OTP or password. Reply STOP to opt out."

  - ref: wf_try_again
    kind: workflow
    about: Network, other or any unlisted reason; one try-again message at most every 6 hours.
    body:
      name: "Failed transaction: try again"
      description: A reassuring SMS for failures that are not funds, limit or fraud.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: "{{ params.failed_event }}"
        property_filters:
          - { property: reason, cond_operator: neq, value: insufficient_funds }
          - { property: reason, cond_operator: neq, value: limit }
          - { property: reason, cond_operator: neq, value: suspected_fraud }
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_try_again.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 6
      goal: {}
      exit_conditions: []
  - ref: wf_funds
    kind: workflow
    about: Insufficient funds; a neutral message that points to the app.
    body:
      name: "Failed transaction: check your account"
      description: An SMS for failures caused by insufficient funds, with no balance in it.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: "{{ params.failed_event }}"
        property_filters: [{ property: reason, cond_operator: eq, value: insufficient_funds }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_funds.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 6
      goal: {}
      exit_conditions: []
  - ref: wf_limit
    kind: workflow
    about: Limit failure; how to raise the limit.
    body:
      name: "Failed transaction: over a limit"
      description: An SMS for failures caused by a limit, with where to raise it.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: "{{ params.failed_event }}"
        property_filters: [{ property: reason, cond_operator: eq, value: limit }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_limit.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 6
      goal: {}
      exit_conditions: []
  - ref: wf_fraud
    kind: workflow
    about: Suspected fraud; a call-us SMS and a fraud-team task due within the hour, escalated if missed.
    body:
      name: "Failed transaction: suspected fraud"
      description: A call-us SMS with no detail, and a call task for the fraud team.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: "{{ params.failed_event }}"
        property_filters: [{ property: reason, cond_operator: eq, value: suspected_fraud }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_fraud.id }}" } } } }
        - id: s2
          type: human_task
          config:
            human_task:
              title: "Suspected fraud: call {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                A transaction for {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) was paused as suspected fraud. Channel: {{ properties.channel | default: "unknown" }}.

                Call now. Check who you are speaking to with your own verification procedure. Never ask for a PIN, one-time code, password or card number, and never ask the customer to read out a code. Record whether they made the transaction, and take any action your fraud procedure requires. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_fraud.id }}" }]
              strategy: all
              completion_policy: first_response_wins
              outcome_options:
                - { id: customer_confirmed, label: Customer made it, intent: positive }
                - { id: not_customer, label: "Not the customer, account secured", intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.fraud_due_hours }}"
              wait: true
              wait_timeout_hours: 72
              notify: immediate
              escalation_ladder:
                - { after_hours: "{{ params.fraud_due_hours }}", to: [{ type: team, id: "{{ ref.team_fraud_leads.id }}" }] }
        - { id: s3, type: action, config: { action_type: set_trait, traits: { fraud_case: "{{ properties.task_outcome | default: \"unknown\" }}" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_repeat
    kind: workflow
    about: Two failures in a day; a support task to help them.
    body:
      name: "Failed transactions: repeated"
      description: When a customer has two failed transactions in a day, support is asked to follow up.
      enabled: false
      trigger_type: segment_enter
      trigger_config: { segment_id: "{{ ref.seg_repeat_failures.id }}" }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "Repeated failures: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) has had two failed transactions today.

                Check the reasons on their profile. If it looks like fraud, hand it to the fraud team. Otherwise call to help them complete what they were doing. Never ask for a PIN, one-time code, password or card number. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_support.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: helped, label: Helped them through, intent: positive }
                - { id: handed_to_fraud, label: Handed to the fraud team, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.repeat_due_hours }}"
              wait: true
              wait_timeout_hours: 72
              notify: immediate
        - { id: s2, type: action, config: { action_type: set_trait, traits: { repeat_failure_call: "{{ properties.task_outcome | default: \"unknown\" }}", repeat_failure_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 24
      goal: {}
      exit_conditions: []

eval:
  name: 106_fintech_failed_transaction_support
  mode: task
  vertical: ""
  prompt: >-
    Our app sends a Transaction Failed event with a reason (insufficient_funds, network, limit,
    suspected_fraud or other). For network or other failures, text the customer that they should
    try again and that any debit is reversed within 24 hours. For limit failures, text how to
    raise their limit. For suspected fraud, don't put any detail in a message: just text them to
    call us and give our fraud team a task due within an hour. If someone has two failed
    transactions in a day, give support a task. Never show amounts or account numbers and never
    ask for PINs or OTPs.
  expect_artifacts: [segment, workflow, message_template]
  forbid_terms: []
