Payments

Sell one-time purchases and subscriptions with Dodo Payments. A purchase gives the buyer a plan (and, if you want, prepaid credits) on their account, which your pages can check.

What it is

Payments connects Arc CMS to Dodo Payments, the only payment provider Arc CMS supports. Dodo hosts the checkout page and takes the money; Arc CMS keeps the product list, records every payment, and writes the buyer's plan onto their account. It gives you:

How an app checks what a person has paid for is on its own page: Premium entitlements. For when to use payments in an app, see Add payments and premium features.

Turn it on or off

Payments is a switchable feature, id payments, on by default. It needs no other feature. To turn it off, list it in src/custom/features.ts:

export const CUSTOM_FEATURES: FeatureChoice = {
    off: ['payments'],
};

When it is off:

Data already in Firestore stays. The full list of feature ids is in Feature ids.

Set it up

  1. In Dodo: create your products (one-time or subscription). For each pricing tier you plan to offer, create a discount code. A code's own usage limit is what caps a tier.

  2. Deploy the functions (see Deploy only what changed). The webhook receiver is deployed as arccms-dodoWebhook.

  3. In Dodo, add a webhook whose URL is the deployed arccms-dodoWebhook function (its URL is in the Firebase console, Functions). Copy the webhook's signing secret.

  4. In Arc CMS, open Settings, Payments and fill in:

    FieldWhat to enter
    Enable Dodo PaymentsOn. With it off, checkout and test links fail.
    ModeTest while you try it, Live for real sales.
    Test API Key, Live API KeyFrom Dodo. The key for the selected mode is used.
    Webhook Signing SecretThe secret from the step before.
    Success / Return URLhttps://your-site/checkout/success
    Cancel URLhttps://your-site/checkout/cancel

    Save, then press Test Connection. Saved keys and the secret show as dots; they are only replaced when you type a new value.

  5. Open Products, New Product for each Dodo product: its name, the Dodo Product ID (prod_...), the type, and the plan it grants (Premium type such as gold, and Tier rank, higher means more access). Mark it Active to show it on the pricing page. The other fields are below.

  6. Try it in Test mode. Buy from /pricing with a test card, then check Transactions and the buyer's /account page.

  7. Go live: switch Mode to Live and enter the Live API Key. Settings holds one signing secret, so it must be the secret of the webhook that sends the events of the mode you use.

Product fields

FieldMeaning
TypeOne-time (lifetime access) or Subscription.
Interval, Trial daysSubscriptions only. The trial is passed to Dodo at checkout.
Free-updates yearsOne-time only. Access never ends; the buyer's updatesUntil date is set this far ahead, and a reminder email goes out 14 days before it.
Credits grantedCredits added for each successful charge: once for a one-time purchase, again on every subscription renewal. 0 means none.
Premium type, Tier rankThe plan written on the buyer's account. See Premium entitlements.
List price, Currency, FeaturesShown on the pricing page only. Dodo charges its own price.
Pricing tiersOptional headcount pricing, below.

Pricing tiers

Tiers give the first buyers a better price, such as "first 100 buyers get 40% off". Each tier has a label, a limit (the cumulative number of buyers it covers), a Dodo discount code, and a display percentage and price. A new buyer gets the first tier whose limit is higher than the number of confirmed buyers so far; a limit of 0 means "everyone else" and belongs last. The code is applied at checkout. Only a first purchase or a first subscription activation counts as a buyer; renewals do not.

Each tier has a Test payment link: a real Dodo checkout with that tier's code, so you can check the price. A purchase through it is marked test in Transactions and grants no plan, no credits, no email, and does not count as a buyer.

A subscription tier keeps its discount on renewals only when the Dodo discount code is set up as recurring. A one-time code discounts the first payment and the buyer pays full price from the first renewal.

Payment emails

Settings, Payments, Payment Emails has four emails you can switch on or off and edit: Payment succeeded, Payment / renewal failed, Subscription lifecycle (trial started, past due, on hold, cancelled, expired, refunded) and Trial ending soon (sent once when a trial ends within 3 days). A test send must be confirmed before a template can be saved. The free-updates reminder (updates_ending_email) is edited in the email composer. They go through the email engine: see Email engine and Email merge tags.

Use it

What a buyer does

  1. Opens /pricing, which lists the active products.

  2. Presses buy. A visitor who is not signed in is sent to sign in first and comes back to /pricing. The createCheckoutSession function picks the pricing tier, and the browser goes to Dodo's checkout.

  3. Pays. Dodo sends them to the Success URL. /checkout/success waits for the webhook to grant the plan (checking every 3 seconds, for about 45 seconds), then links to /account.

