LoopFour

Invoice Automation

Automatically create and send invoices when CRM deals close

Invoice Automation Tutorial

Learn how to automatically create invoices in Stripe when Salesforce opportunities close, eliminating manual data entry and accelerating your revenue cycle.

The Problem

When a sales deal closes, finance teams typically:

  1. Wait for sales to notify them (often via email or spreadsheet)
  2. Manually look up the customer in Stripe
  3. Create an invoice with the correct line items
  4. Send the invoice to the customer
  5. Update the CRM opportunity with invoice details

This manual process causes:

  • Delayed invoicing - Hours or days between deal close and invoice sent
  • Data entry errors - Wrong amounts, typos in customer details
  • Missing invoices - Deals that slip through the cracks
  • No audit trail - Hard to track what was invoiced and when

The Solution

Build a workflow that automatically:

  1. Triggers instantly when a Salesforce opportunity closes as "Won"
  2. Finds or creates the customer in Stripe
  3. Creates an invoice with line items from the opportunity
  4. Sends the invoice immediately
  5. Updates the Salesforce opportunity with the invoice URL
  6. Notifies the sales and finance teams

What You'll Build

Salesforce Opportunity Closes (Won)


    ┌───────────────────┐
    │ Get Account Email │
    └─────────┬─────────┘


    ┌───────────────────┐
    │ Find/Create       │
    │ Stripe Customer   │
    └─────────┬─────────┘


    ┌───────────────────┐
    │ Create Invoice    │
    │ with Line Items   │
    └─────────┬─────────┘


    ┌───────────────────┐
    │ Finalize & Send   │
    │ Invoice           │
    └─────────┬─────────┘


    ┌───────────────────┐
    │ Update Salesforce │
    │ Opportunity       │
    └─────────┬─────────┘


    ┌───────────────────┐
    │ Notify Team       │
    │ in Slack          │
    └───────────────────┘

Prerequisites

Required Connections

ProviderConnection TypePurpose
SalesforceOAuth via NangoReceive webhooks, update opportunities
StripeOAuth via NangoCreate customers, invoices
SlackOAuth via NangoTeam notifications

Salesforce Setup

  1. Enable Change Data Capture (CDC) for the Opportunity object:

    • Setup → Integrations → Change Data Capture
    • Select "Opportunity" and move to Selected Entities
    • Save
  2. Custom Fields (optional but recommended):

    • Invoice_URL__c (URL) - To store the Stripe invoice link
    • Invoice_Status__c (Picklist) - Draft, Sent, Paid, Overdue

Stripe Setup

Ensure your Stripe account has:

  • Products/prices configured for your offerings
  • Tax settings configured (if applicable)
  • Invoice customization (logo, footer, etc.)

Step-by-Step Implementation

Step 1: Create the Workflow

Start with the basic workflow structure:

{
  "name": "Deal Close → Invoice",
  "description": "Automatically create and send Stripe invoice when Salesforce opportunity closes won",
  "trigger": {
    "type": "webhook",
    "provider": "salesforce",
    "events": ["opportunity.updated"],
    "filter": {
      "StageName": { "$eq": "Closed Won" },
      "IsClosed": { "$eq": true },
      "IsWon": { "$eq": true }
    }
  },
  "steps": []
}

Why this filter?

  • We only want opportunities that just closed as "Won"
  • The CDC event fires on any update, so we filter for the specific state
  • IsClosed and IsWon ensure we don't process losses

Step 2: Get Account Details

Retrieve the associated account to get the billing email:

{
  "id": "get-account",
  "type": "action",
  "name": "Get Account Details",
  "config": {
    "connector": "salesforce",
    "action": "getAccount",
    "accountId": "{{input.payload.AccountId}}"
  }
}

Output includes:

  • Name - Company name
  • BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry
  • Custom fields like Billing_Email__c

Step 3: Find or Create Stripe Customer

Check if the customer exists in Stripe, create if not:

