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

# Promotional offers

> Configure targeted offers that customers can accept during cancellation, from Customer Portal v2, or through a secure link sent by a workflow.

Promotional offers let you define a reusable retention or loyalty offer, choose which subscriptions qualify, and control what happens when a customer accepts it. Firmhouse saves the exact copy, benefit, and commitment shown to each customer, so later edits apply only to newly issued offers.

## Enable promotional offers

Promotional offers are part of the Workflow Builder preview.

1. In your Firmhouse project, open **New Features**.
2. Enable **Workflow Builder**.
3. Open **Offers** from the sidebar.

Projects without this preview enabled cannot open offer-management pages, show promotional offers, or issue them through GraphQL.

## Create an offer

1. Open **Offers** and select **New offer**.
2. Enter an internal name and choose whether the offer is a draft, active, or archived.
3. Set its priority and acceptance window.
4. Add the customer-facing headline, explanation, and benefit summary for every language enabled on the project.
5. Add any eligibility conditions.
6. Choose one benefit and, optionally, a minimum term.
7. Save the offer and make it active when it is ready for customers.

Draft and archived offers are not issued. When several active offers match, Firmhouse uses their priority to select the first eligible offer.

## Choose who qualifies

You can target an offer using:

* Cancellation reason
* Subscription status
* Number of completed deliveries
* Revenue for the current subscription
* Current plan
* Current products
* Synchronized subscription segments
* Shopify order tags

For each condition, choose **Require this condition to match** or **Exclude when this condition matches**. Customers must meet every required condition. Matching any exclusion makes them ineligible. Create separate prioritized offers when you want to address alternative audiences. The segment condition is available only when the project has synchronized subscription segments.

Firmhouse checks eligibility again when the customer accepts. An expired offer or one that no longer applies cannot grant its benefit.

### Exclude subscriptions from an offer

In the **Eligibility** tab, add a condition and choose **Exclude when this condition matches**:

* **Exclude a prepaid plan:** Choose **Current plan**, then select the prepaid plan under **Plan is**. Add a separate exclusion for each plan. New prepaid plans are not excluded automatically.
* **Exclude subscriptions that already have a product:** Choose **Current products**, select **Has any of**, and choose the products. Having any selected product active on the subscription makes it ineligible.

The exclusion applies to the complete rule. For example, excluding **Current products → Has none of** makes subscriptions without those products ineligible. To exclude subscriptions that have the products, use **Has any of** with the exclusion choice, or keep **Has none of** as a required condition.

Issued offers keep the conditions saved when they were issued. Editing an offer's conditions generally affects newly issued offers only, but changing a cancellation-reason condition can also make an issued cancellation offer unavailable. Saved conditions are checked against the subscription's current state, so switching to an excluded plan or adding an excluded product can make an issued offer unavailable.

An excluded **Cancellation reason** allows the offer for other cancellation reasons. Offers with a cancellation-reason condition remain limited to cancellation flows; they do not appear in the regular portal where no cancellation reason exists.

### Target subscriptions using Shopify order tags

Choose **Shopify order tags** as a condition, then search and select one or more tags in the dropdown. The choices come from Shopify tags synchronized to this project's orders. For example, select `SUMMER_SALE` to target subscriptions with an order carrying that campaign tag. Tags already saved on the offer remain selected even if they no longer appear on an order.

A subscription qualifies when any of its orders has any of the entered tags. Tags match exactly, including capitalization. Orders from another subscription do not qualify it, even when both subscriptions use the same email address.

Firmhouse synchronizes tags from Shopify when an order is linked and when Shopify sends an order update. Removing a tag can make the offer unavailable, including for a link sent earlier. Previously linked orders need their tags synchronized before they can qualify; contact Firmhouse if historical campaign orders are missing tags.

For a campaign shown only in the second-order reminder, use the order number to control the email block. Add an order-count condition to the offer only if acceptance should also stop after that order.

## Choose the benefit

Each active offer has one benefit:

