Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.authforge.cc/llms.txt

Use this file to discover all available pages before exploring further.

The Commerce pipeline is AuthForge’s recommended payment integration: a managed license fulfilment backend for your store. You paste your provider credentials in the dashboard, map prices to license templates, and AuthForge verifies inbound webhooks, deduplicates retries, creates portal-ready licenses, and can email the buyer; without you hosting a webhook handler or calling the Developer API for each purchase. Commerce supports Stripe and Lemon Squeezy out of the box; you can connect either or both on the same account and have mappings for each route into your AuthForge apps through the same pipeline. If you must own the webhook endpoint yourself (custom Checkout payloads, unusual bundling, or other cases Commerce does not cover), use Custom Stripe webhooks instead. Both paths issue the same licenses through the same APIs.

What you get

  • One-click Stripe setup: paste your secret key and webhook secret; AuthForge stores them encrypted and starts receiving events.
  • Product → license mapping: declare how each Stripe product or price maps to a license (duration, HWID slots, label, custom variables).
  • Idempotent by design: every inbound webhook is recorded in an event ledger keyed on (provider, externalEventId). Duplicate deliveries never create duplicate licenses.
  • Async processing: webhooks enqueue onto SQS. The worker runs the license action. A DLQ captures poison pills.
  • Portal-ready licenses: the customer’s email is attached to the license automatically, so buyers can immediately sign into the self-service portal.
  • Automatic buyer email: the Commerce worker emails the license key to the buyer as soon as their purchase clears, so you don’t need to wire up fulfilment yourself. Opt out per-app in the Portal Policy panel if you’d rather send it from your own system via the license.created webhook.
  • Event browser: every event that hits your account is visible in the dashboard with provider, status, retry metadata, and linked license.

How it works

Each step is independently observable and retryable. The webhook endpoint is intentionally thin; it persists and acknowledges fast. All the expensive work happens in the worker.

Setup

Commerce lives under the main dashboard nav at Dashboard → Commerce. It is global per-account (not per-app), because one Stripe account typically pays licenses into several AuthForge apps. You map individual Stripe prices to specific apps inside Commerce.

0. Create an AuthForge app

Commerce delivers licenses into an app. If you don’t have one yet, open Dashboard → Applications → New application first, then come back.

1. Connect your Stripe account

AuthForge uses the platform_keys connection model: each merchant supplies their own Stripe credentials, stored encrypted in DynamoDB under a KMS key we never export. You will provide two secrets, both from your own Stripe dashboard.

1a. Register the webhook endpoint in Stripe

On the Commerce page, AuthForge shows you a webhook URL that looks like:
https://api.authforge.cc/billing/webhook/stripe/<your-user-id>
Copy it, then in the Stripe Dashboard:
  1. Go to Developers → Webhooks → Add endpoint.
  2. Paste the URL from AuthForge.
  3. Subscribe to at least checkout.session.completed. Add any of the following you’ll use:
    • invoice.paid; subscription renewals
    • customer.subscription.deleted; subscription cancellations
    • charge.refunded; refunds
    • charge.dispute.created; chargebacks
  4. Save. Stripe now shows a Signing secret that starts with whsec_. Keep that tab open.

1b. Paste the webhook signing secret

Back on the AuthForge Commerce page, paste the whsec_… value from step 1a into the “Stripe webhook signing secret” field.

1c. Paste your Stripe API key

This is a different secret from the signing secret. Get it in Stripe under Developers → API keys:
  • Either your Secret key (sk_live_… or sk_test_…), or
  • A Restricted key (rk_live_…) with read access to Checkout Sessions, Invoices, and Payment Intents: recommended for safety.
Paste it into the “Stripe secret API key” field and click Connect Stripe.
Stripe strips line items from webhook payloads by default (notably for checkout.session.completed, which is every Payment Link purchase). AuthForge calls Stripe’s API with your key to find out which product the customer bought. Without an API key, webhooks will be logged as skipped · api_key_required_for_line_items and no licenses will be created.

2. Map your products to licenses

