Loopfour
IntegrationsPayments

Stripe

Payment processing for invoices, subscriptions, and payment intents

Stripe

Connect to Stripe for payment processing, subscription management, and financial operations.

Overview

Stripe is a payment infrastructure platform. The integration supports:

  • Customer Management - Create and manage customer records
  • Invoicing - Create, send, finalize, and void invoices
  • Subscriptions - Create and manage recurring billing
  • Payment Intents - Process one-time payments
  • Balance - Check account balance and transactions

Prerequisites

  • Stripe account (Standard or Connect)
  • API keys or OAuth app credentials
  • Appropriate scopes for desired operations

Authentication

Stripe can be connected in two ways. Both open the same connect dialog and produce a normal Stripe connection — workflow steps, actions, and the account picker behave identically afterwards.

MethodWhen to useWhat you provide
OAuth (Stripe Connect)Default. Authorize in Stripe, nothing to copy, revocable from the Stripe dashboard.Stripe login
API keyWhen OAuth is unavailable — for example a Stripe account that does not expose the Connect flow.A Stripe restricted key (rk_...)

In the Studio, Integrations → Stripe → Connect asks which method to use. The same picker appears on a workflow block's account selector and on the chat "Connect Stripe" card. A reconnect always reuses the method the account was first connected with.

The API-key method needs a restricted key, not a standard secret key: create one under Developers → API keys → Restricted keys in Stripe and grant it the permissions the workflow needs (for example write access to Customers and Invoices). A connection's method is shown next to its name under Integrations → Stripe → Manage.

To start a connect flow over the API, pass authVariant (oauth or api_key):

curl -X POST "https://workflow.loopfour.ai/api/v1/connect/sessions" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"provider": "stripe", "authVariant": "api_key"}'

Available Actions

Customer Actions

getCustomer

Get a customer by ID.

{
  "action": "stripe.getCustomer",
  "config": {
    "customerId": "cus_xxx"
  }
}

createCustomer

Create a new customer.

{
  "id": "create-customer",
  "type": "action",
  "action": "stripe.createCustomer",
  "config": {
    "email": "{{input.email}}",
    "name": "{{input.name}}",
    "description": "Customer from workflow",
    "phone": "{{input.phone}}",
    "address": {
      "line1": "{{input.address.street}}",
      "city": "{{input.address.city}}",
      "state": "{{input.address.state}}",
      "postal_code": "{{input.address.zip}}",
      "country": "{{input.address.country}}"
    },
    "metadata": {
      "source": "workflow",
      "company_id": "{{input.companyId}}"
    }
  }
}

Parameters:

FieldTypeRequiredDescription
emailstringNoCustomer email
namestringNoCustomer name
descriptionstringNoDescription
phonestringNoPhone number
addressobjectNoCustomer address
invoicePrefixstringNoSent as Stripe's invoice_prefix. Stripe numbers this customer's invoices <prefix>-0001, <prefix>-0002, and so on. Must be 3-12 uppercase letters or numbers; anything else fails the step before Stripe is called. Omit it and Stripe assigns a random prefix.
metadataobjectNoKey-value metadata

updateCustomer

Update an existing customer.

{
  "action": "stripe.updateCustomer",
  "config": {
    "customerId": "{{input.customerId}}",
    "email": "{{input.newEmail}}",
    "metadata": {
      "updated_at": "{{now}}"
    }
  }
}

listCustomers

List customers with filters.

{
  "action": "stripe.listCustomers",
  "config": {
    "limit": 100,
    "email": "{{input.email}}",
    "autoPaginate": true
  }
}

Set autoPaginate to true to combine every page of customers matching the email before checking for an existing customer. It also works for listTaxRates. Leave it false to page manually with startingAfter.

Every Stripe list operation — listCustomers, listInvoices, listPaymentIntents, listSubscriptions, listTaxRates and listBalanceTransactions — accepts startingAfter: the id of the last object on the previous page, sent as starting_after. Keep paging while the response's has_more is true. A single unpaged call finding nothing is not proof the record does not exist.

Invoice Actions

createInvoice

Create a new invoice.

{
  "id": "create-invoice",
  "type": "action",
  "action": "stripe.createInvoice",
  "config": {
    "customerId": "{{steps.customer.output.id}}",
    "autoAdvance": false,
    "collectionMethod": "send_invoice",
    "daysUntilDue": 30,
    "description": "Invoice for services",
    "metadata": {
      "order_id": "{{input.orderId}}"
    }
  }
}

Parameters:

FieldTypeRequiredDescription
customerIdstringYesCustomer to invoice
autoAdvancebooleanNoAuto-finalize (default: true)
collectionMethodstringNocharge_automatically or send_invoice
daysUntilDuenumberNoDays until due (for send_invoice)
descriptionstringNoInvoice description
metadataobjectNoCustom metadata

createInvoiceItem

Add a line item to an invoice.

To bill an existing price, pass priceId and quantity instead of productId and a unit amount. The price carries its own currency, and reusing it keeps Stripe from minting a new price for every invoice. priceId is sent as pricing[price]: Stripe API version 2025-03-31.basil removed the top-level price parameter from invoice items (changelog).

{
  "action": "stripe.createInvoiceItem",
  "config": {
    "customerId": "{{steps.customer.output.id}}",
    "invoiceId": "{{steps.create-invoice.output.id}}",
    "amount": 9900,
    "currency": "usd",
    "description": "Professional Services - January 2024",
    "quantity": 1
  }
}

To bill against a catalog product instead of an ad-hoc line, pass productId with unitAmount (or amount). The line is then priced inline through price_data, so the invoice reports revenue per product:

{
  "action": "stripe.createInvoiceItem",
  "config": {
    "customerId": "{{steps.customer.output.id}}",
    "invoiceId": "{{steps.create-invoice.output.id}}",
    "productId": "{{steps.search_product.data.0.id || steps.create_product.id}}",
    "unitAmount": 9900,
    "quantity": 2,
    "description": "ROAR Smart Beacon"
  }
}

