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.
| Method | When to use | What you provide |
|---|---|---|
| OAuth (Stripe Connect) | Default. Authorize in Stripe, nothing to copy, revocable from the Stripe dashboard. | Stripe login |
| API key | When 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:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | No | Customer email |
name | string | No | Customer name |
description | string | No | Description |
phone | string | No | Phone number |
address | object | No | Customer address |
invoicePrefix | string | No | Sent 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. |
metadata | object | No | Key-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:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer to invoice |
autoAdvance | boolean | No | Auto-finalize (default: true) |
collectionMethod | string | No | charge_automatically or send_invoice |
daysUntilDue | number | No | Days until due (for send_invoice) |
description | string | No | Invoice description |
metadata | object | No | Custom 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:
| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | Invoice whose lines to list |
limit | number | No | Page size, 1–100 (Stripe default 10) |
startingAfter | string | No | Line 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:
| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | Finalized invoice to credit (invoice) |
lines | array | Yes | [{ type: "invoice_line_item", invoice_line_item, quantity }] or with amount (cents) instead of quantity |
creditAmount | number | No | Cents credited to the customer balance (credit_amount) |
refundAmount | number | No | Cents refunded to the invoice's charge (refund_amount) |
outOfBandAmount | number | No | Cents credited outside Stripe (out_of_band_amount) |
reason | string | No | duplicate, fraudulent, order_change or product_unsatisfactory |
memo | string | No | Printed on the credit note PDF |
metadata | object | No | String, number or boolean values; keys without [ or ]; null values are skipped |
idempotencyKey | string | Yes (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:
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in cents |
currency | string | Yes | Three-letter currency code |
customerId | string | No | Customer to charge |
description | string | No | Payment description |
paymentMethodTypes | array | No | Allowed payment methods |
paymentMethod | string | No | Specific payment method ID |
captureMethod | string | No | automatic or manual |
confirm | boolean | No | Confirm immediately |
offSession | boolean | No | Off-session payment |
setupFutureUsage | string | No | on_session or off_session |
receiptEmail | string | No | Email for receipt |
statementDescriptor | string | No | Statement descriptor |
metadata | object | No | Custom 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:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
items | array | Yes | Subscription items. An item with price (a price id) and no price_data is sent to Stripe unchanged |
items[].taxRateIds | array | No | Tax rate ids (txr_...) sent as that item's tax_rates; max 10 per item |
defaultPaymentMethod | string | No | Default payment method |
collectionMethod | string | No | charge_automatically or send_invoice |
daysUntilDue | number | No | Days until due (for send_invoice) |
trialPeriodDays | number | No | Trial period in days |
cancelAtPeriodEnd | boolean | No | Cancel at period end |
prorationBehavior | string | No | create_prorations, none, always_invoice |
backdateStartDate | number | No | Past start date in unix seconds (backdate_start_date); a future or fractional value is refused |
billingMode | string | No | Only flexible (billing_mode[type]); any other value is refused before anything is created |
metadata | object | No | Custom 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.
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
phases | array | Yes | Phases 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 |
endBehavior | string | No | release (default, the subscription continues) or cancel |
defaultSettings | object | No | Laid 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 |
metadata | object | No | Custom 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:
| Field | Type | Required | Description |
|---|---|---|---|
subscriptionId | string | Yes | Subscription to update (sub_...) |
items | array | No | Up 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[].taxRateIds | array | No | Tax 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 |
prorationBehavior | string | No | create_prorations (Stripe's default), always_invoice (invoice the proration immediately; requires idempotencyKey) or none; any other value is refused |
prorationDate | number | No | proration_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 |
paymentBehavior | string | No | allow_incomplete, default_incomplete, error_if_incomplete or pending_if_incomplete |
idempotencyKey | string | With always_invoice | Sent 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, billingCycleAnchor | various | No | Sent 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
}
}| Field | Type | Required | Description |
|---|---|---|---|
subscriptionId | string | One target | Sent as subscription; cannot be combined with scheduleId or customerId |
scheduleId | string | One target | Sent as schedule |
customerId | string | One target | Sent as customer |
items | array | No | Same shape as updateSubscription items (taxRateIds becomes tax_rates), sent as subscription_details[items]. Needs subscriptionId |
prorationBehavior | string | No | subscription_details[proration_behavior]: create_prorations, always_invoice or none. Needs subscriptionId |
prorationDate | number | No | subscription_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:
| Field | Type | Required | Description |
|---|---|---|---|
productId | string | No | Only prices of this product (product) |
priceActive | boolean | No | Only active (true) or inactive (false) prices (active); no default |
priceType | string | No | one_time or recurring (type) |
priceCurrency | string | No | Three-letter ISO currency code (currency); blank lists every currency |
lookupKeys | string[] | No | Up to 10 lookup keys (lookup_keys[]), de-duplicated |
priceLimit | number | No | Page size, 1–100 (default 100) (limit) |
startingAfter | string | No | Price 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:
| Field | Type | Required | Description |
|---|---|---|---|
productId | string | Yes | Product the price belongs to (product) |
unitAmountDecimal | string | Yes | Non-negative amount in the smallest currency unit, at most 12 decimals |
currency | string | No | Three-letter ISO currency code (default usd) |
interval | string | No | day, week, month or year (recurring[interval]); blank for one-time |
intervalCount | number | No | Positive integer (recurring[interval_count]); requires interval |
nickname | string | No | Internal label, hidden from customers |
metadata | object | No | Custom metadata |
taxBehavior | string | No | inclusive, exclusive or unspecified (tax_behavior) |
lookupKey | string | No | Unique 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:
| Field | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size, 1–100 (limit) |
startingAfter | string | No | Coupon id to page after (starting_after) |
expandAppliesTo | boolean | No | Include 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:
| Field | Type | Required | Description |
|---|---|---|---|
couponId | string | No | Fixed id (id): letters, digits, _ and -; Stripe generates one when blank |
percentOff | number | One of | Greater than 0 and at most 100, at most 2 decimals (percent_off) |
amountOff | number | One of | Whole cents (amount_off); requires currency |
currency | string | With amountOff | Three-letter ISO currency code |
couponDuration | string | No | once (default), forever or repeating (duration) |
durationInMonths | number | With repeating | Positive integer (duration_in_months); refused with any other duration |
couponName | string | No | Shown on invoices; a string, trimmed, at most 40 characters (name) |
metadata | object | No | Custom 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" }
}
}| Field | Type | Required | Description |
|---|---|---|---|
displayName | string | Yes | Name shown on invoices (display_name) |
percentage | number | Yes | 0–100, at most 4 decimal places (Stripe's precision) |
inclusive | boolean | No | Tax included in the price (default false) |
jurisdiction | string | No | Jurisdiction label |
country | string | No | 2-letter ISO country code |
state | string | No | State or province |
description | string | No | Internal description |
taxType | string | No | Stripe tax_type (sales_tax, vat, gst, ...) |
metadata | object | No | Custom 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"
}
}| Field | Sent as | Notes |
|---|---|---|
type | type | e.g. charge, payout, stripe_fee |
payout | payout | Only the transactions paid out in this automatic payout (po_...). Stripe returns nothing for a manual payout. |
createdFrom | created[gte] | ISO 8601 date or unix seconds, inclusive |
createdTo | created[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:
| Event | Description |
|---|---|
invoice.paid | Invoice was paid |
invoice.payment_failed | Payment failed |
customer.subscription.created | New subscription |
customer.subscription.updated | Subscription changed |
customer.subscription.deleted | Subscription cancelled |
payment_intent.succeeded | Payment completed |
payment_intent.payment_failed | Payment failed |
charge.refunded | Charge 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
| Limit | Value |
|---|---|
| API calls | 100/second (read) |
| API calls | 100/second (write) |
| Webhook delivery | Up to 5 retries |
Troubleshooting
Common Errors
| Error | Cause | Solution |
|---|---|---|
card_declined | Card was declined | Customer should use different card |
invalid_request_error | Invalid parameters | Check required fields |
rate_limit_error | Too many requests | Implement backoff |
authentication_error | Invalid API key | Check connection status |
Loopfour