---
url: https://saas.dev/docs/offers
md: https://saas.dev/docs/offers.md
---

# Offers

An Offer is a plan your customers buy. Leaseclock Pro costs $29 a month and allows 200 Units. The same file sets the pricing page, the Stripe product and price, the limit the API enforces and the upgrade prompt in the app.

## The file

Each Offer is one file in your product's `Offer` folder. This is Leaseclock Pro, as Leaseclock pushes it:

```yaml title="Offer/pro.mdx"
---
name: Leaseclock Pro
price: { amount: 29, currency: USD, unit: month }
limits: { Unit: 200 }
personas: Property manager, Leasing coordinator
status: Live
---
```

Push it with [`npx saas.dev push`](https://saas.dev/docs/saas-dev-cli.md), and Leaseclock is on sale.

## What it sets

| Where | What Leaseclock Pro puts there |
| --- | --- |
| [leaseclock.com/pricing](https://leaseclock.com/pricing) | A card reading "Leaseclock Pro, $29 a month", "Up to 200 units" and "For property managers and leasing coordinators", with a Subscribe button |
| Your Stripe account | A product named Leaseclock Pro with a monthly price of $29.00 USD |
| Stripe Checkout | "Subscribe to Leaseclock Pro, $29.00 per month", in Leaseclock's teal |
| The API | A `402 limit-reached` error for any Unit past a customer's 200th |
| The app | "24 of 200 Units" on Northgate Rentals' Billing screen, and the upgrade prompt when a customer reaches the limit |
| Docs and `llms.txt` | The plan, its price, its limits and who it is for, so a customer's agent can compare plans for its user |

The pricing page, checkout and Stripe read the price from one line, so they always agree.

## Keys

| Key | In Leaseclock Pro | What it does |
| --- | --- | --- |
| `name` | `Leaseclock Pro` | The plan's name on the pricing page, at checkout, in Stripe and on every invoice |
| `price.amount` | `29` | The price in whole units of the currency. `29` charges $29.00, and `29.50` charges $29.50 |
| `price.currency` | `USD` | Any three-letter currency code Stripe accepts |
| `price.unit` | `month` | The billing period, `month` or `year`. Stripe renews the subscription once per period |
| `limits` | `{ Unit: 200 }` | The most records of each named Noun one customer may hold. A Noun left out has no limit |
| `personas` | `Property manager, Leasing coordinator` | Who the plan is for, shown on the pricing card and in your docs |
| `status` | `Live` | `Draft` sells in Test only, `Live` sells in Test and Live, and `Retired` stops new sales |
| `$id` | `leaseclock.com/offers/pro` | The Offer's address. It ends in the file name, `pro` |

## Limits

A limit counts the records one customer holds in its workspace. Northgate Rentals manages 24 apartments, so it holds 24 Units and has room for 176 more on Leaseclock Pro. A Unit that is deleted stops counting.

The API enforces each limit, so the app, the CLI, the SDK and your customers' agents all stop at the same place. Here is the request that would add a customer's 201st Unit:

```sh title="Terminal"
curl -X POST https://leaseclock.com/api/units \
  -H "Authorization: Bearer $LEASECLOCK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "label": "9A", "building": "Birch House" }'
```

```json title="402 Payment Required"
{
  "error": {
    "code": "limit-reached",
    "message": "Leaseclock Pro includes 200 Units, and this workspace holds 200.",
    "noun": "Unit",
    "limit": 200,
    "offer": "leaseclock.com/offers/pro",
    "docs": "https://leaseclock.com/docs/errors#limit_reached"
  }
}
```

The request saves nothing. An import that would pass the limit is rejected whole and saves no records. Your customers read about the error on your own docs at `leaseclock.com/docs/errors`, and you read about it in [Errors](https://saas.dev/docs/errors.md#limit_reached).

## Add a free plan

An Offer with an `amount` of `0` is a free plan. To let property managers start Leaseclock without a card, add a second file:

```yaml title="Offer/starter.mdx"
---
name: Leaseclock Starter
price: { amount: 0, currency: USD, unit: month }
limits: { Unit: 10 }
personas: Independent landlord
status: Live
---
```

New customers start on Leaseclock Starter without visiting checkout. The pricing page shows both cards, cheapest first.

## The upgrade prompt

When a customer reaches a limit, the button that would pass it opens the upgrade prompt in place of the form. The prompt states the limit, how many records the customer holds, and the smallest Live Offer that allows more, with an Upgrade button that opens Stripe Checkout. The new limit applies the moment Stripe confirms the payment.

A Leaseclock Starter customer who adds an eleventh Unit sees:

> **Leaseclock Starter includes 10 Units**
>
> Upgrade to Leaseclock Pro for up to 200 Units, $29 a month.
>
> **Upgrade** · Not now

When no Live Offer allows more, the prompt states the limit and the count, with no Upgrade button.

## Change an Offer

Edit the file and push it again. To raise Leaseclock Pro to $35 a month:

```diff title="Offer/pro.mdx"
- price: { amount: 29, currency: USD, unit: month }
+ price: { amount: 35, currency: USD, unit: month }
```

The pricing page and the next checkout show $35. SaaS.Dev adds a new $35.00 price to the Stripe product, and every customer already subscribed keeps the $29.00 price they bought.

A new limit applies to every customer on the Offer on the next request. Raise `Unit` to 250 and each Pro customer can add 50 more. Lower it, and a customer over the new limit keeps every record it has and adds no more until it is under the limit.

A new `name` renames the Stripe product, the pricing card and the next invoice. The `$id` stays the same, so subscriptions carry over.

To stop selling an Offer, set `status: Retired`. It leaves the pricing page, the upgrade prompt and checkout, and every current subscription renews as before.

## Questions

### Can a plan have a monthly and a yearly price?

Yes. Write `price` as a list: `price: [{ amount: 29, currency: USD, unit: month }, { amount: 290, currency: USD, unit: year }]`. The pricing page gets a Monthly and Yearly switch, and Stripe gets one price for each.

### What can a customer do before it subscribes?

With a free Offer Live, it starts on that plan. Without one, every limit is zero until it subscribes, so a new customer's first New Unit opens the prompt for Leaseclock Pro. Nouns with no limit stay open.

### Can I give a discount?

Yes. Create a promotion code in your Stripe dashboard. Checkout accepts it, and the Offer stays as written.

### Does SaaS.Dev take a share of what customers pay?

No. Customers pay you through your own Stripe account, and Stripe charges its standard card rate. See [Stripe](https://saas.dev/docs/stripe.md) for checkout, subscriptions and payouts.

### Which Nouns can carry a limit?

Any Noun in your product. `limits: { Unit: 200, Lease: 400 }` limits both. A [Noun](https://saas.dev/docs/nouns.md) left out of `limits` has no cap.