* Apply an existing active promotion
* Add a complimentary one-off product
* Swap a current product
* Switch to another plan in the same plan group

You can also add a minimum term in days, weeks, months, or years. Customers see this commitment before accepting. Products added as complimentary gifts keep their price and quantity locked.

## Show offers during cancellation

After a customer selects a cancellation reason, Firmhouse can show the first matching promotional offer before the normal churn-prevention action. The customer can accept or decline the offer, or choose the existing pause, skip, postponement, discount, or cancellation option shown with it.

In the offer editor’s **Eligibility** tab, add a **Cancellation reason** condition to show an offer during cancellation for that reason. The selected reason shows its configured alternative action and an **Edit cancellation reason** link. Offers without a cancellation-reason condition are not shown during cancellation.

You can also start from **Settings → Churn prevention**. Open a reason, check **Create an offer for this reason**, and choose **Save and create offer** to open a draft with that reason preselected. Save the offer to return to the reason. Existing offers can be edited from the same reason screen.

Only active, automatic offers are selected automatically during cancellation. Draft, archived, and manual offers are not. Inactive or deleted cancellation reasons cannot be selected by customers, and pause-flow reasons do not trigger promotional offers.

The reason’s **Alternative action** is independent of offer eligibility. Pause or skip remains available alongside an eligible promotional offer and when the offer is unavailable or declined. **Continue to cancellation** adds no pause or skip alternative; it does not prevent a promotional offer from appearing.

Accepted offers apply their configured benefit and optional minimum term, and record the churn request as prevented. Declining returns the customer to the normal cancellation flow.

For the surrounding cancellation setup, see [Cancellation feedback and winback flows](/configure/activation-and-cancellation/cancellation-feedback-prevention).

## Share an offer outside cancellation

Workflow Builder and the GraphQL API can issue a customer-specific offer and return its plain acceptance URL. In a Liquid email or notification, pipe that URL through `ssc_hotlink` before sending it to a customer. Configure the project's public URL before sending offer links.

```liquid theme={null}
{% assign offer_url = "{{ steps.issue_offer.createPromotionalOffer.promotionalOffer.acceptUrl }}" %}
<a href="{{ offer_url | ssc_hotlink }}">Review your offer</a>
```

Opening the signed link authenticates the customer for that subscription with limited hotlink access. Customers who already have full Customer Portal access can also open the offer from their dashboard. An unsigned offer URL redirects to Customer Portal login. Accepting requires an explicit confirmation, so an email link scanner cannot accept the offer automatically. Reusing the same idempotency key returns the same issued offer instead of creating a duplicate.

