> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metronome.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Implement a free trial

Create a trial contract with an expiring credit, wire up alerts and webhooks, convert the customer to paid, and handle the variations you are most likely to need: ending a trial early, a recurring free tier, and a prepaid landing.

Read [Free trials](/guides/pricing-packaging/billing-model-guides/free-trials/overview) first and complete its prerequisites, so that your custom-field keys are registered and your system notifications are enabled.

## Set up the trial

### Register custom-field keys (once)

Custom fields are how you mark a contract or credit as a trial, so that alerts, webhooks, and reports can tell trials apart from everything else a customer holds. Register each key once per environment before anything references it.

```bash theme={null}
for spec in customer:trial_started_at customer:trial_source contract:contract_type contract_credit:trial contract_credit:trial_model; do
  curl -s -X POST https://api.metronome.com/v1/customFields/addKey \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d "{\"entity\":\"${spec%%:*}\",\"key\":\"${spec##*:}\",\"enforce_uniqueness\":false}"
done
```

### Create the customer

```bash theme={null}
curl -s -X POST https://api.metronome.com/v1/customers \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "name": "Acme Corp",
  "ingest_aliases": ["acme-prod"],
  "custom_fields": { "trial_started_at": "2026-10-01T15:00:00Z", "trial_source": "self_serve" }
}'
```

### Create the trial contract — expiring credit

\$100 of credit, valid 14 days, [scoped](/guides/pricing-packaging/apply-credits-and-commits/target-credit-and-commits) to the products carrying the `Language models` [product tag](/guides/implement-metronome/core-concepts/create-products-contracts), and burned before anything else the customer holds (`priority: 1`).

```bash theme={null}
curl -s -X POST https://api.metronome.com/v1/contracts/create \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "name": "Trial",
  "starting_at": "2026-10-01T15:00:00Z",
  "rate_card_id": "d7abd0cd-4ae9-4db7-8676-e986a4ebd8dc",
  "uniqueness_key": "trial-13117714-3f05-48e5-a6e9-a66093f13b4d",
  "custom_fields": { "contract_type": "trial" },
  "credits": [{
    "name": "Trial credit",
    "product_id": "609e4cf2-6ea2-4b07-a46c-6596f041b69e",
    "priority": 1,
    "applicable_product_tags": ["Language models"],
    "custom_fields": { "trial": "true", "trial_model": "expiring_credit" },
    "access_schedule": {
      "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
      "schedule_items": [{ "amount": 10000, "starting_at": "2026-10-01T15:00:00Z", "ending_before": "2026-10-15T15:00:00Z" }]
    }
  }]
}'
```

To keep trial users out of specific products, add an `overrides[]` entry with `"entitled": false` for those product tags.

The trial ends as soon as either limit is reached: the balance hits 0, or the access schedule reaches its `ending_before` date. If any balance is left when the schedule ends, Metronome expires the remainder and records an expiration entry on the credit's ledger. That entry does not affect revenue, unlike the expiration of a prepaid commit — see [revenue recognition](/guides/reporting-insights/financial-reporting/revenue-recognition) for how each is treated.

Note that the contract above has no end date, and only the credit is time-bounded. When the credit expires the contract carries on and usage is rated at your list prices, so the customer lands on pay-as-you-go with no further action from you. Create a separate paid contract with a transition when the paid terms differ from the trial contract's, such as a different rate card, a prepaid commit, or a recurring free tier. The [reference architecture](/guides/pricing-packaging/billing-model-guides/free-trials/overview#reference-architecture) covers the trade-off between the two shapes.

To build one of the other patterns, start from the request above and change it as follows:

* **Skew toward time.** Keep a modest amount and push `ending_before` further out, for products where adoption ramps slowly. Every access-schedule segment requires an `ending_before`, so a credit that never expires isn't possible.
* **Effectively unlimited.** Grant far more than a trial customer could plausibly consume and keep the window short. The customer never meets the cap, and you still get balance tracking, alerts, and a complete drawdown record to learn from.
* **Quantity-based instead of spend-based.** Denominate the credit in a [custom pricing unit](/guides/pricing-packaging/make-pricing-changes/use-currency-custompricingunits) rather than a currency by setting `credit_type_id` to that unit. The trial then grants an amount of units, such as tokens or requests, instead of dollars.
* **Time-only, uncapped.** Remove the `credits` array and add a time-bounded zero multiplier in its place:

  ```json theme={null}
  "overrides": [{
    "type": "MULTIPLIER", "multiplier": 0,
    "applicable_product_tags": ["Language models"],
    "starting_at": "2026-10-01T15:00:00Z", "ending_before": "2026-10-15T15:00:00Z"
  }]
  ```

  Because there is no credit, this model produces no balance alerts and no `credit.segment.end` event. The override's end date is your only trial-end signal. To limit exposure after the trial, consider a [spend threshold](/guides/customers-billing/optimize-customer-experience/set-customer-spend-control) on the paid contract.