{
  "id": "find-stripe-customer",
  "type": "action",
  "name": "Find Stripe Customer",
  "config": {
    "connector": "stripe",
    "action": "listCustomers",
    "email": "{{steps.get-account.output.Billing_Email__c}}"
  }
}

Then conditionally create:

{
  "id": "create-stripe-customer",
  "type": "action",
  "name": "Create Stripe Customer",
  "condition": "{{steps.find-stripe-customer.output.data.length === 0}}",
  "config": {
    "connector": "stripe",
    "action": "createCustomer",
    "email": "{{steps.get-account.output.Billing_Email__c}}",
    "name": "{{steps.get-account.output.Name}}",
    "metadata": {
      "salesforce_account_id": "{{steps.get-account.output.Id}}",
      "salesforce_opportunity_id": "{{input.payload.Id}}"
    },
    "address": {
      "line1": "{{steps.get-account.output.BillingStreet}}",
      "city": "{{steps.get-account.output.BillingCity}}",
      "state": "{{steps.get-account.output.BillingState}}",
      "postal_code": "{{steps.get-account.output.BillingPostalCode}}",
      "country": "{{steps.get-account.output.BillingCountry}}"
    }
  }
}

Step 4: Resolve Customer ID

Use a transform to get the customer ID regardless of which path was taken:

{
  "id": "resolve-customer",
  "type": "transform",
  "name": "Get Customer ID",
  "config": {
    "operation": "map",
    "data": {
      "customerId": "{{#if steps.create-stripe-customer.output.id}}{{steps.create-stripe-customer.output.id}}{{else}}{{steps.find-stripe-customer.output.data.[0].id}}{{/if}}"
    }
  }
}

Step 5: Create the Invoice

Create the invoice with line items from the opportunity:

{
  "id": "create-invoice",
  "type": "action",
  "name": "Create Stripe Invoice",
  "config": {
    "connector": "stripe",
    "action": "createInvoice",
    "customer": "{{steps.resolve-customer.output.customerId}}",
    "collection_method": "send_invoice",
    "days_until_due": 30,
    "metadata": {
      "salesforce_opportunity_id": "{{input.payload.Id}}",
      "salesforce_opportunity_name": "{{input.payload.Name}}"
    }
  }
}

Step 6: Add Line Items

Add line items based on opportunity products or the total amount:

{
  "id": "add-line-item",
  "type": "action",
  "name": "Add Invoice Line Item",
  "config": {
    "connector": "stripe",
    "action": "createInvoiceItem",
    "customer": "{{steps.resolve-customer.output.customerId}}",
    "invoice": "{{steps.create-invoice.output.id}}",
    "description": "{{input.payload.Name}}",
    "amount": "{{input.payload.Amount | multiply: 100}}",
    "currency": "usd"
  }
}

Note: Stripe amounts are in cents, so we multiply by 100.

Step 7: Finalize and Send Invoice

Finalize the draft invoice and send it:

{
  "id": "finalize-invoice",
  "type": "action",
  "name": "Finalize Invoice",
  "config": {
    "connector": "stripe",
    "action": "finalizeInvoice",
    "invoiceId": "{{steps.create-invoice.output.id}}"
  }
}
{
  "id": "send-invoice",
  "type": "action",
  "name": "Send Invoice",
  "config": {
    "connector": "stripe",
    "action": "sendInvoice",
    "invoiceId": "{{steps.create-invoice.output.id}}"
  }
}

Step 8: Update Salesforce Opportunity

Update the opportunity with the invoice URL:

{
  "id": "update-opportunity",
  "type": "action",
  "name": "Update Opportunity with Invoice",
  "config": {
    "connector": "salesforce",
    "action": "updateOpportunity",
    "opportunityId": "{{input.payload.Id}}",
    "fields": {
      "Invoice_URL__c": "{{steps.send-invoice.output.hosted_invoice_url}}",
      "Invoice_Status__c": "Sent",
      "Description": "{{input.payload.Description}}\n\n---\nInvoice sent automatically on {{now | date: '%Y-%m-%d'}}"
    }
  }
}

