Stripe API Client for Duso

Complete REST API wrapper for Stripe payments. Supports customers, payment intents, subscriptions, checkout sessions, invoices, refunds, products, prices, and webhook verification.

Installation

The Stripe module is built into Duso. Use require() to import it:

stripe = require("stripe")

To vendor a copy — to patch it, or to pin it against a contrib update — extract it and require it by path:

duso extract contrib/stripe .
stripe = require("./stripe/stripe.du")

The leading ./ is not optional. A bare require("stripe") resolves to the embedded module even when a local copy sits right beside the script, and it does so silently — the vendored file is simply never read. Paths resolve against the directory of the entry script, not the working directory or the file doing the requiring.

Quick Start

stripe = require("stripe")
client = stripe.create_client("sk_test_...")  // or set STRIPE_API_KEY env var

// Create a customer
customer = client.customers.create("[email protected]")
print(customer.id)

// Create a payment intent
intent = client.payment_intents.create(
  5000,        // amount in cents ($50.00)
  "usd",       // currency
  customer.id  // optional customer
)
print(intent.client_secret)

Authentication

Set your Stripe API key via environment variable:

export STRIPE_API_KEY="sk_test_..."
duso script.du

Or pass it directly to create_client():

client = stripe.create_client("sk_test_...")

Note: Always use secret API keys (sk_) in server-side code. Never use publishable keys (pk_) for backend operations.

API Reference

Customers

Create a customer:

customer = client.customers.create(
  "[email protected]",
  "Optional description",
  {custom_key = "value"}  // metadata
)

Get a customer:

customer = client.customers.get("cus_XXXXX")

Update a customer — takes an options object, and passes through any field Stripe accepts:

customer = client.customers.update("cus_XXXXX", {
  email = "[email protected]",
  name = "Acme Inc",
  phone = "+1 555 0100",
  address = {line1 = "1 Main St", city = "Springfield", country = "US"}
})

Sending metadata again replaces the whole object rather than merging into it.

Delete a customer:

client.customers.delete("cus_XXXXX")

List customers:

customers = client.customers.list(10)  // limit=10

Payment Intents

Create a payment intent:

intent = client.payment_intents.create(
  5000,        // amount in cents
  "usd",       // currency
  "cus_XXXXX", // customer_id (optional)
  "Order #123" // description (optional)
)

Get a payment intent:

intent = client.payment_intents.get("pi_XXXXX")

Confirm a payment intent:

intent = client.payment_intents.confirm(
  "pi_XXXXX",
  "pm_XXXXX"  // payment_method
)

List payment intents:

intents = client.payment_intents.list("cus_XXXXX", 10)

Charges (Legacy)

Note: Payment Intents are the modern approach. Use Charges for legacy integrations.

Create a charge:

charge = client.charges.create(
  5000,    // amount in cents
  "usd",   // currency
  "tok_XXXXX"  // source token
)

Get a charge:

charge = client.charges.get("ch_XXXXX")

List charges:

charges = client.charges.list("cus_XXXXX", 10)

Refund a charge:

refund = client.charges.refund("ch_XXXXX", 2500)  // partial refund

Refunds

Create a refund:

refund = client.refunds.create(
  "ch_XXXXX",
  2500,      // amount (optional, full refund if omitted)
  "requested_by_customer"  // reason
)

Get a refund:

refund = client.refunds.get("re_XXXXX")

List refunds:

refunds = client.refunds.list(10, "ch_XXXXX")

Subscriptions

Create a subscription:

subscription = client.subscriptions.create(
  "cus_XXXXX",
  [{price = "price_XXXXX", quantity = 1}],  // items array
  {trial_period_days = 14, metadata = {plan = "byok"}}
)

Options: trial_period_days, metadata, payment_behavior, proration_behavior, default_payment_method, collection_method.

Stripe rejects two items sharing one price on the same subscription. Bill several of the same thing with quantity, or give each one its own subscription.

Get a subscription:

subscription = client.subscriptions.get("sub_XXXXX")

Update a subscription:

subscription = client.subscriptions.update("sub_XXXXX", {
  items = [{price = "price_XXXXX", quantity = 2}],
  proration_behavior = "none"
})

Options: items, metadata, cancel_at_period_end, proration_behavior, default_payment_method, collection_method, trial_end.

Stop a subscription renewing:

subscription = client.subscriptions.cancel_at_period_end("sub_XXXXX")

The customer keeps what they paid for until the period ends, and Stripe moves the status to canceled at the boundary on its own. resume() undoes it while the period is still running:

subscription = client.subscriptions.resume("sub_XXXXX")

Cancel a subscription immediately:

subscription = client.subscriptions.cancel("sub_XXXXX")
subscription = client.subscriptions.cancel("sub_XXXXX", {invoice_now = true})

This ends it mid-period. Reach for cancel_at_period_end unless the subscription really should stop now — this one strands a customer who has already paid through the period.

List subscriptions:

subscriptions = client.subscriptions.list("cus_XXXXX", 10, "active")

Invoices

Get an invoice:

invoice = client.invoices.get("in_XXXXX")

List invoices:

invoices = client.invoices.list("cus_XXXXX", 10, "open")

Finalize an invoice:

invoice = client.invoices.finalize("in_XXXXX")

Pay an invoice:

invoice = client.invoices.pay("in_XXXXX")

Products

Create a product:

product = client.products.create(
  "Premium Plan",
  "Monthly subscription"
)

Get a product:

product = client.products.get("prod_XXXXX")

