# Sell your first offer

Build one checkout for a course and put a working **Join the course** button on your own site. The same steps work for a download, workshop, or physical product; replace the example name and slug with yours.

## 1. Prepare the offer

1. In Plandalf, create or import the product and its price. Connect the payment integration this offer will use.
2. Create an offer for that product. Choose a checkout template, set the customer fields and confirmation behavior, and give the offer a slug. This guide uses `ceramics-course`.
3. Use **Preview** to check the product, currency, total, discounts, and what the buyer sees after checkout. Open **Share** for the hosted link or your site's install instructions. [Offer setup](https://plandalf.com/docs/product-guides/checkouts/create) covers the editor and publishing steps.

Keep the **offer slug**, not the product ID or the offer title. Also note your organization's host. The examples below use `https://your-org.plandalf.dev`; replace it with your real host.

## 2. Put the button on your page

Choose the integration that matches the page you already have. All three open the same published offer.

<scalar-tabs default="HTML">
<scalar-tab title="HTML">

Add the script once and put the attribute on the button. The browser SDK binds the click for you.

```html title="course.html"
<script defer
  src="https://your-org.plandalf.dev/js/plandalf-sdk.js"></script>
<button data-plandalf-present="ceramics-course">
  Join the course
</button>
```

</scalar-tab>
<scalar-tab title="JavaScript">

Install `@plandalf/sdk` in your JavaScript app. The returned handle is awaitable; a dismissal is a result, while an opening failure rejects.

```javascript title="course.js"
import { Plandalf } from '@plandalf/sdk'

const sdk = new Plandalf({
  apiBase: 'https://your-org.plandalf.dev',
})
const button = document.querySelector('#join-course')
const message = document.querySelector('#course-message')

button.addEventListener('click', async () => {
  button.disabled = true
  try {
    const result = await sdk.present('ceramics-course')
    message.textContent = result.status === 'complete'
      ? 'Checkout finished. We are confirming your place.'
      : 'You can join later.'
  } catch {
    message.textContent =
      'Checkout could not open. Please try again.'
  } finally {
    button.disabled = false
  }
})
```

```html title="course.html"
<button id="join-course">Join the course</button>
<p id="course-message" role="status"></p>
```

</scalar-tab>
<scalar-tab title="React">

Install `@plandalf/react` and place the provider above the button.

```tsx title="CourseButton.tsx"
import { useState } from 'react'
import { PlandalfProvider, usePresent } from '@plandalf/react'

function CourseButton() {
  const { present, presenting } = usePresent()
  const [message, setMessage] = useState('')

  async function join() {
    try {
      const result = await present('ceramics-course')
      setMessage(result.status === 'complete'
        ? 'Checkout finished. We are confirming your place.'
        : 'You can join later.')
    } catch {
      setMessage('Checkout could not open. Please try again.')
    }
  }

  return <>
    <button disabled={presenting} onClick={join}>
      Join the course
    </button>
    <p role="status">{message}</p>
  </>
}

export function CoursePage() {
  return <PlandalfProvider apiBase="https://your-org.plandalf.dev">
    <CourseButton />
  </PlandalfProvider>
}
```

</scalar-tab>
</scalar-tabs>

## 3. Test the whole journey

1. Connect **Stripe (Test)** in **Settings → Advanced**. For this embedded button, add your development hostname under **Test domains** and save it. For a direct hosted checkout, open **Share → Test mode link** while signed in as an organization member. [Hosted checkout](https://plandalf.com/docs/product-guides/checkouts/hosted-checkout#testing-before-you-publish) explains both paths. A `mode: 'test'` override alone does not guarantee test payments.
2. Load your page and click the button. Confirm the published `ceramics-course` offer opens with the expected price and test payment environment. Without a connected Stripe test account or an authorized test context, checkout can remain live.
3. Dismiss checkout. The page should remain usable; JavaScript and React receive `status: 'dismissed'`.
4. On the confirmed test setup, complete a Stripe test payment. JavaScript and React receive `status: 'complete'`, but that browser result does **not** prove the payment settled.
5. Check the test invoice and the payment in the Stripe test account. Test purchases skip automation sequences unless **Run on test purchases** is enabled. Have your server confirm the order before granting course access, sending a download, or shipping a product.

After testing, publish the checkout and your site. Production pages should use the live payment integration and stay outside the Test domains list.

If checkout cannot find the offer, check the host, published slug, and payment setup. For a fuller course integration, see the [SDK example](https://plandalf.com/docs/guides/await-a-checkout). The exact call and result fields are in [`present()`](https://plandalf.com/docs/sdk/present), [`FlowOpenOptions`](https://plandalf.com/docs/packages/sdk/interfaces/FlowOpenOptions), and [`FlowResult`](https://plandalf.com/docs/packages/sdk/interfaces/FlowResult).

Source: https://plandalf.com/docs/start/checkouts/first-checkout