Step 9: Notify Team in Slack

Send a notification to your finance channel:

{
  "id": "notify-slack",
  "type": "slack",
  "name": "Notify Finance Team",
  "config": {
    "action": "sendMessage",
    "channel": "#finance-notifications",
    "blocks": [
      {
        "type": "header",
        "text": {
          "type": "plain_text",
          "text": "Invoice Sent"
        }
      },
      {
        "type": "section",
        "fields": [
          {
            "type": "mrkdwn",
            "text": "*Opportunity:*\n{{input.payload.Name}}"
          },
          {
            "type": "mrkdwn",
            "text": "*Amount:*\n${{input.payload.Amount | number_format: 2}}"
          },
          {
            "type": "mrkdwn",
            "text": "*Customer:*\n{{steps.get-account.output.Name}}"
          },
          {
            "type": "mrkdwn",
            "text": "*Invoice #:*\n{{steps.send-invoice.output.number}}"
          }
        ]
      },
      {
        "type": "actions",
        "elements": [
          {
            "type": "button",
            "text": {
              "type": "plain_text",
              "text": "View Invoice"
            },
            "url": "{{steps.send-invoice.output.hosted_invoice_url}}"
          },
          {
            "type": "button",
            "text": {
              "type": "plain_text",
              "text": "View in Salesforce"
            },
            "url": "https://yourinstance.salesforce.com/{{input.payload.Id}}"
          }
        ]
      }
    ]
  }
}

Complete Workflow

Here's the complete workflow JSON:

