Self-service AWS CodePipeline platform — developers ship compliant CI/CD pipelines in minutes via dashboard, CLI, CDK, or AI prompt, while platform teams enforce policy-as-code guardrails, governance, and per-team isolation.
Pipeline Builder charges through a pluggable billing provider, selected by BILLING_PROVIDER:
BILLING_PROVIDER |
Use it for | Charging model |
|---|---|---|
stub (default) |
Local dev / demos | No real charges — subscriptions are created in-app with no external provider |
stripe |
Direct SaaS billing you own | Stripe Customers + Subscriptions; the app owns plans/prices and reconciles via webhooks |
aws-marketplace |
Selling through AWS Marketplace | Entitlements flow from AWS; add-ons report as metered usage (BatchMeterUsage) |
Billing must be on (BILLING_ENABLED=true, the default) for any provider to serve plans. This page is the setup walkthrough for the two real providers. For what billing does once configured, see Billing Add-on Bundles, Billing Discounts, and the Environment Variables → Billing reference.
One provider per deployment.
BILLING_PROVIDERis global. You do not run Stripe and Marketplace side by side — pick the one that matches how the deployment is sold.
Stripe billing is direct: the app creates a Stripe Customer per organization and a Subscription per plan, using Price objects you create in Stripe. The app owns the reduction logic (discounts are customer-balance credits, never Stripe coupons — see Billing Discounts); Stripe owns the card, the invoice, and the payment. All state changes flow back through a signed webhook.
developer / pro / team / enterprise) — the app’s plan ids, from Environment Variables → Billing.BILLING_ENABLED=true
BILLING_PROVIDER=stripe
Stripe Dashboard → Developers → API keys → copy the Secret key (sk_test_… in test mode, sk_live_… in production):
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY is a secret — provision it via a sealed secret / SSM, never commit it.
In Stripe, create a Product + a recurring Price for every paid plan and every add-on you sell, one Price per interval (monthly and/or annual). The amount you set in Stripe must match the app’s configured price (Stripe is what actually charges the card). Copy each Price id (price_…).
Then wire them all up with STRIPE_PRICE_MAP — a single JSON object whose keys are <id>_<interval> (<id> is a plan id or a bundle id) and whose values are Stripe Price ids. interval is monthly or annual.
| Plan id | Monthly | Annual | Map keys |
|---|---|---|---|
developer |
Free | Free | — (nothing charged) |
pro |
$49 | $490 | pro_monthly, pro_annual |
team |
$149 | $1,490 | team_monthly, team_annual |
enterprise |
$599 | $5,990 | enterprise_monthly, enterprise_annual |
The free developer tier needs no Price; the hidden unlimited tier is never sold. If a customer picks a plan/interval whose key is missing, subscription creation fails fast with No Stripe Price ID configured for plan "…" with interval "…" — so map every paid combination.
Add-ons are charged as extra subscription line items on the same subscription, so each sellable bundle also needs a Stripe Price per interval, keyed <bundleId>_<interval> in the same STRIPE_PRICE_MAP. Annual defaults to ~10× monthly:
| Bundle id | Monthly | Annual | Map keys |
|---|---|---|---|
seat_pack |
$25 | $250 | seat_pack_monthly, seat_pack_annual |
pipeline_pack |
$15 | $150 | pipeline_pack_monthly, pipeline_pack_annual |
plugin_pack |
$15 | $150 | plugin_pack_monthly, plugin_pack_annual |
api_pack |
$20 | $200 | api_pack_monthly, api_pack_annual |
ai_pack |
$75 | $750 | ai_pack_monthly, ai_pack_annual |
storage_pack |
$25 | $250 | storage_pack_monthly, storage_pack_annual |
retention_pack |
$15 | $150 | retention_pack_monthly, retention_pack_annual |
dora_history_pack |
$30 | $300 | dora_history_pack_monthly, dora_history_pack_annual |
audit_log |
$20 | $200 | audit_log_monthly, audit_log_annual |
sso |
$40 | $400 | sso_monthly, sso_annual |
advanced_reporting |
$30 | $300 | advanced_reporting_monthly, advanced_reporting_annual |
team_usage_analytics |
$30 | $300 | team_usage_analytics_monthly, team_usage_analytics_annual |
compliance_standard |
$29.90 | $299 | compliance_standard_monthly, compliance_standard_annual |
compliance_advanced |
$99.90 | $999 | compliance_advanced_monthly, compliance_advanced_annual |
If a purchased bundle’s
<bundleId>_<interval>key is absent from the map, its line item is silently skipped — the customer gets the entitlement but is never charged for it. Map every bundle you enable (seeBILLING_BUNDLES_ENABLED), only for the intervals you sell.
STRIPE_PRICE_MAP='{
"pro_monthly":"price_1AbcPro","pro_annual":"price_1AbcProYr",
"team_monthly":"price_1DefTeam","team_annual":"price_1DefTeamYr",
"enterprise_monthly":"price_1GhiEnt","enterprise_annual":"price_1GhiEntYr",
"seat_pack_monthly":"price_1JklSeat","seat_pack_annual":"price_1JklSeatYr",
"sso_monthly":"price_1MnoSso","sso_annual":"price_1MnoSsoYr"
}'
(Provide it as a single line — expanded here only for readability. Extend it with the remaining bundle keys from the table for every add-on you sell.)
The app reconciles all subscription and payment state from Stripe webhooks. The endpoint is:
POST https://<your-public-host>/billing/stripe/webhook
Stripe Dashboard → Developers → Webhooks → Add endpoint → enter that URL, then select the events the app consumes:
| Event | Effect in Pipeline Builder |
|---|---|
customer.subscription.created |
Records the subscription + grants the tier |
customer.subscription.updated |
Re-syncs status/plan (status mapped internally; unpaid ⇒ canceled after grace) |
customer.subscription.deleted |
Cancels the subscription, downgrades tier |
invoice.payment_succeeded |
Marks paid / clears past-due |
invoice.payment_failed |
Moves to past-due (grace period applies) |
invoice.upcoming |
Drives renewal reminders + recurring-credit re-grant |
charge.refunded |
Reverses the matching subscription |
charge.dispute.created |
Reverses on dispute |
invoice.voided / invoice.marked_uncollectible |
Reversal handling |
Copy the endpoint’s Signing secret (whsec_…) into:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
The webhook route verifies every delivery against this secret over the raw request body and de-dupes redeliveries — so the URL must be publicly reachable through the nginx gateway, and STRIPE_WEBHOOK_SECRET must match the endpoint exactly or deliveries are rejected.
stripe login
# Forward live test events to your local gateway and print a whsec_ to use as STRIPE_WEBHOOK_SECRET:
stripe listen --forward-to https://localhost:8443/billing/stripe/webhook
# In another shell, simulate the lifecycle:
stripe trigger customer.subscription.created
stripe trigger invoice.payment_succeeded
Confirm the subscription appears (GET /billing/subscription for the org) and the tier is granted.
Swap test → live everywhere: STRIPE_SECRET_KEY=sk_live_…, a live-mode webhook endpoint with its own STRIPE_WEBHOOK_SECRET, and live Price ids in STRIPE_PRICE_MAP. Test-mode and live-mode objects never interoperate.
| Variable | Default | Description |
|---|---|---|
STRIPE_SECRET_KEY |
— | Secret. Stripe API secret key (sk_test_… / sk_live_…). Required when BILLING_PROVIDER=stripe |
STRIPE_WEBHOOK_SECRET |
— | Secret. Signing secret (whsec_…) for the endpoint at POST /billing/stripe/webhook; every delivery is signature-verified against it |
STRIPE_PRICE_MAP |
{} |
JSON map of <id>_<interval> → Stripe Price id, where <id> is a plan id or a bundle id (e.g. {"pro_monthly":"price_…","seat_pack_annual":"price_…"}). A plan/interval absent here cannot be subscribed; a bundle absent here is granted but not charged |
Stripe subscription statuses are mapped to internal statuses by a fixed table in the app (no env var); notably unpaid ⇒ canceled (Stripe sets unpaid only after the grace period), and unknown statuses fall back to incomplete.
Under AWS Marketplace the customer subscribes on AWS, not in-app. AWS owns the contract and the bill; Pipeline Builder resolves the customer on redirect, reads entitlements to set the tier, listens to SNS for lifecycle changes, and reports add-on consumption as metered usage. Self-service in-app bundle purchase is disabled — entitlements flow from AWS.
In the AWS Marketplace Management Portal, create the SaaS product and define its dimensions:
pro / team / enterprise).Note the product code AWS assigns.
BILLING_ENABLED=true
BILLING_PROVIDER=aws-marketplace
AWS_MARKETPLACE_PRODUCT_CODE=<your-product-code>
AWS_MARKETPLACE_REGION=us-east-1 # defaults to AWS_REGION, else us-east-1
Set the product’s Fulfillment URL to a page in your frontend that captures the x-amzn-marketplace-token field (AWS POSTs it as a form field on redirect) and forwards it to the backend registration endpoint:
POST https://<your-public-host>/billing/marketplace/resolve
Body: { "x-amzn-marketplace-token": "<token>" } (also accepts { "token": … })
The endpoint (no auth — it’s the AWS redirect target) runs the standard SaaS flow: ResolveCustomer on the token → look up any existing active subscription → GetEntitlements to determine the entitled tier → create the subscription. An entitlement with no in-map dimension falls back to developer.
AWS Marketplace publishes entitlement/subscription notifications to an SNS topic you own. Set its ARN and subscribe the app’s SNS webhook (HTTPS) to the topic:
AWS_MARKETPLACE_SNS_TOPIC_ARN=arn:aws:sns:us-east-1:<acct>:aws-mp-subscription-notification-<code>
POST https://<your-public-host>/billing/marketplace/sns
The endpoint confirms the SNS SubscriptionConfirmation handshake and verifies message signatures, then processes entitlement updates, cancellations, and reactivations (re-checking entitlements and updating the plan).
The billing service’s task role needs the Marketplace APIs it calls:
{
"Effect": "Allow",
"Action": [
"aws-marketplace:ResolveCustomer",
"aws-marketplace:GetEntitlements",
"aws-marketplace:BatchMeterUsage"
],
"Resource": "*"
}
Plus permission to receive from / confirm the SNS subscription for your topic.
Three JSON maps connect AWS dimensions to the app’s plans, add-ons, and prices:
| Variable | Default | Maps | Purpose |
|---|---|---|---|
AWS_MARKETPLACE_DIMENSION_MAP |
identity | Marketplace tier dimension → local plan id | Resolve the entitled tier from GetEntitlements |
AWS_MARKETPLACE_BUNDLE_DIMENSION_MAP |
identity | Add-on bundle id → metered dimension key | Which dimension each add-on reports under |
AWS_MARKETPLACE_DIMENSION_PRICE_MAP |
{} |
Metered dimension → cents per unit per cycle | Drives credit drawdown; an unpriced dimension is reported in full |
Create one AWS entitlement dimension per paid tier. The free developer tier needs no dimension — an entitlement that resolves to nothing falls back to developer. If you name the AWS dimensions to match the plan ids, the identity default applies and you can omit AWS_MARKETPLACE_DIMENSION_MAP entirely; the explicit map is shown for clarity:
| Plan id | Monthly (default) | Suggested AWS dimension | Needs a dimension? |
|---|---|---|---|
developer |
Free | — | No — the fallback tier |
pro |
$49 | pro |
Yes |
team |
$149 | team |
Yes |
enterprise |
$599 | enterprise |
Yes |
# Identity naming → this line is optional (it's the default):
AWS_MARKETPLACE_DIMENSION_MAP='{"pro":"pro","team":"team","enterprise":"enterprise"}'
Create one metered AWS dimension per add-on you sell. Map each bundle id → its AWS dimension name, and give each dimension its per-unit list price (cents) so credit drawdown values a withheld unit correctly. The rows below use every add-on that ships today, at its default monthly price:
| Bundle id | AWS dimension | List price (default) | Available tiers |
|---|---|---|---|
seat_pack |
SeatPack |
$25 (2500) |
team, enterprise |
pipeline_pack |
PipelinePack |
$15 (1500) |
all |
plugin_pack |
PluginPack |
$15 (1500) |
all |
api_pack |
ApiPack |
$20 (2000) |
all |
ai_pack |
AiPack |
$75 (7500) |
all |
storage_pack |
StoragePack |
$25 (2500) |
all |
retention_pack |
RetentionPack |
$15 (1500) |
all (max 7) |
dora_history_pack |
DoraHistoryPack |
$30 (3000) |
all (max 1) |
audit_log |
AuditLog |
$20 (2000) |
pro |
sso |
Sso |
$40 (4000) |
pro |
advanced_reporting |
AdvancedReporting |
$30 (3000) |
developer, pro, team |
team_usage_analytics |
TeamUsageAnalytics |
$30 (3000) |
pro, team |
compliance_standard |
ComplianceStandard |
$29.90 (2990) |
developer, pro, team |
compliance_advanced |
ComplianceAdvanced |
$99.90 (9990) |
developer, pro, team |
AWS_MARKETPLACE_BUNDLE_DIMENSION_MAP='{"seat_pack":"SeatPack","pipeline_pack":"PipelinePack","plugin_pack":"PluginPack","api_pack":"ApiPack","ai_pack":"AiPack","storage_pack":"StoragePack","retention_pack":"RetentionPack","dora_history_pack":"DoraHistoryPack","audit_log":"AuditLog","sso":"Sso","advanced_reporting":"AdvancedReporting","team_usage_analytics":"TeamUsageAnalytics","compliance_standard":"ComplianceStandard","compliance_advanced":"ComplianceAdvanced"}'
AWS_MARKETPLACE_DIMENSION_PRICE_MAP='{"SeatPack":2500,"PipelinePack":1500,"PluginPack":1500,"ApiPack":2000,"AiPack":7500,"StoragePack":2500,"RetentionPack":1500,"DoraHistoryPack":3000,"AuditLog":2000,"Sso":4000,"AdvancedReporting":3000,"TeamUsageAnalytics":3000,"ComplianceStandard":2990,"ComplianceAdvanced":9990}'
Only list the add-ons you actually sell on Marketplace — a bundle with no dimension mapping isn’t metered, and a dimension with no price in AWS_MARKETPLACE_DIMENSION_PRICE_MAP is reported in full (never drawn against for credit). Tier availability (the “Available tiers” column) is enforced separately by BILLING_BUNDLE_<ID>_TIERS.
The price-map values are cents per metered unit per metering cycle (cycle =
BILLING_METERING_INTERVAL_MS). The prices above are the monthly list defaults; if your metering cadence isn’t monthly, scale each value to the cycle. Either way, mirror your AWS listing’s dimension prices exactly — a wrong value directly mis-draws credit.
Add-on charges and usage-credit realization run on the metering cycle. Turn it on, but shadow it first:
BILLING_METERING_ENABLED=true
BILLING_METERING_INTERVAL_MS=3600000 # 1h; AWS BatchMeterUsage dedupes by (customer, dimension, hour)
BILLING_METERING_DRAWDOWN_DRYRUN=true # compute + log intended withholding, report FULL quantities, touch nothing
Watch the logs for a cycle or two, confirm the intended dimensions/quantities match your listing, then set BILLING_METERING_DRAWDOWN_DRYRUN=false to go live.
Metering is default-off, and usage-credit discounts on Marketplace require both
BILLING_DISCOUNTS_ENABLEDandBILLING_METERING_ENABLED— a credit would otherwise bank but never reduce the AWS bill, so it’s rejected. See Billing Discounts → AWS Marketplace.
GET /billing/marketplace/entitlements).BatchMeterUsage dimensions before going live.| Variable | Default | Description |
|---|---|---|
AWS_MARKETPLACE_PRODUCT_CODE |
— | The Marketplace product code |
AWS_MARKETPLACE_REGION |
AWS_REGION or us-east-1 |
Region for the Metering/Entitlement clients |
AWS_MARKETPLACE_SNS_TOPIC_ARN |
— | SNS topic for entitlement/subscription notifications |
AWS_MARKETPLACE_DIMENSION_MAP |
identity | JSON map of Marketplace tier dimension → local plan id |
AWS_MARKETPLACE_BUNDLE_DIMENSION_MAP |
identity | JSON map of add-on bundle id → metered dimension key |
AWS_MARKETPLACE_DIMENSION_PRICE_MAP |
{} |
JSON map of metered dimension → local list price in cents per metered unit per cycle |
BILLING_METERING_ENABLED |
false |
Run the metering cycle (report add-on usage + realize credits). Off = no metering, and Marketplace credits are rejected |
BILLING_METERING_INTERVAL_MS |
3600000 |
Metering cycle cadence (1 hour) |
BILLING_METERING_DRAWDOWN_DRYRUN |
false |
Shadow mode — compute + log intended withholding but report full quantities and leave balances untouched |
| Endpoint | Auth | Provider | Purpose |
|---|---|---|---|
POST /billing/stripe/webhook |
Signature | Stripe | Receives + verifies Stripe events (raw body) |
POST /billing/marketplace/resolve |
None (AWS redirect) | Marketplace | Exchange a registration token for a subscription |
POST /billing/marketplace/sns |
SNS signature | Marketplace | Entitlement/subscription lifecycle notifications |
GET /billing/marketplace/entitlements |
Auth | Marketplace | Current entitlements for the account |