On the same Commerce page, scroll to Product mappings and click New mapping. One mapping = one Stripe price → one AuthForge app. Each (app, price) pair can map to exactly one license policy; if you try to create a second mapping for the same price in the same app, you’ll get a conflict error; edit the existing row instead.
FieldPurpose
AuthForge applicationThe app whose licenses are generated (pick from your existing apps).
Stripe price IDFind it in Stripe → Product catalogue → open a product → the Prices tab. Looks like price_1N….
Stripe product IDOptional. Used for reporting; matching is done on the price ID.
What kind of product is this?Pick the scenario that matches what you sell; see the table below.
Access length / billing period (days)One-time: blank = perpetual license; a number = fixed term from purchase (renewals never extend). Subscription: days per billing cycle—each renewal extends by this amount. Add-on: days added per purchase.
HWID slotsHow many simultaneous devices per key.
License labelPlain-text tag stored on the license ("Pro Plan", "Lifetime", etc.). Shows in the dashboard and is passed to the SDK.
Respect Stripe quantityIf ON, buying quantity 3 creates 3 licenses.

Pick the right product type

What you sellProduct typeDays field
One-time perpetual license (pay once, never expires)One-time purchase (lifetime or fixed length)Blank
One-time fixed-term license (pay once, expires after N days)One-time purchase (lifetime or fixed length)N (e.g. 90). Use a non-recurring Stripe/Lemon price.
Monthly subscriptionSubscription; auto-renews and extends the license30
Yearly subscriptionSubscription; auto-renews and extends the license365
Top-up SKU that adds 30 more days to an existing licenseAdd-on; extends an existing customer’s license30
A SKU that revokes a customer’s license when bought (rare)Revocation productn/a
One mapping covers the full lifecycle. A subscription mapping creates the license on first purchase, extends it on every renewal, and revokes it on cancellation, refund, or chargeback; all from the same row. You do not need (and cannot create) a second mapping for the same price to handle renewals. One-time mappings create licenses at checkout with your chosen access length; recurring renewal invoices do not extend them—pair with a non-recurring provider price for single-payment SKUs. Subscription mappings extend the license by the billing-period days on each renewal. Renewals (invoice.paid), cancellations (customer.subscription.deleted), refunds (charge.refunded / refund.created), and chargebacks (charge.dispute.created) are dispatched on event type and target the same license that was created at purchase time; AuthForge stores the Stripe customer and subscription IDs on the license so the right license is found even when one customer holds several.

3. Test

Run a test-mode purchase against your Stripe account. Within a few seconds you should see:
  • A new row under Commerce → Events with status processed.
  • A new license in the target app’s Licenses tab, with the customer’s email attached.
  • A license.created webhook (if you have webhooks configured).
  • An email to the buyer’s address with their license key and a link to the self-service portal (unless you’ve opted out under App settings → Self-service portal → Commerce fulfilment).
If the event shows skipped with reason api_key_required_for_line_items, you’re missing step 1c. If it shows no_mapping_for_price, you haven’t mapped that price yet (step 2).
AuthForge has no test/live mode of its own. Stripe’s livemode: false flag is ignored; whatever Stripe sends is processed the same way and the license created is real in either case. Buyer delivery emails fire for both test-mode and live-mode events, so use a throwaway address when testing if you don’t want the notification.

Buyer delivery email

When a purchase completes, the Commerce worker sends the buyer a plain, dark-themed email with:
  • The license key(s) they just bought, in a monospace block.
  • A “Open license portal” CTA that deep-links to portal.authforge.cc.
  • A support contact, if you’ve set one under the Portal Policy panel.
This delivery is best-effort: the license has already been written and the license.created webhook has already fired transactionally before the email is queued. An SES hiccup will log and move on, it won’t put the event back in the queue and won’t cause duplicate keys. If you need guaranteed delivery, subscribe to license.created from your own server.

Turning it off per-app

Some publishers already have their own order-confirmation email and would rather send the license key from there. Open the app in the dashboard → Self-service portal panel → Commerce fulfilment section, toggle “Do not email buyers; I’ll handle fulfilment via webhook”. The license.created webhook still fires, so your own system can take over.

The event ledger

Every inbound webhook is persisted before processing. You can filter it in the dashboard:
  • By provider: stripe or lemon.
  • By event type: checkout.session.completed, customer.subscription.deleted, etc.
  • By status: pending, processed, skipped, failed, or held.
  • By license key: quickly find every event that touched a given license.
The ledger is keyed on (provider, externalEventId), so if Stripe retries a delivery (which it will) AuthForge will recognize the duplicate and skip the license action.

Troubleshooting

The ledger’s status + reason columns are designed so you rarely need to look elsewhere. Match whatever you see in the dashboard against the tables below, then use the one-click Replay action once the root cause is fixed.

Common errors