{
  "name": "Deal Close → Invoice",
  "description": "Automatically create and send Stripe invoice when Salesforce opportunity closes won",
  "trigger": {
    "type": "webhook",
    "provider": "salesforce",
    "events": ["opportunity.updated"],
    "filter": {
      "StageName": { "$eq": "Closed Won" },
      "IsClosed": { "$eq": true },
      "IsWon": { "$eq": true }
    }
  },
  "steps": [
    {
      "id": "get-account",
      "type": "action",
      "name": "Get Account Details",
      "config": {
        "connector": "salesforce",
        "action": "getAccount",
        "accountId": "{{input.payload.AccountId}}"
      }
    },
    {
      "id": "find-stripe-customer",
      "type": "action",
      "name": "Find Stripe Customer",
      "config": {
        "connector": "stripe",
        "action": "listCustomers",
        "email": "{{steps.get-account.output.Billing_Email__c}}"
      }
    },
    {
      "id": "create-stripe-customer",
      "type": "action",
      "name": "Create Stripe Customer",
      "condition": "{{steps.find-stripe-customer.output.data.length === 0}}",
      "config": {
        "connector": "stripe",
        "action": "createCustomer",
        "email": "{{steps.get-account.output.Billing_Email__c}}",
        "name": "{{steps.get-account.output.Name}}",
        "metadata": {
          "salesforce_account_id": "{{steps.get-account.output.Id}}",
          "salesforce_opportunity_id": "{{input.payload.Id}}"
        },
        "address": {
          "line1": "{{steps.get-account.output.BillingStreet}}",
          "city": "{{steps.get-account.output.BillingCity}}",
          "state": "{{steps.get-account.output.BillingState}}",
          "postal_code": "{{steps.get-account.output.BillingPostalCode}}",
          "country": "{{steps.get-account.output.BillingCountry}}"
        }
      }
    },
    {
      "id": "resolve-customer",
      "type": "transform",
      "name": "Get Customer ID",
      "config": {
        "operation": "map",
        "data": {
          "customerId": "{{#if steps.create-stripe-customer.output.id}}{{steps.create-stripe-customer.output.id}}{{else}}{{steps.find-stripe-customer.output.data.[0].id}}{{/if}}"
        }
      }
    },
    {
      "id": "create-invoice",
      "type": "action",
      "name": "Create Stripe Invoice",
      "config": {
        "connector": "stripe",
        "action": "createInvoice",
        "customer": "{{steps.resolve-customer.output.customerId}}",
        "collection_method": "send_invoice",
        "days_until_due": 30,
        "metadata": {
          "salesforce_opportunity_id": "{{input.payload.Id}}",
          "salesforce_opportunity_name": "{{input.payload.Name}}"
        }
      }
    },
    {
      "id": "add-line-item",
      "type": "action",
      "name": "Add Invoice Line Item",
      "config": {
        "connector": "stripe",
        "action": "createInvoiceItem",
        "customer": "{{steps.resolve-customer.output.customerId}}",
        "invoice": "{{steps.create-invoice.output.id}}",
        "description": "{{input.payload.Name}}",
        "amount": "{{input.payload.Amount | multiply: 100}}",
        "currency": "usd"
      }
    },
    {
      "id": "finalize-invoice",
      "type": "action",
      "name": "Finalize Invoice",
      "config": {
        "connector": "stripe",
        "action": "finalizeInvoice",
        "invoiceId": "{{steps.create-invoice.output.id}}"
      }
    },
    {
      "id": "send-invoice",
      "type": "action",
      "name": "Send Invoice",
      "config": {
        "connector": "stripe",
        "action": "sendInvoice",
        "invoiceId": "{{steps.create-invoice.output.id}}"
      }
    },
    {
      "id": "update-opportunity",
      "type": "action",
      "name": "Update Opportunity with Invoice",
      "config": {
        "connector": "salesforce",
        "action": "updateOpportunity",
        "opportunityId": "{{input.payload.Id}}",
        "fields": {
          "Invoice_URL__c": "{{steps.send-invoice.output.hosted_invoice_url}}",
          "Invoice_Status__c": "Sent"
        }
      }
    },
    {
      "id": "notify-slack",
      "type": "slack",
      "name": "Notify Finance Team",
      "config": {
        "action": "sendMessage",
        "channel": "#finance-notifications",
        "blocks": [
          {
            "type": "header",
            "text": {
              "type": "plain_text",
              "text": "Invoice Sent"
            }
          },
          {
            "type": "section",
            "fields": [
              {
                "type": "mrkdwn",
                "text": "*Opportunity:*\n{{input.payload.Name}}"
              },
              {
                "type": "mrkdwn",
                "text": "*Amount:*\n${{input.payload.Amount}}"
              }
            ]
          },
          {
            "type": "actions",
            "elements": [
              {
                "type": "button",
                "text": {
                  "type": "plain_text",
                  "text": "View Invoice"
                },
                "url": "{{steps.send-invoice.output.hosted_invoice_url}}"
              }
            ]
          }
        ]
      }
    }
  ]
}

Testing

1. Create Test Workflow

curl -X POST http://localhost:3000/api/v1/workflows \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d @workflow.json

2. Activate Workflow

curl -X POST http://localhost:3000/api/v1/workflows/{workflowId}/activate \
  -H "x-api-key: YOUR_API_KEY"

3. Test with Simulated Webhook

curl -X POST http://localhost:3000/webhooks/salesforce/{companyId} \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "Id": "006XXXXXXXXXXXXXXX",
      "Name": "Test Opportunity - Enterprise Plan",
      "Amount": 50000,
      "StageName": "Closed Won",
      "IsClosed": true,
      "IsWon": true,
      "AccountId": "001XXXXXXXXXXXXXXX"
    }
  }'

4. Verify Results

Check each system:

SystemWhat to Verify
Stripe DashboardNew customer created, invoice sent
SalesforceOpportunity updated with Invoice_URL__c
SlackNotification in #finance-notifications
Workflow RunsRun completed successfully

5. End-to-End Test

For a real test:

  1. Create a test opportunity in Salesforce
  2. Move it through stages to "Closed Won"
  3. Watch the workflow execute in real-time
  4. Verify all systems updated correctly

Error Handling

Handle Missing Customer Email

{
  "id": "validate-email",
  "type": "condition",
  "name": "Validate Email Exists",
  "config": {
    "if": "{{steps.get-account.output.Billing_Email__c}}",
    "then": "continue",
    "else": "notify-missing-email"
  }
}

