-
-
Notifications
You must be signed in to change notification settings - Fork 122
Payments and Checkout
Daptin does not contain a second payment or subscription subsystem. A payment flow composes the authorities already present:
user_account + api_plan + actions + credential + integration
-> checkout_attempt -> verified provider state -> api_member
The runnable Stripe test-mode example lives in
examples/payment-checkout.
Run it with the latest published Daptin image.
A permitted customer selects a backend-defined plan. Daptin records an owned checkout attempt before contacting the provider, uses a private service-account credential to create the provider session, and activates plan membership only after retrieving and verifying provider state.
Four decisions remain independent:
| Decision | Authority |
|---|---|
| Who is buying | authenticated user_account and checkout row owner |
| Who may start or refresh checkout | world, row, and action permissions |
| Which provider secret may be used | service-user-owned credential
|
| What the customer may consume |
api_plan, api_member, metering, and quotas |
Permission to execute checkout never grants direct credential access. A paid provider session never bypasses Daptin plan membership or resource permission.
Use three named actions rather than trusting a redirect or putting provider logic in a frontend:
-
prepare_checkoutruns as the authenticated buyer. It loads the selectedapi_planrow and creates a customer-ownedcheckout_attemptcontaining the backend price, currency, and provider Price ID. -
begin_checkoutruns on that attempt, switches to a fixed payments service user, resolves that user's credential by a backend-fixed name, and invokes the installed Stripe integration. The attempt reference becomes the provider idempotency and client reference. -
refresh_checkoutretrieves the stored provider session with the same service credential. It verifies the client reference, mode, amount, currency, and paid status before creating onecheckout_entitlementand oneapi_memberowned by the buyer.
The durable attempt exists before the provider call. A retry therefore uses the same provider idempotency key rather than creating an unrelated checkout.
Success and cancellation URLs are navigation. They are not authorization and must not activate a plan. Query parameters, posted status values, customer IDs, and provider session IDs are caller input.
refresh_checkout takes only the Daptin checkout-attempt reference. It reads
the provider session ID from that owned row and retrieves the session through
the installed integration. A mismatch or unavailable provider response fails
closed before membership is created.
Webhooks and scheduled recovery use this same action. A webhook may locate the attempt, but it must not implement a separate entitlement path or treat its payload as sufficient proof.
Create a recurring USD 12.00 Stripe test Price, obtain its Price ID and a test secret key, then:
cd examples/payment-checkout
cp .env.example .env.local
# Fill the passwords, STRIPE_SECRET_KEY, and STRIPE_PRICE_ID.
npm install
docker compose --env-file .env.local up --wait
npm run setupThe first run uploads the schema. Restart Daptin and finish provisioning:
docker compose --env-file .env.local restart daptin
npm run setup
npm run verifyThe scripts provision through Daptin APIs and actions only. They do not write the SQL database directly.
List plans available for new purchases with the ordinary resource query:
curl -G http://localhost:6336/api/api_plan \
-H "Authorization: Bearer $BUYER_TOKEN" \
--data-urlencode 'query=[{"column":"archived_at","operator":"is empty","value":null}]'An operator retires a plan by PATCHing archived_at to a timestamp and restores
it by PATCHing the field to null. Ordinary api_plan reads remain available,
subject to permissions, so administrators and historical relationships can
still resolve retired plans.
After sign-in, prepare an attempt using an available plan reference:
curl -X POST http://localhost:6336/action/api_plan/prepare_checkout \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary '{"attributes":{"api_plan_id":"PLAN_REFERENCE_ID"}}'Use the returned attempt reference to create or recover the provider session:
curl -X POST http://localhost:6336/action/checkout_attempt/begin_checkout \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary '{"attributes":{"checkout_attempt_id":"CHECKOUT_ATTEMPT_REFERENCE_ID"}}'Redirect the browser to the returned checkout_url. After the provider returns,
refresh from provider truth:
curl -X POST http://localhost:6336/action/checkout_attempt/refresh_checkout \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary '{"attributes":{"checkout_attempt_id":"CHECKOUT_ATTEMPT_REFERENCE_ID"}}'The frontend may poll its owned checkout_attempt row or subscribe to resource
events. It must never receive or submit the service user, provider secret,
credential reference, price, currency, provider Price ID, or membership owner.
Both prepare_checkout and begin_checkout read the persisted plan state.
Archival therefore blocks a new attempt and also blocks provider initiation for
an attempt prepared before retirement. A provider session already created may
still be reconciled by refresh_checkout; verified payment history and its
resulting membership retain their relationship to the retired plan.
Retirement prevents work whose initiation check observes the archived state;
provider work already admitted by an action may finish.
Grant customers only the gates required to:
- read permitted
api_planrows and executeprepare_checkout; the action separately requiresarchived_atto benull; - create/read/update/execute their own
checkout_attemptrows; - execute the three wrapper actions.
Set the generated Stripe integration actions to no broad execute permission.
The wrapper definition contains the fixed service user and credential name and
performs SWITCH_USER before resolving the credential and invoking the
integration. Neither value is caller input.
The service credential remains owner-readable by the service user and administrators. Do not add it to an ordinary customer group merely to make checkout work.
See Permissions, Credentials, and Integrations#run-an-integration-as-a-service-account for the individual gates.
The example uses two durable uniqueness boundaries:
- provider session creation uses the checkout-attempt reference as Stripe's
Idempotency-Key; -
checkout_entitlement.checkout_attempt_referenceand provider session ID are unique beforeapi_membercreation.
Successful entitlement creation and membership creation occur in the owning Daptin action transaction. Duplicate refreshes observe the entitlement and do not create another membership. Provider timeouts leave the attempt pending so the same action can retrieve or retry it.
Do not keep a SQL transaction open across an unbounded stream. Checkout create and retrieve calls must use bounded provider timeouts. Long-running recovery belongs in a persisted task that invokes the same refresh action.
Treat status as explicit data. At minimum model:
| Local state | Meaning |
|---|---|
prepared |
attempt is durable; provider session not yet recorded |
provider_created |
hosted checkout URL is available |
open / unpaid
|
provider has not confirmed payment |
complete / paid
|
verified and eligible for terminalization |
expired |
provider session can no longer complete |
cancelled |
application or provider cancelled the attempt |
failed |
bounded provider or validation failure requiring review/retry |
Only the verified complete and paid combination with matching backend data
may create entitlement.
Initial checkout is only the first subscription event. Before production, define separate named actions for renewals, payment failures, subscription cancellation, refunds, and disputes. Each handler must retrieve provider truth, locate the existing Daptin membership through persisted references, and update that membership through normal resource APIs.
Do not infer ongoing entitlement merely from the existence of a historical
successful payment. The current api_member.status and period remain the
durable Daptin authority used by metering.
- Use PostgreSQL 15 for v0.13.14 production. MySQL/MariaDB initialization is a known release blocker; see Release-v0.13.14-Feature-Status.
- Pin Daptin and the provider API contract.
- Protect and rotate the Daptin encryption secret and Stripe key.
- Use provider-restricted credentials where available.
- Configure bounded timeouts and persisted reconciliation tasks.
- Monitor pending/failed attempts and held metering reservations.
- Test duplicated, reordered, forged, mismatched, expired, cancelled, refunded, and disputed provider events.
- Confirm denied calls never reach Stripe.
- Confirm checkout rows and caller-facing metering belong to the buyer.
Related guides: API-Metering, Authorization-Scenarios, Custom-Actions, Integrations, and Task-Scheduling.
- Home
- Getting-Started-Guide
- Installation
- First-Admin-Setup
- Configuration
- Database-Setup
- Production-Deployment
- Common-Errors
- Actions-Overview
- Action-Permission-Schema-Sync-Technical-KT
- User-Actions
- Admin-Actions
- Data-Actions
- Cloud-Actions
- Email-Actions
- Certificate-Actions
- Custom-Actions