These show up as skipped or failed rows in the events table, with the reason visible in the row detail.
ReasonWhat it meansFix
no_mapping_for_priceThe event’s price ID has no mapping under your account.Create the mapping in the dashboard, then replay the event.
ambiguous_mapping_multiple_appsThe same price is mapped to more than one app on your account.Delete the duplicate so only one app owns the price, then replay.
api_key_required_for_line_itemsStripe stripped line_items from the webhook payload and we don’t have a Stripe API key to fetch them.Paste the Stripe secret key in the connection panel (step 1c above), then replay.
line_items_not_foundThe API fallback ran but Stripe returned no line items. Typically a non-purchase event (zero-quantity bundle, test artefact). Usually safe to leave.None; informational.
unmapped_event_typeProvider sent an event type Commerce does not currently normalize.None; informational.
no_license_to_extend / no_license_to_revokeA renewal or cancel landed before any licenses existed for the app.Check whether the matching purchase_succeeded event was also missed. If so, that’s the one to fix first.

The held state

If the Commerce worker fails to process an event repeatedly, the retry system will eventually move the event into a held state. Held rows stay in the ledger with a distinct chip colour until you look at them; nothing auto-expires.
Held reasonWhat it means
transient_retry_exhaustedThe error looked transient (throttling, timeout, upstream 5xx). The system auto-replayed once, it failed again, and now it’s waiting for you to decide.
non_transientThe error didn’t match a known transient pattern, so the system never retried it automatically.
unknownThe worker crashed before it could record a structured error.

Replaying an event

Any event in held or failed state shows a Replay action in the events table. Click it, confirm in the modal, and the event is re-enqueued with the stored normalised payload; nothing is re-fetched from the provider, no signatures are re-verified. The row transitions back to pending and then through the normal lifecycle. Most triage looks like:
  1. Open Dashboard → Commerce → Events, filter to Held or Failed.
  2. Read the reason column, fix the root cause (missing mapping, missing API key, etc.).
  3. Click Replay on the row. Done.
If you have a large batch of held rows from the same root cause, fix the cause first and then replay each row; there is no multi-select today, but replay is cheap and safe to click repeatedly.

Customer “right to be forgotten”

When a buyer asks you to delete the personal data you hold about them; for example under GDPR Article 17; you can run the deletion from the dashboard API:
curl -X POST "https://api.authforge.cc/apps/$APP_ID/customers/forget" \
  -H "Authorization: Bearer $DASHBOARD_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customerEmail": "alice@example.com",
    "providerCustomerId": "cus_xxx",
    "reason": "Ticket #1234"
  }'
You must supply at least one of customerEmail or providerCustomerId; provide both if you have them so the lookup widens. The endpoint:
  • finds every license on this app whose email or provider customer id matches, removes those personal fields, and revokes the license so it can no longer authenticate;
  • redacts the matching rows in the inbound commerce event ledger; the row itself is kept (so payment-history audit trails still show that an event happened), but the buyer’s email and customer id are tombstoned out of the payload;
  • writes a dashboard audit log entry capturing who initiated the deletion and how many records were touched. The audit row never stores the email value itself, so the audit log itself remains compliant.
The response gives you a count to forward back to the customer:
{ "ok": true, "licensesRedacted": 1, "ledgerRowsScanned": 42, "ledgerRowsRedacted": 3 }
What the endpoint does not do for you:
  • It does not delete the customer inside Stripe or Lemon Squeezy. Deletion in your payment processor is a separate request you make on your provider’s dashboard or API; payment processors typically retain financial records under a separate legal basis.
  • It does not delete the customer’s self-service portal account. That account is owned by the end user, not by you, so they delete it themselves from the portal.

Portal-ready by default

Licenses created by the Commerce pipeline automatically carry the buyer’s email, which is the credential the end-user portal uses for magic-code authentication. Publishers don’t need to wire anything extra; the customer can go to portal.authforge.cc, enter their license key and email, and manage their own HWID resets. For manually-created licenses (via the dashboard or the Developer API), you can attach an email after the fact; see the portal feature for details.
  • Portal: Self-service surface for end users. Commerce licenses are portal-ready out of the box.
  • Webhooks: Notify your own systems when licenses change. Orthogonal to Commerce.
  • Variables: Values attached per license. Set them in the product mapping so every buyer gets them.
  • Custom Stripe webhooks: Self-hosted webhook + Developer API when Commerce is not enough.