Handle Stripe API Errors

{
  "id": "create-invoice",
  "type": "action",
  "config": { ... },
  "onError": {
    "action": "goto",
    "step": "notify-error"
  }
}

Notify on Failure

{
  "id": "notify-error",
  "type": "slack",
  "name": "Notify Invoice Error",
  "config": {
    "action": "sendMessage",
    "channel": "#finance-alerts",
    "text": ":warning: Failed to create invoice for opportunity {{input.payload.Name}}. Error: {{error.message}}"
  }
}

Variations and Extensions

Multi-Currency Support

Add currency detection based on account country:

{
  "id": "determine-currency",
  "type": "transform",
  "config": {
    "operation": "map",
    "data": {
      "currency": "{{#if (eq steps.get-account.output.BillingCountry 'United Kingdom')}}gbp{{else if (eq steps.get-account.output.BillingCountry 'Germany')}}eur{{else}}usd{{/if}}"
    }
  }
}

Approval for Large Deals

Add an approval step for deals over $100k:

{
  "id": "check-approval-needed",
  "type": "condition",
  "config": {
    "if": "{{input.payload.Amount > 100000}}",
    "then": "request-approval",
    "else": "create-invoice"
  }
},
{
  "id": "request-approval",
  "type": "slack",
  "config": {
    "action": "sendMessage",
    "channel": "#finance-approvals",
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*Invoice Approval Required*\n\nOpportunity: {{input.payload.Name}}\nAmount: ${{input.payload.Amount}}"
        }
      },
      {
        "type": "actions",
        "elements": [
          {
            "type": "button",
            "text": { "type": "plain_text", "text": "Approve" },
            "style": "primary",
            "action_id": "approve_invoice_{{input.payload.Id}}"
          },
          {
            "type": "button",
            "text": { "type": "plain_text", "text": "Reject" },
            "style": "danger",
            "action_id": "reject_invoice_{{input.payload.Id}}"
          }
        ]
      }
    ]
  }
}

Multiple Line Items from Opportunity Products

If your opportunities have product line items:

{
  "id": "get-opportunity-products",
  "type": "action",
  "config": {
    "connector": "salesforce",
    "action": "query",
    "query": "SELECT Id, Name, Quantity, UnitPrice, TotalPrice FROM OpportunityLineItem WHERE OpportunityId = '{{input.payload.Id}}'"
  }
},
{
  "id": "add-line-items",
  "type": "loop",
  "config": {
    "items": "{{steps.get-opportunity-products.output.records}}",
    "step": {
      "type": "action",
      "config": {
        "connector": "stripe",
        "action": "createInvoiceItem",
        "customer": "{{steps.resolve-customer.output.customerId}}",
        "invoice": "{{steps.create-invoice.output.id}}",
        "description": "{{item.Name}} (Qty: {{item.Quantity}})",
        "amount": "{{item.TotalPrice | multiply: 100}}",
        "currency": "usd"
      }
    }
  }
}

Add Tax Calculation

Integrate tax calculation for compliant invoicing:

{
  "id": "create-invoice",
  "type": "action",
  "config": {
    "connector": "stripe",
    "action": "createInvoice",
    "customer": "{{steps.resolve-customer.output.customerId}}",
    "automatic_tax": {
      "enabled": true
    }
  }
}

HubSpot Variant

Use HubSpot instead of Salesforce:

{
  "trigger": {
    "type": "webhook",
    "provider": "hubspot",
    "events": ["deal.propertyChange"],
    "filter": {
      "propertyName": "dealstage",
      "propertyValue": "closedwon"
    }
  }
}

Common Issues

IssueCauseSolution
Invoice not createdMissing customer in StripeEnsure find-or-create logic works
Wrong amountCurrency conversionVerify amount is in cents for Stripe
Duplicate invoicesWebhook retriesAdd idempotency key using opportunity ID
Missing emailNo billing email on accountAdd validation step, notify if missing