# Migrating from Stripe

> Migrate from the Stripe API to Qualy: Stripe Payment Links, Checkout, PaymentIntents, Customers, Subscriptions, Refunds, Connect and webhooks mapped to Qualy, with code side by side.

If you know Stripe, you already know most of Qualy. This guide maps each Stripe concept to its Qualy equivalent and shows the code side by side.

---

## Concept map

| Stripe | Qualy | Notes |
| --- | --- | --- |
| Customer | [Contact](https://v1-spec.qualyhq.com/#post-/v1/contacts/create) | Names go in a nested `profile`. Reference a contact by id or email. |
| Product / Price | [Service](https://v1-spec.qualyhq.com/#post-/v1/services/create) | A course, a fee, an insurance product. Optional on one-off payments. |
| Payment Link | A public service + its price template | Reusable link anyone can pay. See [Payment links](/docs/guides/payment-links.md#reusable-links). |
| Checkout Session | Payment portal | Hosted page for cards, bank transfers and local methods. |
| PaymentIntent | [Payment intent](/docs/guides/creating-payment-intents.md) | Same idea, but it carries line `items`, a `dueAt`, and tax. |
| Invoice | Payment intent with `items` | No separate invoice object. Tax invoices are generated from the payment. |
| Subscription | [Subscription](/docs/guides/creating-subscriptions.md) | Generates a payment intent per period. |
| Refund | [Refund](/docs/guides/issuing-transaction-refunds.md) | A refund intent against the original charge. |
| Dispute | [Dispute](/docs/guides/handling-disputes.md) | — |
| Connect: connected account | [Partnership](https://v1-spec.qualyhq.com/#post-/v1/partnerships/create) | A school, agent or supplier you pay. |
| Connect: transfers / `application_fee_amount` | [Payment splits](/docs/guides/creating-payment-splits.md) | Declared up front; Qualy pays out after settlement. |
| `metadata` | `custom` | Visible in the dashboard once a [custom field](https://v1-spec.qualyhq.com/#post-/v1/fields/create) with the same name exists. |
| Smart Retries / dunning emails | [Dunning](/docs/guides/dunning.md) | Reminders for overdue payments. |
| `Stripe-Account` header | `X-TENANT-ID` header | See [Multi-tenancy](/docs/tenants.md). |

---

## Authentication

Stripe uses a secret key as a Bearer token. Qualy uses an API key plus your tenant id:

```bash
# Stripe
curl https://api.stripe.com/v1/customers -u sk_live_...:

# Qualy
curl https://api.qualyhq.com/v1/contacts \
  -H 'Authorization: ApiKey your-api-key-here' \
  -H 'X-TENANT-ID: your-tenant-id-here'
```

Qualy takes JSON bodies, not form-encoded ones, and writes use `POST .../create` and `POST .../{id}/update`.

---

## Customers → contacts

**Stripe (Node)**

```javascript
const customer = await stripe.customers.create({
  email: 'john.doe@example.com',
  name: 'John Doe',
  metadata: { crmId: 'A-1042' },
});
```

**Qualy (JavaScript)**

```javascript
const res = await fetch('https://api.qualyhq.com/v1/contacts/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'ApiKey your-api-key-here',
    'X-TENANT-ID': 'your-tenant-id-here',
  },
  body: JSON.stringify({
    email: 'john.doe@example.com',
    profile: { firstName: 'John', lastName: 'Doe' },
  }),
});

const { data: contact } = await res.json();
```

---

## Payment Links

A Stripe Payment Link is reusable and anyone can pay it. Qualy has both kinds:

* **Reusable, for anyone** — like Stripe: a **service** (the Product) marked public with `displayEcommerce: true`, and a **template** (the Price). The link is `https://{subdomain}.qualyhqportal.com/services/{serviceId}?action=buy-now`. See [Payment links](/docs/guides/payment-links.md#reusable-links).
* **One-off, for one student** — every payment intent has its own link. Use it when you already know who owes what.

Side by side for a reusable link:

**Stripe (Node)**

```javascript
const product = await stripe.products.create({ name: 'Application fee' });
const price = await stripe.prices.create({
  product: product.id,
  currency: 'aud',
  unit_amount: 25000,
});
const link = await stripe.paymentLinks.create({
  line_items: [{ price: price.id, quantity: 1 }],
});
console.log(link.url);
```

**Qualy (JavaScript)**

```javascript
const headers = {
  'Content-Type': 'application/json',
  'Authorization': 'ApiKey your-api-key-here',
  'X-TENANT-ID': 'your-tenant-id-here',
};

// Product → service, public
const { data: service } = await fetch('https://api.qualyhq.com/v1/services/create', {
  method: 'POST',
  headers,
  body: JSON.stringify({ name: 'Application fee', displayEcommerce: true }),
}).then((r) => r.json());

// Price → template
await fetch('https://api.qualyhq.com/v1/templates/create', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    name: 'Application fee',
    category: 'payment-plan-list',
    services: [service._id],
    displayEcommerce: true,
    payments: [{ currency: 'AUD', items: [{ name: 'Application fee', amount: 25000 }] }],
  }),
});

console.log(`https://yourcompany.qualyhqportal.com/services/${service._id}?action=buy-now`);
```

When someone pays, Qualy creates the contact, an order and the payment for you, and sends the usual webhooks. For a one-off link, create a payment intent and send its `links.short` instead. Either way the student picks how to pay (card, bank transfer, PIX, PayID and more, depending on their country and currency) and Qualy confirms the payment by webhook.

> **Note — Pay-what-you-owe and plans**
>
> Stripe's "customer chooses price" is a template with `category: "payment-ad-hoc-variable-amount"`. A template can also hold several payments (a deposit and instalments) and splits, so one link can pay a school and keep your commission.

---

## Checkout and PaymentIntents

**Stripe Checkout → the payment portal.** Redirect to `links.long` (or `links.short`) instead of `session.url`. Card payments always go through the portal, as Checkout does.

**Stripe PaymentIntent + Elements → the payment intent + sign.** For non-card methods you can build your own UI: get the methods available to the student, then sign the chosen one to get a QR code, a boleto, a PayID or bank details. See [Collecting payments](/docs/guides/collecting-payments.md#via-api-direct).

Statuses map roughly as follows:

| Stripe PaymentIntent | Qualy payment intent |
| --- | --- |
| `requires_payment_method` | `due` |
| `processing` | `processing` |
| `succeeded` | `paid-full` |
| — | `paid-partial` (Qualy accepts partial payments) |
| — | `overdue` (past `dueAt`) |
| `canceled` | `canceled` |
| `refunded` (on the charge) | `refunded-partial` / `refunded-full` |

---

## Subscriptions

Stripe bills a Price on an interval. Qualy bills an amount on a schedule and creates a payment intent each period:

**Stripe (Node)**

```javascript
await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: 'price_monthly_support' }],
  billing_cycle_anchor_config: { day_of_month: 1 },
});
```

**Qualy (JavaScript)**

```javascript
await fetch('https://api.qualyhq.com/v1/subscriptions/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'ApiKey your-api-key-here',
    'X-TENANT-ID': 'your-tenant-id-here',
  },
  body: JSON.stringify({
    name: 'Monthly Service Plan',
    mode: 'schedule',
    contact: 'john.doe@example.com',
    service: 'Premium Support',
    amount: 10000,
    currency: 'AUD',
    billing: { startAt: '2026-12-01T00:00:00.000Z' },
    schedule: { frequency: 'every-month', dayOfMonth: 1 },
  }),
});
```

For a fixed number of installments (a course paid in 6 parts) use an [order](/docs/guides/creating-orders.md) with `installment` payments, or offer [financing](/docs/guides/financing-options.md).

---

## Refunds

**Stripe (Node)**

```javascript
await stripe.refunds.create({ payment_intent: 'pi_...', amount: 150000 });
```

**Qualy (JavaScript)**

```javascript
await fetch('https://api.qualyhq.com/v1/refund-intents/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'ApiKey your-api-key-here',
    'X-TENANT-ID': 'your-tenant-id-here',
  },
  body: JSON.stringify({
    currency: 'AUD',
    route: {
      method: 'ZAI_CC',
      kind: 'native-reverse', // back to the original card
      source: 'tenant',
      destination: 'contact',
      settlementCurrency: 'AUD',
    },
    items: [
      { appliesTo: '668bfc47f44334c60d96848e', grossAmount: 150000, basisCurrency: 'AUD' },
    ],
    refundReason: 'requested_by_customer',
  }),
});
```

Unlike Stripe, you reference the **charge transaction**, can preview fees and FX first, and the refund may wait for approval. See [Issuing refunds](/docs/guides/issuing-transaction-refunds.md) for options, previews and statuses.

---

## Connect → partnerships and splits

With Connect you collect on the platform and move money with `transfer_data`, `application_fee_amount` or separate Transfers. In Qualy you declare up front who gets what with **payment splits**, and Qualy pays each partner when the payment settles.

| Stripe Connect pattern | Qualy |
| --- | --- |
| Destination charge with `application_fee_amount` | A `keep-percentage` split: the partner gets the payment, you keep your commission. |
| Separate charges and transfers | One split per recipient: `fixed`, `percentage-on-amount` or `percentage-on-splits`. |
| `on_behalf_of` a single seller | `suppliers` on the payment intent: everything goes to the supplier unless a split says otherwise. |

You can see the payouts before you create the payment with [`POST /v1/payment-splits/preview/draft`](/docs/guides/creating-payment-splits.md#previewing-payouts).

---

## Webhooks

| Stripe event | Qualy event |
| --- | --- |
| `customer.created` / `customer.updated` | `contacts.create` / `contacts.update` |
| `payment_intent.created` | `paymentIntents.create` |
| `payment_intent.succeeded`, `invoice.paid` | `paymentIntents.update` with `status: "paid-full"` |
| `charge.succeeded` / `charge.refunded` | `transactions.create` / `transactions.update` |
| `transfer.created` | `splitIntents.create` / `splitIntents.update` |

Qualy sends fewer, broader events: watch `paymentIntents.update` and read `status` rather than subscribing to one event per state. See [Webhook events](/docs/webhooks/events.md).

---

## Things that work differently

* **Every payment belongs to a contact.** Reusable links create the contact at checkout; over the API, create the contact first (or pass an email).
* **Partial payments are normal.** A student can pay a deposit now and the rest later against the same payment.
* **Students pay in their own currency.** Set `settlementCurrency`, or let the student choose on the portal. See [Collecting payments](/docs/guides/collecting-payments.md#understanding-currency-vs-settlement-currency).
* **Saved cards can't be imported.** Students enter their card again on their first Qualy payment.
