---
title: Payments quickstart
description: Sell a monthly plan in test mode with one key - connect a test Stripe account, declare the plan, open a checkout, pay with a test card, and check access.
type: tutorial
product: payments
summary: A connected test account, a synced plan, a paid checkout, and an entitlement
updated: 2026-10-07
order: 1
---

This takes you from nothing to a paying test subscriber, using only a key,
your Stripe account in test mode, and `curl`. Nothing is deployed and no
webhook is set up by hand: Payments creates its own on your account.

<Prerequisites
	items={[
		'An account, and a project with an environment',
		'A secret key holding the billing:write and billing:read scopes, as SYLPHX_API_KEY',
		'A Stripe account, in test mode',
		'curl and jq',
	]}
/>

The examples use the environment `orgs/acme/projects/shop/envs/production`;
use your own. `sylphx access whoami` prints the environment your key belongs
to, and a key only reaches its own environment.

```bash
export ENV=orgs/acme/projects/shop/envs/production
```

## 1. Connect your Stripe account in test mode

In the Stripe dashboard, in test mode, open Developers, API keys, and create a
restricted key with: Account read; Webhook Endpoints, Products, Prices,
Customers, Checkout Sessions, Billing Portal, Subscriptions and Refunds write;
Invoices, Charges, Payment Intents and Disputes read. Then connect it:

```bash
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/merchant_accounts:connect" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"restricted_key": "rk_test_…"}' | jq .merchant_account.account_id
```

The account connects in this call. The key is sealed: it is never returned
or logged. Without a restricted key, send `{"return_url": "https://…"}`
instead and open the `authorize_url` in the answer to approve the connection
with Stripe's OAuth.

## 2. Declare a plan and push it to Stripe

The catalog is the one place your prices live. Amounts are integer minor units
(`499` is US$4.99), and prices are tax inclusive.

**cURL**

```bash
curl -sS -X PATCH "https://api.sylphx.com/v1/$ENV/price_catalogs/default?allow_missing=true" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec": {
        "features": [{"key": "plus"}],
        "products": [{"key": "plus", "display_name": "Plus", "features": {"plus": "true"},
          "prices": [{"key": "plus_monthly", "recurring_interval": "month",
            "tax_behavior": "inclusive", "unit_amounts": {"USD": "499"}}]}]
      }}'
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/price_catalogs/default:sync" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**TypeScript**

```ts
import { Sylphx } from '@sylphx/sdk'

const sylphx = new Sylphx()
const { env } = await sylphx.access.whoami({})
const name = `${env}/price_catalogs/default`
await sylphx.money.priceCatalogs.update({
  allowMissing: true,
  catalog: {
    name,
    spec: {
      features: [{ key: 'plus' }],
      products: [
        {
          key: 'plus',
          displayName: 'Plus',
          features: { plus: 'true' },
          prices: [
            { key: 'plus_monthly', recurringInterval: 'month', taxBehavior: 'inclusive', unitAmounts: { USD: '499' } },
          ],
        },
      ],
    },
  },
})
await sylphx.money.priceCatalogs.sync({ name })
```

The product, its price and its lookup key now exist in your Stripe account.

## 3. Open a checkout

From your server, open a checkout for one of your users. The subject is your
own account id for them.

**cURL**

```bash
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/checkout_sessions" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject": {"end_user": "user-42"},
       "line_items": [{"price": "plus_monthly"}],
       "success_url": "https://example.com/billing/return?s={CHECKOUT_SESSION_ID}",
       "cancel_url": "https://example.com/pricing"}' | jq -r .url
```

**TypeScript**

```ts
const checkout = await sylphx.money.checkoutSessions.create({
  parent: env,
  checkoutSession: {
    subject: { endUser: 'user-42' },
    lineItems: [{ price: 'plus_monthly' }],
    successUrl: 'https://example.com/billing/return?s={CHECKOUT_SESSION_ID}',
    cancelUrl: 'https://example.com/pricing',
  },
})
// Redirect the buyer to checkout.url
```

Open the `url`, and pay with the test card `4242 4242 4242 4242`, any future
date and any CVC. A retried create never starts a second checkout.

## 4. Check access

The success page grants nothing on its own: the grant arrives when Stripe's
webhook reaches Payments, usually within seconds. Ask:

**cURL**

```bash
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/entitlement_grants:check" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject": {"end_user": "user-42"}, "feature": "plus"}'
```

**TypeScript**

```ts
const { entitled, expireTime } = await sylphx.money.entitlementGrants.check({
  parent: env,
  subject: { endUser: 'user-42' },
  feature: 'plus',
})
```

The answer is `entitled: true` with the `expire_time` of the paid period. Cache
it for at most 60 seconds and never past `expire_time`, and check again when
access is decided.

## Read it back

```bash
curl -sS "https://api.sylphx.com/v1/$ENV/customer_subscriptions" \
  -H "Authorization: Bearer $SYLPHX_API_KEY"
```

The subscription, mirrored from Stripe. A portal session
(`POST …/portal_sessions` with the subject and a `return_url`) lets the buyer
cancel at period end; `:check` stays true until that end. To go live, connect
your live account with an `rk_live_…` key and sync the catalog again.

## Next

<RelatedDocs
	links={[
		{
			href: '/docs/payments',
			label: 'Payments',
			description: 'The catalog, checkout, subscriptions and entitlements.',
		},
		{
			href: '/docs/api/customer_subscriptions',
			label: 'The customer_subscriptions collection',
			description: 'Cancel, resume, change plan and change seats.',
		},
		{
			href: '/docs/api/price_catalogs',
			label: 'The price_catalogs collection',
			description: 'Currencies, trials, one-time prices and consumables.',
		},
	]}
/>