Members see their plan, credit balance, recent activity and full transaction history on /user/payments and /account. /user/premium opens only for a paying member; others are sent to /pricing.

What each Dodo event does

EventEffect
payment.succeeded, subscription.active, subscription.renewedRecords the transaction, grants or renews the plan, grants credits, adds the buyer to the all-customers list (with the audience feature), notifies the member and the admins. The receipt email is sent for payment.succeeded only.
payment.failedRecords it, sends the failure email, notifies the member and the admins.
subscription.failed, subscription.on_holdMarks the plan past_due (access stays) and sends the lifecycle email.
subscription.cancelled, subscription.expiredEnds the plan and sends the lifecycle email.
refund.succeededRecords the refund and sends the lifecycle email. A full refund also ends the plan and takes back the credits that charge granted; a partial refund leaves the plan and credits as they are.

Every step is safe to repeat, so a webhook Dodo sends twice is counted once. Events that arrive out of order never undo a newer one.

Every day

FunctionWhen (UTC)What
scanExpiredEntitlements08:00Ends any subscription whose paid period ended more than 3 days ago, in case the cancel or expiry webhook never came.
scanTrialEndings09:00Sends Trial ending soon to trials that end in the next 3 days, once each.
scanUpdatesEnding09:30Sends the free-updates reminder 14 days before updatesUntil, once each.

Extend it

Another payment provider cannot be plugged in from an app: the records are provider-neutral, but only Dodo is wired up.

What it stores

WhereWhat
Settings/dodo-paymentsOn or off, mode, API keys, webhook secret, success and cancel URLs.
ProductsYour products, tiers and purchaseCount (confirmed buyers).
TransactionsOne document per payment event: buyer uid (userId) and email, product, plan, amount, currency, status (succeeded, failed, refunded, pending), tier and code, and isTest for test links.
WebhookEventsEvery verified webhook exactly as Dodo sent it, keyed by its webhook-id, with processed, processingError and processingAttempts.
CreditLedgerEvery credit change (purchase, renewal, refund, consume), with the balance after it.
CountedBuyersOne marker per counted buyer, so a buyer is counted once.
users/{docId}The plan and credit fields: see Premium entitlements.
EmailTemplateThe payment email templates.

Payments stores no files. Its Cloud Functions, built only when the feature is on: createCheckoutSession, createTestCheckoutLink, testDodoConnection, consumeCredits (callables), dodoWebhook (HTTP), handlePaymentEvent (runs on each new WebhookEvents document), and the three daily scans above. All are listed in Cloud Functions; the collections in Data model.

Who can do what

Data or actionWho
Read productsAnyone (the pricing page is public)
Create, edit, delete products; payment settingsAdmins
Read transactions and credit ledger entriesAdmins, and the person they belong to
Read webhook eventsAdmins
Write transactions, ledger, webhook eventsCloud Functions only
Start a checkout, spend own creditsAny signed-in person
Test payment link, Test ConnectionAdmins

People cannot write their own plan or credit fields: the rules refuse it. See Roles and claims.

Troubleshooting

SymptomCause and fix
Dodo shows the webhook failing with 401The signing secret in Settings, Payments does not match the webhook's. Paste it again.
Dodo shows 500 "Webhook not configured"No signing secret is saved. Paste it in Settings, Payments and save.
Dodo shows 500 "Webhook configuration unavailable"The payment settings were never saved, or there is no API key for the selected Mode. Fill them in and save.
Paid, but the plan never arrivedOpen the event in WebhookEvents: processingError says why. After 5 failed tries it is marked processingFailedPermanently. The buyer is found by the uid sent at checkout, then by email; the product by its id in the checkout metadata, so a deleted product grants nothing.
Pressing buy on /pricing does nothingThe page shows no message; the browser console has the reason. "Your account has no email address": checkout needs an email for Dodo, so a person who signed up with a phone number adds an email under Profile, Sign-in methods. "Product is not linked to a Dodo product": give the product its Dodo Product ID in Products. "Failed to create checkout session.": check that Enable Dodo Payments is on and the key for the selected mode is saved (Test Connection).
/checkout/success shows "Payment received" instead of "You're all set!"The webhook took longer than about 45 seconds, or failed. Check again starts checking again; the plan appears on /account once the event is processed.
A test purchase gave no accessPurchases through a Test payment link never do. Buy from /pricing in Test mode instead.

Never grant access from the checkout return URL. Its query parameters are not proof of payment; only the webhook writes the plan.