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

StripePAIDNotes
PaymentIntentPOST /v1/payment_intentsExplicit authorizecapture steps instead of a single confirm; refund and void round out the lifecycle.
Checkout SessionPOST /v1/checkout/sessionsResponse 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.
CustomerPOST /v1/customersName, email, phone, address, shipping, tax info, metadata.
Product + PricePOST /v1/plansOne object instead of two. external_id can hold your Stripe price id.
SubscriptionPOST /v1/subscriptionsCancel (optionally at period end), pause, resume. Cycle charging is driven by your server — see below.
InvoicePOST /v1/invoicesDraft → line items → finalize → pay / void / mark uncollectible / send.
RefundPOST /v1/payment_intents/:id/refundRefunds hang off the intent; omit amount for a full refund.
Webhook endpointDashboard → WebhooksSame 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.

One difference worth knowing up front: on PAID, 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.

create-session.sh
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.

intent-lifecycle.sh
# 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.

refund.sh
# 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.

StripePAID
Stripe-Signature: t=…,v1=…Webhook-Signature: t=…,v1=… (same HMAC recipe)
payment_intent.succeededpayment_intent.captured — fires when the capture completes; authorize fires payment_intent.authorized, declines fire payment_intent.failed
checkout.session.completedcheckout.session.completed (same name)
charge.refundedpayment_intent.refunded — refunds are a state change on the intent, not a separate Refund object event
charge.dispute.createddispute.created
customer.subscription.createdsubscription.created (plus .canceled, .paused, .resumed; .updated is reserved and not yet emitted)
invoice.paidinvoice.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.

migrate-subscriptions.sh
# 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.

billing-cycle.sh
# 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"
Not shipped yet, stated plainly: there is no automated Stripe-subscription importer — the mapping script above is yours to run. Hosted checkout's 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 with POST /v1/accounts/me/go-live once 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-Key on every money-moving POST — it is required, and it is what makes your retries safe.
  • Pass payer_name and billing_country on 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.
Stuck on a mapping this page doesn't cover? The API reference lists the full surface, and [email protected] reads every migration question.