* **Reverse trial.** Keep the trial credit as-is, and add a `recurring_credits[]` item to the paid contract you renew into, so conversion lands the customer on a small permanent allowance. See [recurring free tier](#recurring-free-tier).

### Alerts

[Threshold alerts](/guides/customers-billing/set-up-notifications/threshold-notifications) are evaluated per customer, so create each one once and leave out `customer_id` to have it apply to everyone. Use [`alert_specifiers`](/guides/customers-billing/set-up-notifications/create-alert-specifiers) to evaluate only trial-tagged credits. Set a `uniqueness_key` so that retrying a request doesn't create a duplicate alert.

```bash theme={null}
# Nudge: 25% of trial credit remaining. threshold = percentage REMAINING.
curl -s -X POST https://api.metronome.com/v1/alerts/create -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "name": "Trial credit 25% remaining",
  "alert_type": "low_remaining_contract_credit_percentage_reached",
  "threshold": 25,
  "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
  "evaluate_on_create": false,
  "uniqueness_key": "trial-pct-25"
}'

# Nudge: 3 days left on the trial credit segment. Evaluated by a daily cron.
curl -s -X POST https://api.metronome.com/v1/alerts/create -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "name": "Trial ends in 3 days",
  "alert_type": "low_remaining_days_for_contract_credit_segment_reached",
  "threshold": 3,
  "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
  "evaluate_on_create": false,
  "uniqueness_key": "trial-days-3"
}'

# Backstop: trial balance exhausted by usage. Scoped to trial credits via alert_specifiers.
curl -s -X POST https://api.metronome.com/v1/alerts/create -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "name": "Trial balance zero",
  "alert_type": "low_remaining_contract_credit_and_commit_balance_reached",
  "threshold": 0,
  "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
  "evaluate_on_create": false,
  "uniqueness_key": "trial-balance-zero",
  "alert_specifiers": [{ "custom_field_filters": [{ "entity": "ContractCredit", "key": "trial", "value": "true" }] }]
}'
```

A few things to keep in mind when creating these alerts:

* `evaluate_on_create: false` skips only the initial evaluation. An alert that applies to all customers still fires for existing ones the next time their state changes.
* The days-remaining alert fires as soon as you create it if the trial is shorter than its threshold.
* `alert_specifiers` controls which credits are evaluated, but it is not echoed back in the webhook payload. Valid `entity` values are `Contract`, `Commit`, `ContractCredit`, and `ContractCreditOrCommit`. Using `group_key_filter` or per-key grouped specifiers requires account enablement, so contact Metronome support if you need them.

### Handle the webhooks

Enable the `credit.segment.start`, `credit.segment.end`, `credit.archive`, and `contract.end` [system notifications](/guides/customers-billing/set-up-notifications/system-notifications) and point them at your webhook endpoint.

<Tip>
  **Use `credit.segment.end` as the trial-end signal.** It fires at the segment boundary itself rather than after a later evaluation, and it carries credit, contract, and customer custom fields, so your handler needs no follow-up lookup.
</Tip>

Example payload:

```json theme={null}
{
  "id": "56a6f43c-1719-56a1-b339-398f072093f0",
  "type": "credit.segment.end",
  "timestamp": "2026-10-15T15:00:00Z",
  "environment_type": "PRODUCTION",
  "credit_id": "71189e85-f86c-4ec7-a31d-b491d585037a",
  "credit_custom_fields": { "trial": "true", "trial_model": "expiring_credit" },
  "contract_id": "65528c67-fff6-4130-a6fa-115a65223694",
  "contract_custom_fields": { "contract_type": "trial" },
  "parent_recurring_credit_id": null,
  "segment_index": 0,
  "segment_count": 1,
  "segment_id": "9693970e-663f-5457-96cf-2a50f4e14278",
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "customer_custom_fields": { "trial_started_at": "2026-10-01T15:00:00Z", "trial_source": "self_serve" }
}
```

The same event fires for three different reasons: the segment reached its end date, a conversion ended the trial early, or the credit was archived. Use the events that accompany it to tell them apart:

```python theme={null}
def on_webhook(evt):
    if evt["type"] in ("credit.segment.end", "contract.end", "credit.archive"):
        # Record every event against customer + timestamp, the two fields all three share.
        record(evt["type"], evt["customer_id"], evt["timestamp"])

    if evt["type"] == "credit.segment.end" and evt.get("credit_custom_fields", {}).get("trial") == "true":
        # contract.end and credit.archive carry the same timestamp but may be delivered
        # after this one, so classify after a short grace period rather than right now.
        schedule(classify_trial_end, evt["customer_id"], evt["timestamp"], delay_seconds=60)

    elif evt["type"] == "alerts.low_remaining_contract_credit_and_commit_balance_reached":
        # Backstop for USAGE exhaustion only. Every other path to a $0 balance fires this
        # alert too, so guard twice. Never branch on triggered_by: it names what triggered
        # the evaluation, not what emptied the balance.
        cid = evt["properties"]["customer_id"]
        if trial_already_ended(cid):
            return                                                  # segment.end path already classified it
        bal = list_balances(cid)                                     # POST /v1/contracts/customerBalances/list
        trial = next((c for c in bal if c["type"] == "CREDIT"
                      and c["custom_fields"].get("trial") == "true"), None)
        # Usage is the only path that empties the balance while the segment is still open.
        if trial and trial["balance"] == 0 and segment_open(trial, at=evt["properties"]["timestamp"]):
            mark_trial_exhausted(cid, at=evt["properties"]["timestamp"])

    elif evt["type"] in ("alerts.low_remaining_contract_credit_percentage_reached",
                         "alerts.low_remaining_days_for_contract_credit_segment_reached"):
        nudge(evt["properties"]["customer_id"], evt["type"], evt["properties"])


def classify_trial_end(customer_id, timestamp):
    if recorded("credit.archive", customer_id, timestamp): cause = "archived"
    elif recorded("contract.end", customer_id, timestamp): cause = "converted_or_ended"
    else:                                                  cause = "expired"
    mark_trial_ended(customer_id, cause, at=timestamp)


def segment_open(credit, at):
    # Expiry, conversion, and truncation all close the segment before the alert evaluates.
    return any(i["starting_at"] <= at < i["ending_before"]
               for i in credit["access_schedule"]["schedule_items"])
```

Alert payloads for reference:

```json theme={null}
{ "type": "alerts.low_remaining_contract_credit_and_commit_balance_reached",
  "properties": { "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d", "alert_id": "5a5c0b8e-3f21-4c7d-8b95-1e6a0d24f7b3",
                  "timestamp": "2026-10-08T21:39:25.513Z", "threshold": 0, "alert_name": "Trial balance zero",
                  "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2", "remaining_balance": 0, "triggered_by": "usage" } }

{ "type": "alerts.low_remaining_contract_credit_percentage_reached",
  "properties": { "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d", "alert_id": "fafadab6-7c14-4e82-9d35-08b1c6ea5f49",
                  "timestamp": "2026-10-06T21:33:54.577Z", "threshold": 25, "alert_name": "Trial credit 25% remaining",
                  "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2", "remaining_percentage": 20 } }

{ "type": "alerts.low_remaining_days_for_contract_credit_segment_reached",
  "properties": { "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d", "alert_id": "937d89a6-6b02-4f51-8a7c-3e94d5b1c082",
                  "credit_id": "71189e85-f86c-4ec7-a31d-b491d585037a", "threshold": 3,
                  "alert_name": "Trial ends in 3 days", "remaining_days": 2.8 } }
```

Unlike the system notifications above, alert payloads do not include custom fields, `contract_id`, or `environment_type`, so look the credit up through the API when you need those. Of the three alerts, only the days-remaining one includes a `credit_id`, and only it has no `timestamp`. `triggered_by` names the evaluation trigger rather than the reason the threshold was crossed, so don't branch on it.

### Convert to paid

Create the paid contract with a transition from the trial contract. `starting_at` must be hour-aligned, and it is the instant the trial ends.

```bash theme={null}
curl -s -X POST https://api.metronome.com/v1/contracts/create \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "name": "Pay as you go",
  "starting_at": "2026-10-15T15:00:00Z",
  "rate_card_id": "d7abd0cd-4ae9-4db7-8676-e986a4ebd8dc",
  "custom_fields": { "contract_type": "paid" },
  "transition": { "from_contract_id": "65528c67-fff6-4130-a6fa-115a65223694", "type": "RENEWAL" }
}'
```

For a pay-as-you-go landing with a card on file, add a [spend threshold](/guides/customers-billing/optimize-customer-experience/set-customer-spend-control) to the paid contract to control post-trial risk.

The transition has these effects:

* The trial contract's `ending_before` is set to the paid contract's `starting_at`, leaving no gap and no overlap.
* The trial credit is truncated to that instant and any unused balance expires. The ledger records a deduction for consumed usage and an expiration entry for the remainder, both stamped at the transition time. Nothing rolls over unless you set `rollover_fraction`.
* Accrued trial usage is re-dated from the billing-period boundary to the transition instant, then invoiced once the grace period ends and the invoice finalizes.
* You receive `contract.edit` when the paid contract is created, followed by `credit.segment.end` and `contract.end` at the transition instant.
* Both contracts return the same `transitions[]` entry, holding `type`, `from_contract_id`, and `to_contract_id`. It has no timestamp, so read the effective time from `contracts_transitions.date` in Data Export.

### End a trial early

You might need to end a trial before its window closes: the customer converts ahead of schedule and you don't want free credit burning alongside paid usage, sales renegotiates the evaluation period, or a trial is being misused and you want to shut off the allowance.

The cleanest way to do it is to truncate the credit's access schedule. The credit keeps its ledger, Metronome emits `credit.segment.end`, and the \$0 balance alert fires shortly afterward. Set `ending_before` to an hour boundary at or after the segment's `starting_at`; a zero-length segment is accepted and expires the full balance immediately.

```bash theme={null}
curl -s -X POST https://api.metronome.com/v2/contracts/edit \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "contract_id": "65528c67-fff6-4130-a6fa-115a65223694",
  "update_credits": [{
    "credit_id": "71189e85-f86c-4ec7-a31d-b491d585037a",
    "access_schedule": { "update_schedule_items": [{ "id": "b3d7e912-5c48-4a6f-9e21-7fa0c85d3b64", "ending_before": "2026-10-03T14:00:00Z" }] }
  }]
}'
```

Note that `update_credits[]` identifies the credit with `credit_id` while the nested schedule item uses `id`. Both values come from `POST /v1/contracts/customerBalances/list`.

<Warning>
  **Avoid archiving a trial credit.** Archiving zeroes the balance and deletes the credit's ledger from both the API and Data Export, hides the credit unless you pass `include_archived: true`, and leaves it out of the in-app Commits and credits report. You are left with a grant that has no consumption history to reconcile against. Reserve archiving for cleanup where you want the grant to disappear entirely.
</Warning>

### Recurring free tier

A recurring credit grants an allowance that refreshes every period, which suits both a permanent free tier and the landing state of a reverse trial. This example grants \$10 of usage a month on a perpetual contract, with no rollover. `recurrence_frequency` is an enum string, and recurring credits do not accept `custom_fields`.

```bash theme={null}
curl -s -X POST https://api.metronome.com/v1/contracts/create \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "name": "Free tier",
  "starting_at": "2026-10-15T15:00:00Z",
  "rate_card_id": "d7abd0cd-4ae9-4db7-8676-e986a4ebd8dc",
  "custom_fields": { "contract_type": "free_tier" },
  "transition": { "from_contract_id": "65528c67-fff6-4130-a6fa-115a65223694", "type": "RENEWAL" },
  "recurring_credits": [{
    "name": "Monthly free allowance",
    "product_id": "609e4cf2-6ea2-4b07-a46c-6596f041b69e",
    "priority": 1,
    "access_amount": { "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2", "unit_price": 1000, "quantity": 1 },
    "commit_duration": { "unit": "PERIODS", "value": 1 },
    "recurrence_frequency": "MONTHLY",
    "proration": "NONE",
    "rollover_fraction": 0,
    "starting_at": "2026-10-15T15:00:00Z",
    "applicable_product_tags": ["Language models"]
  }]
}'
```

Metronome pre-generates two child credit segments, and the upcoming one contributes nothing to the balance until its period begins. Each child carries a `recurring_credit_id`, which webhook payloads call `parent_recurring_credit_id`. Setting `recurrence_frequency` explicitly anchors each period to the credit's own `starting_at`, which means a period can reset partway through a billing period. Omit it to follow the contract's usage statement schedule instead.

Alerts mean something different on this model. A \$0 or percentage-remaining alert now tells you that this period's allowance is used up, which is a monthly throttling signal rather than the end of a trial, so keep a separate alert configuration for each model.

### Prepaid landing with a payment gate

For a "buy \$20 before you continue" conversion, add a [payment-gated prepaid commit](/guides/pricing-packaging/apply-credits-and-commits/manual-payment-gated-commits) to the paid contract. Metronome creates the invoice immediately and releases the commit once payment succeeds. See that guide for the billing-provider setup and the rest of the requirements.

```bash theme={null}
curl -s -X POST https://api.metronome.com/v2/contracts/edit \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "customer_id": "13117714-3f05-48e5-a6e9-a66093f13b4d",
  "contract_id": "8f2a1c64-9b7e-4d18-a5c3-6e0f2b9d4a71",
  "add_commits": [{
    "type": "PREPAID",
    "name": "Prepaid balance",
    "product_id": "3f6c8ad1-2e54-4b79-9c08-5d1ea7b46f23",
    "priority": 100,
    "access_schedule": { "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
      "schedule_items": [{ "amount": 2000, "starting_at": "2026-10-15T15:00:00Z", "ending_before": "2027-10-15T15:00:00Z" }] },
    "invoice_schedule": { "credit_type_id": "2714e483-4ff1-48e4-9e25-ac732e8f24f2",
      "schedule_items": [{ "amount": 2000, "timestamp": "2026-10-15T15:00:00Z" }] },
    "payment_gate_config": { "payment_gate_type": "STRIPE", "tax_type": "STRIPE" }
  }]
}'
```

For auto-recharge once the customer is paying, add a [prepaid balance threshold](/guides/customers-billing/optimize-customer-experience/prepaid-balance-thresholds) to the contract.

## Behavior reference

### How each trial-end path shows up

| Path to zero balance | `credit.segment.end` | `contract.end` | `$0` balance alert | Ledger (API literal) |
| - | - | - | - | - |
| Usage exhaustion | no (segment still open) | no | **fires**, `triggered_by: usage` | `CREDIT_AUTOMATED_INVOICE_DEDUCTION` |
| Natural expiry (`ending_before`) | **fires at the boundary** | no (contract stays open) | **does not fire** | `CREDIT_EXPIRATION` |
| Conversion to a paid contract | fires at transition instant | fires | fires later, once the trial invoice finalizes | deduction + `CREDIT_EXPIRATION` at transition time |
| `/v2/contracts/edit` truncation | fires | no | fires, no `triggered_by` | `CREDIT_EXPIRATION` |
| `credits/archive` | fires | no | fires, no `triggered_by` | **ledger deleted** |

The \$0 balance alert is a usage signal. [Threshold notifications](/guides/customers-billing/set-up-notifications/threshold-notifications) are evaluated when usage is ingested and when customer metadata changes, so a credit that reaches its end date with unused balance does not produce one. Use `credit.segment.end` to detect a trial that ends on schedule, and keep the \$0 alert for trials exhausted by usage.

### Payload differences between the two notification families

| | [System notifications](/guides/customers-billing/set-up-notifications/system-notifications) (`credit.*`, `contract.*`) | [Threshold notifications](/guides/customers-billing/set-up-notifications/threshold-notifications) (`alerts.*`) |
| - | - | - |
| Fields live under | the top level | `properties` |
| `environment_type` | yes | no |
| Custom fields | credit, contract, and customer | none |
| Object ids | `credit_id`, `contract_id`, `segment_id` | `customer_id`, plus `credit_id` on days-remaining |
| Timestamp | the object's configured time | the evaluation time, and absent on days-remaining |

### Things that are easy to get backwards

* The percentage alert's `threshold` is the percentage **remaining**, so a threshold of 25 fires when 25% is left.
* Ledger entry types are named differently in each surface: `CREDIT_EXPIRATION` in the API is `credit_segment_expiration` in Data Export and in-app reports. See the [database reference](/guides/reporting-insights/data-export/database-reference) for the full mapping.
* Usage drawdown appears as a single deduction entry per invoice, dated at the end of the service period and rewritten in place, so don't use the ledger to date individual usage.
* An invoice's `issued_at` is its nominal date, while finalization happens after the [grace period](/guides/implement-metronome/core-concepts/how-invoicing-works).
