Migrate from Stripe
PAID's API is deliberately Stripe-shaped: Bearer auth, int64 minor-unit amounts, idempotency keys, a hosted checkout you redirect to, and a webhook signature scheme your existing verifier already understands. This guide maps each Stripe primitive to its PAID equivalent — and is explicit about what does not exist yet.
How the primitives line up
| Stripe | PAID | Notes |
|---|---|---|
| PaymentIntent | POST /v1/payment_intents | Explicit authorize → capture steps instead of a single confirm; refund and void round out the lifecycle. |
| Checkout Session | POST /v1/checkout/sessions | Response includes a hosted url (/c/<session_id>) — redirect the customer to the returned url rather than building the path yourself, same pattern as Stripe Checkout. |
| Customer | POST /v1/customers | Name, email, phone, address, shipping, tax info, metadata. |
| Product + Price | POST /v1/plans | One object instead of two. external_id can hold your Stripe price id. |
| Subscription | POST /v1/subscriptions | Cancel (optionally at period end), pause, resume. Cycle charging is driven by your server — see below. |
| Invoice | POST /v1/invoices | Draft → line items → finalize → pay / void / mark uncollectible / send. |
| Refund | POST /v1/payment_intents/:id/refund | Refunds hang off the intent; omit amount for a full refund. |
| Webhook endpoint | Dashboard → Webhooks | Same t=…,v1=… HMAC-SHA256 scheme; the header is Webhook-Signature instead of Stripe-Signature. |
1. Account and API keys
Sign up, verify your business email, and copy your sk_live_… key from Dashboard → API keys. Auth is a straight swap: the
same Authorization: Bearer sk_… header you send to Stripe, pointed at api.trustfabric.ai. One difference from Stripe's key model: there is no separate
test keyspace. Every account gets an sk_live_ secret key (plus an rk_live_ restricted key) at signup, but the account itself starts in test mode
(livemode=false) and only processes live money after POST /v1/accounts/me/go-live. Actual key prefixes are sk_live_ and rk_live_ — see API keys.
Idempotency-Key is required on money-moving POSTs, not optional. Retries with the same key collapse onto
the first response; the same key with a different body returns 409.2. One-time payments: Checkout Sessions
If you use Stripe Checkout, this is the shortest path. Create a session server-side, redirect
the customer to the returned url, and listen for checkout.session.completed. Line items use inline ad_hoc pricing (or price_id referencing a plan); the server computes the charged total from them, so
a tampered client can never drift the amount. A caller-supplied expires_at must
fall between 30 minutes and 24 hours out.
curl https://api.trustfabric.ai/v1/checkout/sessions \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{
"mode": "payment",
"currency": "USD",
"payment_method_types": ["card"],
"line_items": [{
"ad_hoc": {"amount": 4999, "currency": "USD", "description": "Pro plan"},
"quantity": 1
}],
"success_url": "https://example.com/thanks",
"cancel_url": "https://example.com/cancel"
}'3. Direct charges: PaymentIntents
Where Stripe collapses authorization and capture into confirm, PAID keeps them as
separate calls — which also gives you auth-then-capture-later for free. Each mutating call
takes both the Idempotency-Key header and an idempotency_key body
field.
# 1. Create the intent. Amounts are int64 minor units, like Stripe.
# merchant_id is your own account id — a key can only create
# intents for the account it belongs to.
curl https://api.trustfabric.ai/v1/payment_intents \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-create" \
-d '{
"amount": 4999,
"currency": "USD",
"merchant_id": "acct_yourmerchantid",
"idempotency_key": "order-1234-create"
}'
INTENT=<id from the response>
# 2. Authorize. payer_name + billing_country identify the person
# being charged — the payer is sanctions-screened before any
# money moves, and the screen fails closed in production.
curl https://api.trustfabric.ai/v1/payment_intents/$INTENT/authorize \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-auth" \
-d '{
"payment_intent_id": "'$INTENT'",
"idempotency_key": "order-1234-auth",
"payer_name": "Ada Lovelace",
"billing_country": "US"
}'
# 3. Capture. Omit "amount" for a full capture.
curl https://api.trustfabric.ai/v1/payment_intents/$INTENT/capture \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-capture" \
-d '{
"payment_intent_id": "'$INTENT'",
"idempotency_key": "order-1234-capture"
}'Refunds are issued against the intent rather than a separate Refund object. POST /v1/payment_intents/:id/void cancels an authorized-but-uncaptured intent.
# Omit "amount" for a full refund; include it for a partial refund.
curl https://api.trustfabric.ai/v1/payment_intents/$INTENT/refund \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-order-1234" \
-d '{
"payment_intent_id": "'$INTENT'",
"idempotency_key": "refund-order-1234",
"amount": 1000
}'4. Webhooks
PAID's signature scheme is Stripe-compatible: t=<timestamp>,v1=<hmac>,
HMAC-SHA256 over "<timestamp>.<raw body>" with your endpoint's whsec_… secret. Most verifiers port by changing the header name and the secret.
Register endpoints in Dashboard → Webhooks; delivery,
retries, and the dead-letter queue are covered in Webhooks.
| Stripe | PAID |
|---|---|
Stripe-Signature: t=…,v1=… | Webhook-Signature: t=…,v1=… (same HMAC recipe) |
payment_intent.succeeded | payment_intent.captured — fires when the capture completes; authorize fires payment_intent.authorized, declines fire payment_intent.failed |
checkout.session.completed | checkout.session.completed (same name) |
charge.refunded | payment_intent.refunded — refunds are a state change on the intent, not a separate Refund object event |
charge.dispute.created | dispute.created |
customer.subscription.created | subscription.created (plus .canceled, .paused, .resumed; .updated is reserved and not yet emitted) |
invoice.paid | invoice.paid (plus .created, .finalized, .voided, .sent) |
5. Subscriptions and recurring billing
The recurring surface that exists today: plans, subscriptions (with cancel / pause / resume and at-period-end cancellation), billing cycles, and invoices — all first-class API objects with webhook events. Migration is a re-creation exercise driven by your own script against the Stripe API and the endpoints below.
# 1. Recreate each Stripe price as a PAID plan. billing_period is
# weekly | monthly | quarterly | annual. external_id is a free
# field — store the Stripe price id so the mapping survives.
curl https://api.trustfabric.ai/v1/plans \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro plan",
"amount": 4999,
"currency": "USD",
"billing_period": "monthly",
"trial_period_days": 14,
"external_id": "price_YourStripePriceId"
}'
# Plans are created as drafts ({"id": "plan_...", "status": "draft"}).
# Activate before subscribing anyone:
curl -X POST https://api.trustfabric.ai/v1/plans/$PLAN/activate \
-H "Authorization: Bearer $PAID_SECRET_KEY"
# 2. Recreate the customer.
curl https://api.trustfabric.ai/v1/customers \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace", "email": "[email protected]"}'
# 3. Create the subscription (customer id comes from step 2).
curl https://api.trustfabric.ai/v1/subscriptions \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "'$PLAN'",
"customer_id": "cus_...",
"idempotency_key": "mig-sub-ada-pro"
}'Charging each period works differently from Stripe, and this is the honest part: PAID does not yet run a scheduler that charges due subscriptions for you. Your server drives each cycle — generate it, collect with a payment intent, record the result. The cycle and invoice objects keep the books; the charge timing is yours.
# a. When a period comes due, generate the next billing cycle.
curl -X POST https://api.trustfabric.ai/v1/subscriptions/$SUB/generate_cycle \
-H "Authorization: Bearer $PAID_SECRET_KEY"
# b. Collect the money with the payment-intent lifecycle above,
# then record the outcome on the cycle.
curl https://api.trustfabric.ai/v1/billing_cycles/$CYCLE/charge_result \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"success": true, "payment_intent_id": "'$INTENT'"}'
# c. Optionally issue the invoice for the cycle.
curl -X POST https://api.trustfabric.ai/v1/billing_cycles/$CYCLE/invoice \
-H "Authorization: Bearer $PAID_SECRET_KEY"mode: "subscription" is defined in the API but currently switched off in
production wiring, so subscription sessions are refused at create time; collect recurring
charges through the payment-intents API for now. Automatic execution is roadmap for both
cycles and dunning — but the dunning primitives (retry schedules, campaigns, GET /v1/dunning/due_retries) are live under /v1/dunning/* and are
driven by your server the same way cycles are. No typed SDKs are published yet — every sample
here is plain HTTP on purpose.6. Go-live checklist
- Your account starts in test mode (
livemode=false) — run the full flow there first, then flip to live withPOST /v1/accounts/me/go-liveonce onboarding-ready. The mode lives on the account, not on separate test/live keys. - Register your live webhook URL in Dashboard → Webhooks before flipping the account live.
- Send an
Idempotency-Keyon every money-moving POST — it is required, and it is what makes your retries safe. - Pass
payer_nameandbilling_countryon authorize: payer sanctions screening is fail-closed in production, and a blank name is refused. - Cut each subscription over at its period boundary (cancel at period end on Stripe, create on PAID) so no customer is double-billed.
- Keep your Stripe account open through the dispute window — PAID can only refund charges PAID processed.