> ## 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.

# Show promotional offers in a headless portal

> Configure offers through GraphQL or MCP and display currently eligible offers in your own customer portal.

Configure the offer and its eligibility rules in Firmhouse, then ask Firmhouse which offers to show for a subscription. Your portal does not need to reproduce the rules.

Call these APIs from your backend with a [project API access token](/integrations/api-access-tokens.md). Reading offers requires **Read** or **Write** access; creating and editing definitions requires **Write** access. Authenticate your customer and authorize access to the requested subscription in your own backend before querying. Keep the project token on the server.

## Create an offer definition

An offer definition describes the customer-facing copy, eligibility conditions, benefit, and any commitment. New definitions default to `DRAFT`. An `ACTIVE` definition with `AUTOMATIC` preparation becomes available to eligible subscriptions without sending emails or issuing offers in advance.

Creating and editing offer definitions through the API or MCP requires **Workflow Builder** to be enabled for your project. Contact the Firmhouse team if it is not available.

```graphql theme={null}
mutation CreateOffer($input: CreateOfferDefinitionInput!) {
  createOfferDefinition(input: $input) {
    offerDefinition { id slug status revision lockVersion }
    errors { attribute message }
  }
}
```

Example variables, using an existing promotion from the same project:

```json theme={null}
{
  "input": {
    "name": "Loyal customer discount",
    "status": "ACTIVE",
    "preparationMode": "AUTOMATIC",
    "content": {
      "en": { "title": "Thank you for staying with us", "description": "Save on your next delivery." }
    },
    "requirements": [
      { "attribute": "confirmed_order_count", "operator": "greater_than_or_equal", "value": 3 }
    ],
    "benefit": { "type": "promotion", "promotion_id": 123, "label": "10% off your next delivery" },
    "validityPeriodDays": 30
  }
}
```

Include a title in the project's primary locale. Add other locales as needed. All requirements must match; an empty list targets every subscription for which the benefit and commitment apply. Benefits reference project records such as promotions, products, and plans; they do not create those records.

Requirement attributes include `subscription_status`, `confirmed_order_count`, `subscription_revenue`, `subscription_segment`, `current_plan`, `current_products`, and `shopify_order_tags`. Status values are `activated`, `paused`, and `cancelled`, using `equal`. Counts and revenue support `equal`, `greater_than_or_equal`, and `less_than_or_equal`. Revenue values are integer cents and require a `currency` key. Conditions requiring a cancellation reason or a chosen product need that interaction context and will not normally match a general portal listing.

Set `preparationMode` to `MANUAL` when an offer should appear only after explicit issuance. Issuance uses the existing `createPromotionalOffer` mutation, available with Workflow Builder enabled.

## Show eligible offers

```graphql theme={null}
query OffersForPortal($subscriptionId: ID!) {
  eligiblePromotionalOffers(subscriptionId: $subscriptionId) {
    id
    offerDefinition { id slug }
    definitionRevision
    content
    benefit
    commitment
    expiresAt
    acceptUrl
  }
}
```

The result follows Firmhouse's portal ordering and combines eligible issued offers with unissued automatic offers. An unissued offer has `id: null`; use the definition ID as its identity. Reading this list does not issue an offer or send a notification. Expiry is provisional for unissued offers and is set at issuance.

Use the customer's locale to select a title and description from `content`, falling back to the project's primary locale. `benefit` and `commitment` contain the terms of this particular offer. For issued offers, those terms retain their issuance snapshot, while `content` reflects the definition's current copy.

Direct the customer to `acceptUrl` to review and accept the offer through Firmhouse. The customer must authenticate as the matching subscription; the URL does not sign them in. Eligibility is checked again during acceptance. Acceptance entirely within your own portal is not provided by these operations.

## Read issued history

```graphql theme={null}
query IssuedOffers($subscriptionId: ID!, $after: String) {
  promotionalOffers(subscriptionId: $subscriptionId, first: 20, after: $after) {
    nodes { id status content benefit commitment expiresAt acceptedAt }
    pageInfo { hasNextPage endCursor }
  }
}
```

History includes offers that are no longer eligible. Use `eligiblePromotionalOffers` to decide what to display, rather than treating every issued offer as available.

## Edit or archive a definition

Read the definition with `offerDefinitions(id: ...)` or `offerDefinitions(slug: ...)`. Both return a connection; select the definition and its `lockVersion` under `nodes`:

```graphql theme={null}
query OfferToEdit($id: ID!) {
  offerDefinitions(id: $id) {
    nodes { id slug lockVersion }
  }
}
```

Pass that version as `expectedLockVersion` to prevent overwriting another editor's changes.

```graphql theme={null}
mutation EditOffer($input: UpdateOfferDefinitionInput!) {
  updateOfferDefinition(input: $input) {
    offerDefinition { id status revision lockVersion }
    errors { attribute message }
  }
}
```

For example, archive with `{"input":{"id":"456","expectedLockVersion":0,"status":"ARCHIVED"}}`.

Omitted fields remain unchanged. Supplied JSON fields replace the complete field, including all translations or requirements. Use `[]` to clear requirements and `{}` to clear an optional commitment. Archiving retains offer history and cancels pending issued offers. If the lock version is out of date, read the definition again and review your changes before retrying.

## Configure offers through MCP

After [connecting your agent](/agent-and-mcp/connect-your-agent.md), ask:

> Prepare an automatic offer for subscriptions with at least three confirmed orders. Use our existing 10% promotion, show “Thank you for staying with us”, and make each issued offer valid for 30 days.

The agent can read definitions and eligible offers through project GraphQL queries, then prepare `createOfferDefinition` or `updateOfferDefinition` using the existing [operation approval flow](/agent-and-mcp/approving-operations.md). For creation, use the current `Project` as the operation subject, once. For editing, use the `OfferDefinition` and its current lock version. The changes take effect after approval.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.