Install
openclaw skills install @gaolfun/stripe-paymentIntegrate with Stripe to manage customers, create payments and subscriptions, handle invoices and refunds, and retrieve balances via Stripe API.
openclaw skills install @gaolfun/stripe-paymentName: stripe-payment Description: Enables AI assistants to interact with the Stripe payment platform — manage customers, payments, subscriptions, invoices, refunds, and balance queries via the Stripe REST API. Language: English Version: 1.0.0 Author: OpenClaw / CCD Platform: OpenClaw (ClawHub compatible)
Users activate this skill with natural English sentences such as:
This skill covers the following Stripe operations:
sk_test_ keys safely without live data impactBefore using this skill, the user must provide:
| Item | Description | Format |
|---|---|---|
STRIPE_SECRET_KEY | Stripe Secret API key (server-side only) | sk_test_... or sk_live_... |
STRIPE_WEBHOOK_SECRET | Webhook signing secret (whsec_...) | Only for webhook verification |
CURRENCY | Default currency for charges (optional, defaults to usd) | ISO 4217 e.g., usd, eur |
For production, prefer a Restricted Key (rk_live_...) with minimum required permissions:
customers: Read/Writepayment_intents: Read/Writesubscriptions: Read/Writeinvoices: Readrefunds: Writebalance: Read⚠️ Never expose Secret Keys client-side. This skill operates server-side via
execcalls tocurlor Stripe SDK commands.
The skill expects environment variables or a .env file:
export STRIPE_SECRET_KEY="***"
export STRIPE_WEBHOOK_SECRET="***"
export STRIPE_CURRENCY="usd"
Purpose: Register a new customer in Stripe before charging them.
API: POST /v1/customers
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
curl https://api.stripe.com/v1/customers \
-u "${STRIPE_SECRET_KEY}:" \
-d email="alice@example.com" \
-d name="Alice Smith" \
-d metadata[user_id]="12345"
Stripe SDK (Node.js):
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const customer = await stripe.customers.create({
email: 'alice@example.com',
name: 'Alice Smith',
metadata: { user_id: '12345' }
});
console.log(customer.id); // cus_xxx
Stripe SDK (Python):
import stripe
stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
customer = stripe.Customer.create(
email='alice@example.com',
name='Alice Smith',
metadata={'user_id': '12345'}
)
print(customer.id) # cus_xxx
Response fields to extract:
id — Customer ID (e.g., cus_NqM7xK9vL2)email, name, metadatacreated — Unix timestampPurpose: Initiate a one-time payment. Returns a client_secret for frontend collection.
API: POST /v1/payment_intents
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
curl https://api.stripe.com/v1/payment_intents \
-u "${STRIPE_SECRET_KEY}:" \
-d amount=5000 \
-d currency="usd" \
-d customer="cus_NqM7xK9vL2" \
-d metadata[order_id]="order_789"
Note:
amountis in smallest currency unit (cents for USD). $50.00 =5000.
Stripe SDK (Node.js):
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
customer: 'cus_NqM7xK9vL2',
metadata: { order_id: 'order_789' }
});
console.log(paymentIntent.client_secret);
// Send client_secret to frontend to complete payment with Stripe.js
Key statuses: requires_payment_method, requires_confirmation, requires_action, processing, succeeded, canceled
Purpose: Confirm that a payment has been authorized.
API: POST /v1/payment_intents/{id}/confirm
curl:
curl https://api.stripe.com/v1/payment_intents/pi_3Qx9vL2eZvKYlo2C/confirm \
-u "${STRIPE_SECRET_KEY}:" \
-d payment_method="pm_card_visa"
Purpose: Capture funds that were previously authorized (for manual capture workflows).
API: POST /v1/payment_intents/{id}/capture
curl:
curl https://api.stripe.com/v1/payment_intents/pi_3Qx9vL2eZvKYlo2C/capture \
-u "${STRIPE_SECRET_KEY}:"
Note: uncaptured PaymentIntents expire after 7 days.
Purpose: Set up recurring billing for a customer.
API: POST /v1/subscriptions
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
Prerequisites:
curl:
curl https://api.stripe.com/v1/subscriptions \
-u "${STRIPE_SECRET_KEY}:" \
-d customer="cus_NqM7xK9vL2" \
-d items[0][price]="price_1Qx9vL2eZvKYlo2C1234"
Stripe SDK (Node.js):
const subscription = await stripe.subscriptions.create({
customer: 'cus_NqM7xK9vL2',
items: [{ price: 'price_1Qx9vL2eZvKYlo2C1234' }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent']
});
console.log(subscription.id); // sub_xxx
const clientSecret = subscription.latest_invoice.payment_intent.client_secret;
Key subscription statuses: trialing, active, past_due, canceled, paused
Common subscription parameters:
billing_cycle_anchor — manually set billing datetrial_period_days — start with a free trialcancel_at_period_end — don't renew, cancel at period endAPI: DELETE /v1/subscriptions/{id}
curl:
curl https://api.stripe.com/v1/subscriptions/sub_3Qx9vL2eZvKYlo2C \
-u "${STRIPE_SECRET_KEY}:" \
-X DELETE
To cancel at period end (not immediately):
curl https://api.stripe.com/v1/subscriptions/sub_3Qx9vL2eZvKYlo2C \
-u "${STRIPE_SECRET_KEY}:" \
-d cancel_at_period_end=true
API: GET /v1/invoices (with optional customer filter)
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
# All invoices
curl "https://api.stripe.com/v1/invoices?limit=10" \
-u "${STRIPE_SECRET_KEY}:"
# Invoices for a specific customer
curl "https://api.stripe.com/v1/invoices?customer=cus_NqM7xK9vL2&limit=10" \
-u "${STRIPE_SECRET_KEY}:"
Stripe SDK (Node.js):
const invoices = await stripe.invoices.list({
customer: 'cus_NqM7xK9vL2',
limit: 10
});
for (const invoice of invoices.data) {
console.log(`${invoice.id} — ${invoice.amount_paid / 100} ${invoice.currency} — ${invoice.status}`);
}
Key invoice fields: id, customer, amount_due, amount_paid, status, due_date, period_start, period_end, invoice_pdf
API: POST /v1/refunds
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
Full refund by charge ID:
curl https://api.stripe.com/v1/refunds \
-u "${STRIPE_SECRET_KEY}:" \
-d charge="ch_3Qx9vL2eZvKYlo2C1234"
Partial refund by payment intent ID:
curl https://api.stripe.com/v1/refunds \
-u "${STRIPE_SECRET_KEY}:" \
-d payment_intent="pi_3Qx9vL2eZvKYlo2C" \
-d amount=2500 # partial amount in cents
Refund statuses: pending, succeeded, failed, canceled
API: GET /v1/balance
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
curl https://api.stripe.com/v1/balance \
-u "${STRIPE_SECRET_KEY}:"
Stripe SDK (Node.js):
const balance = await stripe.balance.retrieve();
console.log(`Available: ${balance.available[0].amount / 100} ${balance.available[0].currency}`);
console.log(`Pending: ${balance.pending[0].amount / 100} ${balance.pending[0].currency}`);
Response includes:
available — funds ready to pay outpending — funds not yet availableamount, currency, source breakdownAPI: GET /v1/charges
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
curl "https://api.stripe.com/v1/charges?limit=20&expand[]=data.payment_intent" \
-u "${STRIPE_SECRET_KEY}:"
Stripe SDK (Node.js):
const charges = await stripe.charges.list({ limit: 20 });
for (const charge of charges.data) {
const emoji = charge.paid ? '✅' : '❌';
console.log(`${emoji} ${charge.id} — ${charge.amount / 100} ${charge.currency} — ${charge.description || 'no description'}`);
}
API: GET /v1/payment_intents/{id}
Auth: Authorization: Bearer {STRIPE_SECRET_KEY}
curl:
curl "https://api.stripe.com/v1/payment_intents/pi_3Qx9vL2eZvKYlo2C" \
-u "${STRIPE_SECRET_KEY}:"
Stripe SDK (Node.js):
const pi = await stripe.paymentIntents.retrieve('pi_3Qx9vL2eZvKYlo2C');
console.log(`Status: ${pi.status}`);
console.log(`Amount: ${pi.amount / 100} ${pi.currency}`);
console.log(`Customer: ${pi.customer}`);
Purpose: Verify that incoming webhook requests are genuinely from Stripe.
Concept: Stripe signs every webhook with Stripe-Signature header using HMAC-SHA256 and your STRIPE_WEBHOOK_SECRET (whsec_...).
Node.js verification example:
const express = require('express');
const app = express();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
const pi = event.data.object;
console.log(`PaymentIntent succeeded: ${pi.id}`);
break;
case 'customer.subscription.created':
console.log('New subscription created');
break;
}
res.json({ received: true });
});
Important: Use express.raw() for the webhook route — not express.json() — because Stripe sends raw body for signature verification.
✅ Customer created in Stripe
🆔 Customer ID: cus_NqM7xK9vL2
📧 Email: alice@example.com
👤 Name: Alice Smith
🏷️ Metadata: { user_id: "12345" }
🕐 Created: 2026-07-04 12:58:00 UTC
✅ Payment Intent created
🆔 ID: pi_3Qx9vL2eZvKYlo2C
💵 Amount: $50.00 USD
📊 Status: requires_confirmation
🔑 Client Secret: pi_3Qx9vL2eZvKYlo2C_secret_xxxxx
👤 Customer: cus_NqM7xK9vL2
ℹ️ Next step: Confirm this Payment Intent with Stripe.js on your frontend
✅ Subscription created
🆔 Subscription ID: sub_3Qx9vL2eZvKYlo2C
👤 Customer: cus_NqM7xK9vL2
💰 Price: $29.00/month
📊 Status: active
🗓️ Current period: 2026-07-04 → 2026-08-04
✅ Refund issued
🆔 Refund ID: re_3Qx9vL2eZvKYlo2C
💵 Amount refunded: $25.00 USD
🔗 Charge ID: ch_3Qx9vL2eZvKYlo2C1234
📊 Status: succeeded
✅ Stripe Balance
💰 Available: $1,234.56 USD
⏳ Pending: $567.89 USD
📅 Updated: 2026-07-04 12:58:00 UTC
❌ Failed to retrieve customer
Reason: No such customer: cus_nonexistent
Fix: Verify the customer ID is correct and belongs to your Stripe account
❌ Payment failed
Reason: Your card was declined. (In test mode, use card 4000000000000002 to trigger insufficient_funds)
Fix: Use Stripe test card 4000000000000002 to simulate this scenario
User: "I need to set up a new customer and start them on the $29/month plan"
AI: "Sure, creating the customer and subscription now..."
Execute:
price_1Qx9vL2eZvKYlo2C1234AI: "✅ All set! 👤 Customer: Alice Smith (cus_NqM7xK9vL2) 💰 Subscription: $29.00/month 📊 Status: active 🗓️ Next billing: 2026-08-04"
User: "Show me my Stripe balance and any failed payments in the last 7 days"
AI: "Checking your balance and payment status..."
Execute:
/v1/balance/v1/charges?limit=100 and filter for paid: falseAI: "✅ Stripe Account Status 💰 Available: $1,234.56 USD ⏳ Pending: $567.89 USD
❌ Failed Payments (3 found):
User: "Issue a $15 refund for order pi_3Qx9vL2eZvKYlo2C — the customer only received part of their order"
AI: "Processing a $15.00 partial refund for that payment intent..."
Execute:
/v1/refunds with payment_intent=pi_... and amount=1500AI: "✅ Partial refund issued 💵 Amount refunded: $15.00 USD 🔗 Payment Intent: pi_3Qx9vL2eZvKYlo2C 📊 Refund status: succeeded 🕐 Processed: 2026-07-04 12:59:00 UTC ℹ️ Note: Original payment was $50.00 — $15.00 remaining"
sk_test_) — for development, no real money movessk_live_) — real transactionssk_test_ or sk_live_)⚠️ Keep Test Mode ON while developing. Toggle to Live Mode only when ready for real payments.
Add to ~/.openclaw/.env:
STRIPE_SECRET_KEY="sk_test_51Nx..."
STRIPE_WEBHOOK_SECRET="whsec_..." # Optional, only for webhook verification
STRIPE_CURRENCY="usd" # Optional, defaults to usd
Restart OpenClaw to load environment variables.
Before creating subscriptions, you need at least one Price in Stripe:
Via Dashboard:
price_id (starts with price_)Via API:
# Create product
curl https://api.stripe.com/v1/products \
-u "${STRIPE_SECRET_KEY}:" \
-d name="Pro Plan"
# Create recurring price
curl https://api.stripe.com/v1/prices \
-u "${STRIPE_SECRET_KEY}:" \
-d product="prod_xxx" \
-d unit_amount=2900 \
-d currency="usd" \
-d recurring[interval]="month"
To test webhooks locally:
stripe loginstripe listen --forward-to localhost:3000/webhook/stripe
whsec_... secret from output into STRIPE_WEBHOOK_SECRETTry these commands to verify everything works:
sk_test_) keys: simulate all operations without real moneysk_live_) keys: real charges, real refunds, real settlementssk_test_ before going live| Scenario | Card Number |
|---|---|
| Successful payment | 4242 4242 4242 4242 |
| Insufficient funds | 4000 0000 0000 0002 |
| Card declined | 4000 0000 0000 0002 |
| Expired card | 4000 0000 0000 0069 |
| Incorrect CVC | 4000 0000 0000 0127 |
50001000 (no decimal)POST /v1/balance (retrieve): 25/second429 Too Many Requests with Retry-After header4242)payment_method) insteadclient_secretclient_secret + Stripe.js to collect cardpayment_intent.succeeded fires to your serverThis skill supports steps 1, 3, and 5 server-side. Frontend Stripe.js integration requires separate HTML/JS (outside scope of this skill).
stripe.customers.delete() actually archives, not deletesIdempotency-Key: <unique-string> header when retrying POST requestscurrency explicitly — defaults vary by API call