To attach an externally computed sales tax (for example a rate Avalara returned), pass taxRateIds — an array of Stripe tax rate ids (txr_...) — and the line is sent with tax_rates. Create or look the rates up first with listTaxRates / createTaxRate. The value must be an array of at most 10 non-empty strings (Stripe's per-item cap); anything else fails before the request is sent. A Studio code field hands the value over as a JSON string, so '["txr_1"]' is accepted as well as a real array.

{
  "action": "stripe.createInvoiceItem",
  "config": {
    "customerId": "{{steps.customer.output.id}}",
    "invoiceId": "{{steps.create-invoice.output.id}}",
    "unitAmount": 9900,
    "quantity": 2,
    "taxRateIds": ["{{steps.create_tax_rate.id}}"]
  }
}

To discount a line with a coupon that already exists, pass its id as couponId; it is trimmed, must contain only letters, digits, _ and -, and is sent as discounts[0][coupon]. No coupon is created. Find or create the coupon first with listCoupons and createCoupon. couponId cannot be combined with discountAmount (the legacy path that creates a single-use amount_off coupon on every call); the call fails before anything is sent. A blank couponId sends no discount, so a loop can pass '' for undiscounted lines.

{
  "action": "stripe.createInvoiceItem",
  "config": {
    "customerId": "{{steps.customer.output.id}}",
    "invoiceId": "{{steps.create-invoice.output.id}}",
    "priceId": "price_1MoBy5LkdIwHu7ixZhnattbh",
    "quantity": 3,
    "couponId": "lf-pct-10"
  }
}

getInvoice

Get an invoice by ID.

{
  "action": "stripe.getInvoice",
  "config": {
    "invoiceId": "in_xxx"
  }
}

listInvoices

List invoices with filters.

{
  "action": "stripe.listInvoices",
  "config": {
    "customerId": "{{input.customerId}}",
    "status": "open",
    "limit": 50
  }
}

finalizeInvoice

Finalize a draft invoice.

{
  "action": "stripe.finalizeInvoice",
  "config": {
    "invoiceId": "{{steps.create-invoice.output.id}}"
  }
}

sendInvoice

Send an invoice to the customer.

{
  "action": "stripe.sendInvoice",
  "config": {
    "invoiceId": "{{steps.finalize.output.id}}"
  }
}

voidInvoice

Void an invoice.

{
  "action": "stripe.voidInvoice",
  "config": {
    "invoiceId": "{{input.invoiceId}}"
  }
}

deleteInvoice

Permanently delete a draft invoice (DELETE /v1/invoices/:id). Stripe refuses any other status; void a finalized invoice instead.

{
  "action": "stripe.deleteInvoice",
  "config": {
    "invoiceId": "{{input.invoiceId}}"
  }
}

listInvoiceLineItems

List one page of an invoice's line items (GET /v1/invoices/{id}/lines). The page comes back as Stripe returns it; keep paging with startingAfter set to the last line's id while has_more is true. limit is 1–100; blank uses Stripe's default of 10.

{
  "action": "stripe.listInvoiceLineItems",
  "config": {
    "invoiceId": "{{steps.get-run-row.output.invoiceId}}",
    "limit": 100
  }
}

Parameters:

FieldTypeRequiredDescription
invoiceIdstringYesInvoice whose lines to list
limitnumberNoPage size, 1–100 (Stripe default 10)
startingAfterstringNoLine item id to page after (starting_after)

previewCreditNote

Preview a credit note without creating it (GET /v1/credit_notes/preview). It takes the same fields as createCreditNote and returns the credit note Stripe would issue, including per-line amount, total and total_taxes. Use it to read the credit before an approval, then create the note with the same fields.

createCreditNote

Issue a credit note on a finalized invoice (POST /v1/credit_notes). Each entry in lines credits one invoice line, either by quantity (Stripe credits that line's tax proportionally through its tax rates) or by amount in cents, not both. Only invoice_line_item lines are supported.

Stripe first reduces the invoice's amount due. Whatever exceeds it must be split across creditAmount (customer balance, applied to the next finalized invoice), refundAmount and outOfBandAmount, and those must add up to the excess (create a credit note).

idempotencyKey is required and sent as Stripe's Idempotency-Key as is: 1–255 printable characters, no whitespace. Build it from stable ids (for example cn:<invoice>:<quote>) so a retry or a second run gets the first credit note back instead of crediting the invoice twice. A call without one is refused. Stripe keeps keys for 24 hours, so also guard re-runs in the workflow.

{
  "action": "stripe.createCreditNote",
  "config": {
    "invoiceId": "in_1MxvRkLkdIwHu7ixABNtI99m",
    "lines": [
      { "type": "invoice_line_item", "invoice_line_item": "il_1MxvRlLkdIwHu7ixnkbntxUV", "quantity": 2 }
    ],
    "creditAmount": 0,
    "reason": "order_change",
    "memo": "Units not installed",
    "metadata": { "hubspot_deal_id": "{{trigger.data.objectId}}" },
    "idempotencyKey": "cn:in_1MxvRkLkdIwHu7ixABNtI99m:quote-2"
  }
}

Parameters:

FieldTypeRequiredDescription
invoiceIdstringYesFinalized invoice to credit (invoice)
linesarrayYes[{ type: "invoice_line_item", invoice_line_item, quantity }] or with amount (cents) instead of quantity
creditAmountnumberNoCents credited to the customer balance (credit_amount)
refundAmountnumberNoCents refunded to the invoice's charge (refund_amount)
outOfBandAmountnumberNoCents credited outside Stripe (out_of_band_amount)
reasonstringNoduplicate, fraudulent, order_change or product_unsatisfactory
memostringNoPrinted on the credit note PDF
metadataobjectNoString, number or boolean values; keys without [ or ]; null values are skipped
idempotencyKeystringYes (create)Idempotency-Key sent as is, built from stable ids; required by createCreditNote. previewCreditNote never sends one

Payment Intent Actions

createPaymentIntent

Create a payment intent for one-time payments.

{
  "id": "create-payment",
  "type": "action",
  "action": "stripe.createPaymentIntent",
  "config": {
    "amount": 9900,
    "currency": "usd",
    "customerId": "{{input.customerId}}",
    "description": "Order #{{input.orderId}}",
    "paymentMethodTypes": ["card"],
    "captureMethod": "automatic",
    "metadata": {
      "order_id": "{{input.orderId}}"
    }
  }
}

Parameters:

FieldTypeRequiredDescription
amountnumberYesAmount in cents
currencystringYesThree-letter currency code
customerIdstringNoCustomer to charge
descriptionstringNoPayment description
paymentMethodTypesarrayNoAllowed payment methods
paymentMethodstringNoSpecific payment method ID
captureMethodstringNoautomatic or manual
confirmbooleanNoConfirm immediately
offSessionbooleanNoOff-session payment
setupFutureUsagestringNoon_session or off_session
receiptEmailstringNoEmail for receipt
statementDescriptorstringNoStatement descriptor
metadataobjectNoCustom metadata

getPaymentIntent

Get a payment intent by ID.

{
  "action": "stripe.getPaymentIntent",
  "config": {
    "paymentIntentId": "pi_xxx"
  }
}

confirmPaymentIntent

Confirm a payment intent.

{
  "action": "stripe.confirmPaymentIntent",
  "config": {
    "paymentIntentId": "{{steps.create-payment.output.id}}",
    "paymentMethod": "{{input.paymentMethodId}}",
    "returnUrl": "https://example.com/payment/complete"
  }
}

capturePaymentIntent

Capture a payment intent (for manual capture).

{
  "action": "stripe.capturePaymentIntent",
  "config": {
    "paymentIntentId": "{{input.paymentIntentId}}",
    "amountToCapture": 5000
  }
}

cancelPaymentIntent

Cancel a payment intent.

{
  "action": "stripe.cancelPaymentIntent",
  "config": {
    "paymentIntentId": "{{input.paymentIntentId}}",
    "cancellationReason": "requested_by_customer"
  }
}

listPaymentIntents

List payment intents.

{
  "action": "stripe.listPaymentIntents",
  "config": {
    "customerId": "{{input.customerId}}",
    "limit": 50
  }
}

Subscription Actions

createSubscription

Create a new subscription.

{
  "id": "create-subscription",
  "type": "action",
  "action": "stripe.createSubscription",
  "config": {
    "customerId": "{{input.customerId}}",
    "items": [
      { "price": "price_xxx" }
    ],
    "collectionMethod": "charge_automatically",
    "trialPeriodDays": 14,
    "metadata": {
      "plan": "{{input.planName}}"
    }
  }
}

Parameters:

FieldTypeRequiredDescription
customerIdstringYesCustomer ID
itemsarrayYesSubscription items. An item with price (a price id) and no price_data is sent to Stripe unchanged
items[].taxRateIdsarrayNoTax rate ids (txr_...) sent as that item's tax_rates; max 10 per item
defaultPaymentMethodstringNoDefault payment method
collectionMethodstringNocharge_automatically or send_invoice
daysUntilDuenumberNoDays until due (for send_invoice)
trialPeriodDaysnumberNoTrial period in days
cancelAtPeriodEndbooleanNoCancel at period end
prorationBehaviorstringNocreate_prorations, none, always_invoice
backdateStartDatenumberNoPast start date in unix seconds (backdate_start_date); a future or fractional value is refused
billingModestringNoOnly flexible (billing_mode[type]); any other value is refused before anything is created
metadataobjectNoCustom metadata

Items with mixed billing periods turn on flexible billing mode and pin Stripe-Version: 2025-06-30.basil. When billingMode: "flexible" is set, the action sends it and adds no version pin, so the account's own API version applies.

With billingMode: "flexible", a backdateStartDate and no billingCycleAnchor, Stripe anchors the billing cycle to the backdated start and the first invoice bills one full period from it (backdating). A start date more than one period in the past bills every elapsed period.

createSubscriptionSchedule

Create a subscription that starts on a future date. The first phase's start_date becomes the schedule's start; a start already in the past is sent as now.

FieldTypeRequiredDescription
customerIdstringYesCustomer ID
phasesarrayYesPhases with start_date (unix seconds) and items. duration is supported only on a single-phase schedule and is kept as given; a schedule with two or more phases where any phase has a duration is refused. iterations is handled as before: it is replaced by the next phase's start or a fallback of 1
endBehaviorstringNorelease (default, the subscription continues) or cancel
defaultSettingsobjectNoLaid over the defaults: collection_method from collectionMethod (default send_invoice) and invoice_settings.days_until_due from daysUntilDue (default 30). Accepts only collection_method, days_until_due (sent under invoice_settings), invoice_settings, automatic_tax, billing_cycle_anchor and description; any other key is refused. With charge_automatically no days_until_due is sent, and an explicit one (daysUntilDue, days_until_due or invoice_settings.days_until_due) is refused
metadataobjectNoCustom metadata

Stripe removed a phase's iterations in API version 2025-08-27.basil; use duration ({ "interval": "month", "interval_count": 1 }) on newer versions (changelog). duration arrived in 2025-07-30.basil, after the 2025-06-30.basil version that mixed billing intervals pin, so a schedule with both is refused.

getSubscription

Get a subscription by ID.

{
  "action": "stripe.getSubscription",
  "config": {
    "subscriptionId": "sub_xxx"
  }
}

updateSubscription

Update a subscription.

{
  "action": "stripe.updateSubscription",
  "config": {
    "subscriptionId": "{{input.subscriptionId}}",
    "items": [
      { "id": "{{input.itemId}}", "price": "price_new_xxx" }
    ],
    "prorationBehavior": "create_prorations"
  }
}

An upsell that adds a product, raises the quantity of an existing item, and invoices the proration immediately:

{
  "action": "stripe.updateSubscription",
  "config": {
    "subscriptionId": "{{input.subscriptionId}}",
    "items": [
      { "price": "price_addon_xxx", "quantity": 2, "taxRateIds": ["txr_xxx"] },
      { "id": "si_xxx", "quantity": 5 }
    ],
    "prorationBehavior": "always_invoice",
    "prorationDate": "{{input.prorationDate}}",
    "idempotencyKey": "upsell:{{input.subscriptionId}}:{{input.quoteId}}"
  }
}

Parameters:

FieldTypeRequiredDescription
subscriptionIdstringYesSubscription to update (sub_...)
itemsarrayNoUp to 20 item changes; an empty array is refused (omit items to leave the items unchanged). { "id": "si_...", "quantity": 5 } changes an existing subscription item; { "price": "price_...", "quantity": 2 } with no id adds one; { "id": "si_...", "deleted": true } removes one. price and price_data are not accepted together. discounts and other Stripe item fields pass through
items[].taxRateIdsarrayNoTax rate ids (txr_...) sent as that item's tax_rates; max 10 per item. An item that already spells tax_rates is sent unchanged; an item with both is refused
prorationBehaviorstringNocreate_prorations (Stripe's default), always_invoice (invoice the proration immediately; requires idempotencyKey) or none; any other value is refused
prorationDatenumberNoproration_date, unix seconds or ISO 8601. Needs items and is refused with prorationBehavior: "none", the same rules as previewInvoice. Use the value you previewed with so the update bills what was previewed
paymentBehaviorstringNoallow_incomplete, default_incomplete, error_if_incomplete or pending_if_incomplete
idempotencyKeystringWith always_invoiceSent as the Idempotency-Key header: 1-255 printable characters, no whitespace. A retry with the same key returns the first result instead of updating (and invoicing) twice
cancelAtPeriodEnd, pauseCollection, collectionMethod, daysUntilDue, defaultPaymentMethod, metadata, billingCycleAnchorvariousNoSent as the matching Stripe field

subscriptionId, items, prorationBehavior, prorationDate, paymentBehavior and idempotencyKey are checked before the request, so a refused value in one of them changes nothing in Stripe. The other fields are sent as given and Stripe validates them.

Idempotency. Stripe may prune a key once it is at least 24 hours old; a re-run after that applies the update, and bills the proration, again. Reusing a key with different parameters fails with an idempotency error (idempotent requests). Build the key from ids that identify this exact change, such as the subscription id and the quote id, not just a deal id that several changes share.

Trusted data only. Build items from your own records (approved quote lines, price ids you created). Every key on an item other than the ones listed above is forwarded to Stripe, so an item built from an untrusted payload could, for example, set its own price_data.unit_amount.

See Stripe's update a subscription and prorations.

previewInvoice

Preview an invoice without creating it (POST /v1/invoices/create_preview). Give one target: scheduleId (the schedule's next invoice), customerId (the customer's upcoming invoice) or subscriptionId. With subscriptionId, items, prorationBehavior and prorationDate preview a proposed change, for example the proration an upsell would bill, before an approval step:

{
  "action": "stripe.previewInvoice",
  "config": {
    "subscriptionId": "{{input.subscriptionId}}",
    "items": [
      { "price": "price_addon_xxx", "quantity": 2, "taxRateIds": ["txr_xxx"] },
      { "id": "si_xxx", "quantity": 5 }
    ],
    "prorationBehavior": "always_invoice",
    "prorationDate": 1790000000
  }
}
FieldTypeRequiredDescription
subscriptionIdstringOne targetSent as subscription; cannot be combined with scheduleId or customerId
scheduleIdstringOne targetSent as schedule
customerIdstringOne targetSent as customer
itemsarrayNoSame shape as updateSubscription items (taxRateIds becomes tax_rates), sent as subscription_details[items]. Needs subscriptionId
prorationBehaviorstringNosubscription_details[proration_behavior]: create_prorations, always_invoice or none. Needs subscriptionId
prorationDatenumberNosubscription_details[proration_date], unix seconds or ISO 8601. Needs items and cannot be combined with prorationBehavior: "none"

Pass the same prorationDate to updateSubscription so the update bills exactly the prorations that were previewed (create a preview invoice). The proration lines are the preview's line items whose parent.subscription_item_details.proration is true on API version 2025-03-31.basil and later; earlier versions put proration at the top level of the line item (changelog). Read it from wherever the connected account's API version puts it.

cancelSubscription

Cancel a subscription.

{
  "action": "stripe.cancelSubscription",
  "config": {
    "subscriptionId": "{{input.subscriptionId}}",
    "invoiceNow": true,
    "prorate": true
  }
}

listSubscriptions

List subscriptions.

{
  "action": "stripe.listSubscriptions",
  "config": {
    "customerId": "{{input.customerId}}",
    "status": "active",
    "limit": 50
  }
}

Product Actions

createProduct

Create a catalog product.

Set idempotencyKey (at most 255 characters) to send it as Stripe's Idempotency-Key header. A second request with the same key, for example from a concurrent run, gets the first product back instead of creating another. Stripe keeps keys for at least 24 hours and rejects a reused key whose parameters differ (idempotent requests). Without it, no key is sent.

{
  "action": "stripe.createProduct",
  "config": {
    "name": "ROAR Smart Beacon",
    "description": "Battery-powered panic button",
    "metadata": { "qbo_item_id": "375" },
    "idempotencyKey": "hubspot-product:375"
  }
}

searchProducts

Search products with Stripe's search query language. limit defaults to 10 (1–100).

{
  "action": "stripe.searchProducts",
  "config": {
    "query": "active:'true' AND metadata['qbo_item_id']:'375'",
    "limit": 1
  }
}

listPrices

List prices. priceLimit defaults to 100 (1–100); keep paging with startingAfter set to the last price's id while has_more is true. Leave priceActive blank for Stripe's default, which returns only active prices; set it to false for inactive ones.

lookupKeys finds prices by their lookup_key without paging: pass up to 10 keys as an array, a JSON array, or a comma-separated list. Duplicates are dropped; more than 10 distinct keys are rejected before the call, matching Stripe's limit. The currency filter is priceCurrency, not the shared currency field, so a list is never filtered to usd by default.

{
  "action": "stripe.listPrices",
  "config": {
    "productId": "prod_NZKdYqrwEYx6iK",
    "priceActive": true,
    "priceType": "one_time",
    "priceCurrency": "usd",
    "lookupKeys": ["lf:prod_NZKdYqrwEYx6iK:one_time:1000"]
  }
}

Parameters:

FieldTypeRequiredDescription
productIdstringNoOnly prices of this product (product)
priceActivebooleanNoOnly active (true) or inactive (false) prices (active); no default
priceTypestringNoone_time or recurring (type)
priceCurrencystringNoThree-letter ISO currency code (currency); blank lists every currency
lookupKeysstring[]NoUp to 10 lookup keys (lookup_keys[]), de-duplicated
priceLimitnumberNoPage size, 1–100 (default 100) (limit)
startingAfterstringNoPrice id to page after (starting_after)

createPrice

Create a one-time or recurring price for an existing product. The amount is sent as unit_amount_decimal, so fractional cents (for example 3599.1) are kept. 0 creates a free price. Stripe accepts at most 12 decimals, so round a computed amount to 12 decimals or fewer before passing it; longer values are rejected before the call. Leave interval blank for a one-time price.

Each call sends an Idempotency-Key built from the run, the loop iteration and the price fields, so a retry after a timeout returns the price Stripe already created instead of a second one. Identical prices requested twice in the same run and iteration resolve to the same price.

lookupKey (at most 200 characters) is sent as lookup_key. Stripe keeps lookup keys unique, so creating a second price with a key another price holds fails; the action never sends transfer_lookup_key, which would move the key instead (create a price).

{
  "action": "stripe.createPrice",
  "config": {
    "productId": "prod_NZKdYqrwEYx6iK",
    "currency": "usd",
    "unitAmountDecimal": "1000",
    "interval": "month",
    "intervalCount": 1,
    "lookupKey": "lf:prod_NZKdYqrwEYx6iK:recurring:1000:month:1",
    "metadata": { "source": "loopfour-workflows" }
  }
}

Parameters:

FieldTypeRequiredDescription
productIdstringYesProduct the price belongs to (product)
unitAmountDecimalstringYesNon-negative amount in the smallest currency unit, at most 12 decimals
currencystringNoThree-letter ISO currency code (default usd)
intervalstringNoday, week, month or year (recurring[interval]); blank for one-time
intervalCountnumberNoPositive integer (recurring[interval_count]); requires interval
nicknamestringNoInternal label, hidden from customers
metadataobjectNoCustom metadata
taxBehaviorstringNoinclusive, exclusive or unspecified (tax_behavior)
lookupKeystringNoUnique key, at most 200 characters (lookup_key); a taken key fails

Stripe prices cannot change amount after creation. To reuse prices across runs, give each price a deterministic lookupKey, call listPrices with lookupKeys, and call createPrice only for keys with no match.

Reusing products across runs

Keep one Stripe product per catalog item by composing the two actions: searchProducts by a metadata key, a condition on whether anything came back, and createProduct only when nothing did. Cache the returned id (for example in a data table) and pass it as productId to createInvoiceItem or as price_data.product to createSubscription. Stripe's search index is eventually consistent, so the cached id, not the search, is what makes back-to-back runs reuse the same product.

[
  {
    "id": "search_product",
    "action": "stripe.searchProducts",
    "config": { "query": "active:'true' AND metadata['qbo_item_id']:'375'", "limit": 1 }
  },
  {
    "id": "check_product_missing",
    "type": "condition",
    "config": {
      "conditions": { "left": "{{steps.search_product.data.length}}", "operator": "eq", "right": "0" },
      "then": ["create_product"]
    }
  },
  {
    "id": "create_product",
    "action": "stripe.createProduct",
    "config": { "name": "ROAR Smart Beacon", "metadata": { "qbo_item_id": "375" } }
  }
]

Coupon Actions

A coupon with a fixed id can be reused on every invoice item that needs the same discount. Stripe refuses a second coupon with an id that is already taken, so the id itself keeps runs from creating duplicates.

listCoupons

List one page of coupons. limit is 1–100 (Stripe's default is 10); keep paging with startingAfter set to the last coupon's id while has_more is true. Stripe leaves out a coupon's applies_to (the products it is restricted to) unless asked; set expandAppliesTo to true to include it (expand[]=data.applies_to), for example before reusing a coupon you did not create.

{
  "action": "stripe.listCoupons",
  "config": { "limit": 100, "expandAppliesTo": true }
}

Parameters:

FieldTypeRequiredDescription
limitnumberNoPage size, 1–100 (limit)
startingAfterstringNoCoupon id to page after (starting_after)
expandAppliesTobooleanNoInclude each coupon's applies_to (expand[]=data.applies_to); default false

createCoupon

Create a percentage or amount coupon. Pass exactly one of percentOff or amountOff; amountOff needs a currency, and a currency is never sent with percentOff. couponDuration defaults to once, Stripe's own default; pass forever for a coupon that is reused across invoices. metadata values must be strings, numbers or booleans. The action never sends max_redemptions or redeem_by. Each call sends an Idempotency-Key built from the run, the loop iteration and the coupon fields.

When couponId is already taken, Stripe fails the call with error code resource_already_exists (error codes), and the step error ends with (resource_already_exists). A workflow that creates coupons by fixed id can treat that error as "reuse the existing coupon", after checking the existing coupon's terms with listCoupons.

{
  "action": "stripe.createCoupon",
  "config": {
    "couponId": "lf-pct-10",
    "percentOff": 10,
    "couponDuration": "forever",
    "couponName": "10% line discount"
  }
}

Parameters:

FieldTypeRequiredDescription
couponIdstringNoFixed id (id): letters, digits, _ and -; Stripe generates one when blank
percentOffnumberOne ofGreater than 0 and at most 100, at most 2 decimals (percent_off)
amountOffnumberOne ofWhole cents (amount_off); requires currency
currencystringWith amountOffThree-letter ISO currency code
couponDurationstringNoonce (default), forever or repeating (duration)
durationInMonthsnumberWith repeatingPositive integer (duration_in_months); refused with any other duration
couponNamestringNoShown on invoices; a string, trimmed, at most 40 characters (name)
metadataobjectNoCustom metadata; string, number or boolean values

Tax Rate Actions

Stripe tax rates carry an externally computed sales tax (for example from Avalara) onto invoice items and subscription items via taxRateIds. Stripe treats a tax rate as immutable once created, so create one per distinct jurisdiction/percentage pair and reuse its id.

listTaxRates

List tax rates. Returns a Stripe list response (data[]), combining pages when autoPaginate is true. active defaults to true; inclusive is an optional filter; limit defaults to 100 (1–100); startingAfter is the pagination cursor, sent as starting_after.

{
  "action": "stripe.listTaxRates",
  "config": {
    "active": true,
    "inclusive": false,
    "limit": 100,
    "autoPaginate": true
  }
}

Stripe has no search endpoint for tax rates. Set autoPaginate: true when checking whether a rate already exists: the action follows starting_after until has_more is false and returns all rates in data. Without it, the action returns one page of at most 100 rates; finding nothing on that page does not prove the rate is absent.

createTaxRate

Create a tax rate. Returns the raw tax rate object.

{
  "action": "stripe.createTaxRate",
  "config": {
    "displayName": "Sales Tax",
    "percentage": 8.25,
    "inclusive": false,
    "jurisdiction": "TX",
    "country": "US",
    "state": "TX",
    "taxType": "sales_tax",
    "metadata": { "avalara_jurisdiction": "TX-AUSTIN" }
  }
}
FieldTypeRequiredDescription
displayNamestringYesName shown on invoices (display_name)
percentagenumberYes0–100, at most 4 decimal places (Stripe's precision)
inclusivebooleanNoTax included in the price (default false)
jurisdictionstringNoJurisdiction label
countrystringNo2-letter ISO country code
statestringNoState or province
descriptionstringNoInternal description
taxTypestringNoStripe tax_type (sales_tax, vat, gst, ...)
metadataobjectNoCustom metadata

jurisdiction, state, description and taxType must each be a string or a number; any other shape is rejected before the request rather than stringified.

Balance Actions

getBalance

Get account balance.

{
  "action": "stripe.getBalance",
  "config": {}
}

listBalanceTransactions

List balance transactions, newest first.

{
  "action": "stripe.listBalanceTransactions",
  "config": {
    "limit": 100,
    "type": "charge",
    "createdFrom": "2026-09-01T00:00:00Z",
    "createdTo": "2026-09-30T23:59:59Z"
  }
}
FieldSent asNotes
typetypee.g. charge, payout, stripe_fee
payoutpayoutOnly the transactions paid out in this automatic payout (po_...). Stripe returns nothing for a manual payout.
createdFromcreated[gte]ISO 8601 date or unix seconds, inclusive
createdTocreated[lte]ISO 8601 date or unix seconds, inclusive. Must not be before createdFrom.

To reconcile a payout against a bank deposit, list its transactions with payout rather than grouping transactions by date: grouping only approximates what Stripe settled in the payout. The net values of the non-payout rows sum to the payout amount, and processing fees, Stripe billing fees (stripe_fee), refunds, disputes and reserve holds or releases (risk_reserved_funds) each appear as their own rows.

{
  "id": "each_payout",
  "type": "loop",
  "config": {
    "collection": "{{steps.list_payouts.data}}",
    "steps": ["payout_transactions"]
  }
},
{
  "id": "payout_transactions",
  "type": "action",
  "action": "stripe.listBalanceTransactions",
  "config": {
    "payout": "{{variables.loop.item.source}}",
    "limit": 100
  }
}

Each iteration's output is that payout's list. A payout with more than 100 transactions comes back with has_more: true; page it with startingAfter before relying on the totals.

Webhook Triggers

Stripe webhooks trigger workflows on payment events.

{
  "trigger": {
    "type": "webhook",
    "provider": "stripe",
    "events": ["invoice.paid", "customer.subscription.updated"]
  }
}

Common Events:

EventDescription
invoice.paidInvoice was paid
invoice.payment_failedPayment failed
customer.subscription.createdNew subscription
customer.subscription.updatedSubscription changed
customer.subscription.deletedSubscription cancelled
payment_intent.succeededPayment completed
payment_intent.payment_failedPayment failed
charge.refundedCharge refunded

Example Workflow

Invoice and payment workflow:

{
  "name": "Create and Send Invoice",
  "trigger": {
    "type": "api"
  },
  "steps": [
    {
      "id": "get-or-create-customer",
      "type": "action",
      "action": "stripe.listCustomers",
      "config": {
        "email": "{{input.email}}",
        "limit": 1
      }
    },
    {
      "id": "check-customer",
      "type": "condition",
      "config": {
        "conditions": {
          "left": "{{steps.get-or-create-customer.output.data.length}}",
          "operator": "gt",
          "right": 0
        },
        "then": ["create-invoice"],
        "else": ["create-customer"]
      }
    },
    {
      "id": "create-customer",
      "type": "action",
      "action": "stripe.createCustomer",
      "config": {
        "email": "{{input.email}}",
        "name": "{{input.name}}"
      }
    },
    {
      "id": "create-invoice",
      "type": "action",
      "action": "stripe.createInvoice",
      "config": {
        "customerId": "{{steps.get-or-create-customer.output.data[0].id || steps.create-customer.output.id}}",
        "autoAdvance": false,
        "collectionMethod": "send_invoice",
        "daysUntilDue": 30
      }
    },
    {
      "id": "add-line-item",
      "type": "action",
      "action": "stripe.createInvoiceItem",
      "config": {
        "customerId": "{{steps.get-or-create-customer.output.data[0].id || steps.create-customer.output.id}}",
        "invoiceId": "{{steps.create-invoice.output.id}}",
        "amount": "{{input.amount}}",
        "currency": "usd",
        "description": "{{input.description}}"
      }
    },
    {
      "id": "finalize",
      "type": "action",
      "action": "stripe.finalizeInvoice",
      "config": {
        "invoiceId": "{{steps.create-invoice.output.id}}"
      }
    },
    {
      "id": "send",
      "type": "action",
      "action": "stripe.sendInvoice",
      "config": {
        "invoiceId": "{{steps.create-invoice.output.id}}"
      }
    }
  ]
}

Rate Limits

LimitValue
API calls100/second (read)
API calls100/second (write)
Webhook deliveryUp to 5 retries

Troubleshooting

Common Errors

ErrorCauseSolution
card_declinedCard was declinedCustomer should use different card
invalid_request_errorInvalid parametersCheck required fields
rate_limit_errorToo many requestsImplement backoff
authentication_errorInvalid API keyCheck connection status

On this page