See [Workflow Builder](/configure/workflows/workflow-builder#use-promotional-offers-in-workflows) for the workflow setup.

## Show offers in Customer Portal v2

The default Customer Portal v2 dashboard shows the highest-priority pending eligible offer. Custom dashboard templates can use `{% promotional_offer %}` or loop over `subscription.eligible_promotional_offers`.

See the [dashboard template reference](/customer-portal-v2/components/dashboard-template#promotional_offer) for Liquid examples.

## Review issued offers

Open a subscription in Firmhouse to review its issued promotional offers and their current status. An issued offer records whether it was viewed, accepted, rejected, or expired.

## Find customers by offer interaction

On **Offers**, click an offer's **issued**, **accepted**, or **declined** subscription count to open the matching Customers list. Each count represents distinct subscriptions: a subscription that received the same offer several times appears once in that list.

You can also open **Customers** and add these filters:

1. **Offer:** Choose the offer, including an archived offer with history.
2. **Offer interaction:** Choose **Issued**, **Accepted**, or **Declined**. Selecting only an offer defaults to **Issued**.
3. **Offer interaction date:** Optionally choose when the interaction happened. The dates include the whole selected day in Firmhouse's Amsterdam timezone.

**Accepted** means the offer was successfully accepted. **Declined** also includes choosing another action during cancellation; it does not include expiry, leaving the page, or an offer cancelled by the system. **Issued** means an offer instance was saved, not that an email was delivered or the customer saw it.

These filters use historical interactions across all revisions of the offer. A customer who accepted and later cancelled remains in the accepted group. Add a subscription status filter to inspect their current status. A subscription can appear in both accepted and declined groups if it received separate offers.

Use the existing customer export or save the filters as a view. Saved views run the filters again when opened. Fixed dates keep the same historical period; a preset such as **This month** moves with the calendar. Customer rows represent subscriptions, so a person with several subscriptions may have several rows.

### Analyze the group with Claude or Oppy

Connect your agent to [Firmhouse MCP](/agent-and-mcp/connect-your-agent), then ask:

> Find subscriptions that accepted \[offer name] between September 1 and September 30. Show their orders after acceptance and their current subscription status. For repeat acceptances, use the first acceptance in that period as the starting point and state that choice.

The GraphQL API provides `offerDefinitions` to find the offer, `subscriptions` for distinct subscriptions, and `promotionalOffers` for the matching offer instances and timestamps. Both connections accept `offerDefinitionId`, `offerEvent`, `offerEventSince`, and `offerEventUntil` as direct filter arguments. Queries do not issue offers or require tags. Use the same offer filters for the subscription list and offer history, and paginate both connections.

The API event `REJECTED` corresponds to **Declined** in the admin. Selecting only `offerDefinitionId` includes all issued offers, regardless of their current status. Event filters require an offer definition, and date filters require an explicit `offerEvent`. To retrieve issued-offer history without selecting a particular event or offer, query `promotionalOffers` without offer filters; optionally use `subscriptionId` to restrict it to one subscription.

For example, after finding the offer ID:

```graphql theme={null}
query OfferCohort(
  $offerDefinitionId: ID!
  $offerEvent: PromotionalOfferEvent!
  $offerEventSince: ISO8601DateTime
  $offerEventUntil: ISO8601DateTime
) {
  subscriptions(
    offerDefinitionId: $offerDefinitionId
    offerEvent: $offerEvent
    offerEventSince: $offerEventSince
    offerEventUntil: $offerEventUntil
    first: 50
  ) {
    nodes { id subscriptionAccountId status }
    pageInfo { endCursor hasNextPage }
  }
  promotionalOffers(
    offerDefinitionId: $offerDefinitionId
    offerEvent: $offerEvent
    offerEventSince: $offerEventSince
    offerEventUntil: $offerEventUntil
    first: 50
  ) {
    nodes { id subscriptionId acceptedAt definitionRevision benefit commitment }
    pageInfo { endCursor hasNextPage }
  }
}
```

```json theme={null}
{
  "offerDefinitionId": "YOUR_OFFER_ID",
  "offerEvent": "ACCEPTED",
  "offerEventSince": "2026-09-01T00:00:00+02:00",
  "offerEventUntil": "2026-10-01T00:00:00+02:00"
}
```

API date ranges include the start and exclude the end. Subscription results count subscriptions; offer history results count individual offer instances. The customer admin lists completed signups and customers pending completion; the API defaults to all non-draft subscriptions, so these populations can differ for incomplete signups. Group by `subscriptionAccountId` only when you want account-level analysis instead of subscription-level analysis.

Recorded `viewedAt` timestamps and the API's `VIEWED` filter have incomplete historical coverage. Some portal offers can be displayed without a saved offer record. An absent view timestamp does not prove the customer never saw the offer, and an issued count is not a shown count.

## Troubleshooting

* **Offers is missing from the sidebar:** Enable **Workflow Builder** under **New Features**.
* **No offer is shown:** Confirm that the offer is active, has not expired, and that every condition and selected benefit applies to the subscription.
* **A segment condition is unavailable:** Confirm that subscription segments have been synchronized for the project.
* **An offer hotlink cannot be created:** Configure the project's public URL, ensure the issuing project has Workflow Builder enabled, and apply `ssc_hotlink` to the returned acceptance URL in Liquid.