List products:

products = client.products.list(10)

Prices

Create a price:

price = client.prices.create(
  "prod_XXXXX",
  2999,  // $29.99 in cents
  "usd",
  {interval = "month", interval_count = 1}  // recurring config
)

Get a price:

price = client.prices.get("price_XXXXX")

List prices:

prices = client.prices.list("prod_XXXXX", 10)

Payment Methods

Get a payment method:

pm = client.payment_methods.get("pm_XXXXX")

List payment methods:

pms = client.payment_methods.list("cus_XXXXX", 10)

Checkout Sessions

The hosted payment page. Stripe collects the card on its own domain, so the card never reaches your server.

Create a session:

session = client.checkout.sessions.create({
  mode = "subscription",
  customer = "cus_XXXXX",
  line_items = [{price = "price_XXXXX", quantity = 1}],
  success_url = "https://example.com/done?session={CHECKOUT_SESSION_ID}",
  cancel_url = "https://example.com/cancelled",
  metadata = {order = "1234"}
})
print(session.url)   // send the customer here

mode is "payment", "subscription" or "setup". line_items and success_url are required. {CHECKOUT_SESSION_ID} in success_url is substituted by Stripe. Other options: cancel_url, customer, customer_email, client_reference_id, metadata, subscription_data, allow_promotion_codes.

Metadata on the session comes back on the checkout.session.completed webhook, which is the usual way to know what a payment was for. subscription_data.metadata lands on the subscription instead, where it survives for the life of the subscription.

Get a session:

session = client.checkout.sessions.get("cs_XXXXX")

Check session.payment_status == "paid" before treating it as complete.

Expire / list:

client.checkout.sessions.expire("cs_XXXXX")
sessions = client.checkout.sessions.list(10)

Webhooks

Verifying an inbound event is a pure function of the raw body, the signature header and the endpoint secret, so it does not need an API key and does not live on the client:

event = stripe.webhooks.construct_event(payload, sig_header, secret)
print(event.type)

secret defaults to STRIPE_WEBHOOK_SECRET. A fourth argument sets the replay tolerance in seconds (default 300). Anything wrong — bad signature, missing header, stale timestamp — throws, so a failed verification can never be mistaken for a valid event.

The payload must be the exact bytes Stripe sent. Parsing it to JSON and re-encoding reorders keys and changes whitespace; the signature is over bytes, so a round-tripped body never verifies. In an http_server() handler that means req.body, untouched:

ctx = context()
req = ctx.request()
res = ctx.response()

try
  event = stripe.webhooks.construct_event(
    req.body,
    req.headers["Stripe-Signature"],
    env("STRIPE_WEBHOOK_SECRET")
  )
catch (e)
  res.json({error = "invalid signature"}, 400)
end

res.json({received = true}, 200)

Get the endpoint secret from the Stripe dashboard when you add the endpoint, or from stripe listen when forwarding to a local server.

Error Handling

API errors throw exceptions with descriptive messages:

try
  customer = client.customers.get("invalid_id")
catch (e)
  print("Error: " + e)
end

Examples

Example 1: Create a customer and charge them

stripe = require("stripe")
client = stripe.create_client()

// Create customer
customer = client.customers.create("[email protected]", "Alice Smith")
print("Created customer: " + customer.id)

// Create a charge
charge = client.charges.create(
  5000,      // $50.00
  "usd",
  "tok_visa",
  "Order #001",
  customer.id
)
print("Charge created: " + charge.id + " - Status: " + charge.status)

Example 2: Create a subscription

stripe = require("stripe")
client = stripe.create_client()

// Create product and price
product = client.products.create("Premium Plan")
price = client.prices.create(
  product.id,
  9999,  // $99.99/month
  "usd",
  {interval = "month"}
)

// Create customer
customer = client.customers.create("[email protected]")

// Create subscription
subscription = client.subscriptions.create(
  customer.id,
  [{price = price.id, quantity = 1}],
  30  // 30-day trial
)

print("Subscription created: " + subscription.id)
print("Trial ends: " + subscription.trial_end)

Example 3: List and refund a charge

stripe = require("stripe")
client = stripe.create_client()

// List recent charges
charges = client.charges.list("cus_XXXXX", 1)

if len(charges) > 0 then
  charge = charges[0]
  print("Latest charge: " + charge.id + " - " + charge.amount + " " + charge.currency)

  // Refund half
  refund = client.charges.refund(charge.id, charge.amount / 2)
  print("Refunded: " + refund.id)
end

Example 4: List all active subscriptions

stripe = require("stripe")
client = stripe.create_client()

subscriptions = client.subscriptions.list(nil, 100, "active")

for sub in subscriptions do
  print("Subscription: " + sub.id)
  print("  Customer: " + sub.customer)
  print("  Status: " + sub.status)
  print("  Current period ends: " + sub.current_period_end)
end

API Response Format

All API calls return Stripe objects as-is. Common fields:

See Stripe API documentation for complete object schemas.

Limitations

Testing

Test against a sandbox, never a live account. A sandbox is disposable and its keys die with it:

export STRIPE_API_KEY="sk_test_..."

Use test card numbers on the Checkout page:

Any future expiry, any CVC, any postcode.

To exercise webhooks against a server on localhost, forward them with the Stripe CLI — it prints the endpoint secret to set as STRIPE_WEBHOOK_SECRET:

stripe listen --forward-to localhost:8090/stripe/webhook
stripe trigger checkout.session.completed

See Stripe Testing Documentation for more test cards.