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

# Create a storefront app

> Build a React or Next.js storefront on top of Firmhouse using the GraphQL API.

Use this guide when you are building a React, Next.js, or other JavaScript storefront that needs to create subscriptions in Firmhouse.

The flow is:

1. Fetch products and plans from Firmhouse.
2. Create a cart.
3. Add products to the cart.
4. Let customers update product quantities or remove products.
5. Update address and customer details.
6. Create a payment link and redirect the customer to payment.

## Requirements

Before you start:

* Use a Firmhouse project on the **Advanced plan or higher**. API integrations are not available on the **Base plan**.
* Create the products you want to sell in Firmhouse.
* For Product-as-a-Service projects, create at least one plan.
* Set up a payment provider.
* Generate a project access token in **Settings > Integrations**. For client-side storefront calls, use a token with **Storefront** access.

## Fetch products

Use the `products` query to show products configured in the Firmhouse Portal.

```javascript theme={null}
const projectAccessToken = "<Your-project-access-token>";

const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  },
  body: JSON.stringify({
    query: `
      query Products($after: String, $first: Int) {
        products(after: $after, first: $first) {
          totalCount
          pageInfo {
            endCursor
            hasNextPage
          }
          nodes {
            id
            title
            slug
            available
            imageUrl
            priceCents
            priceWithSymbol
            productType
            sku
          }
        }
      }
    `,
    variables: {
      after: null,
      first: 10,
    },
  }),
});

const body = await response.json();
const { nodes: products, pageInfo } = body.data.products;
```

Use `after` and `first` for pagination. Pass `pageInfo.endCursor` as `after` to fetch the next page.

## Fetch plans

For Product-as-a-Service projects, use the `plans` query to show the plans configured in Firmhouse.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  },
  body: JSON.stringify({
    query: `
      query Plans($after: String, $first: Int) {
        plans(after: $after, first: $first) {
          totalCount
          pageInfo {
            endCursor
            hasNextPage
          }
          nodes {
            id
            name
            slug
            available
            currency
            monthlyAmountCents
            initialAmountIncludingTaxCents
            planProducts {
              quantity
              product {
                id
              }
            }
          }
        }
      }
    `,
    variables: {
      after: null,
      first: 10,
    },
  }),
});

const body = await response.json();
const { nodes: plans } = body.data.plans;
```

## Create a cart

Create a cart with `createCart`. Firmhouse returns a subscription token for the draft subscription.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  },
  body: JSON.stringify({
    query: `
      mutation CreateCart {
        createCart(input: {}) {
          subscription {
            token
          }
        }
      }
    `,
  }),
});

const body = await response.json();
const subscriptionToken = body.data.createCart.subscription.token;
```

Pass this token in the `X-Subscription-Token` header for later cart calls. In a web app, store it in a cookie or local storage so returning customers can continue the same checkout.

If the default plan includes products, those products are automatically added when you create the cart.

## Add products to the cart

Use `createOrderedProduct` to add a product.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation CreateOrderedProduct($input: CreateOrderedProductInput!) {
        createOrderedProduct(input: $input) {
          orderedProduct {
            id
            title
            quantity
            priceIncludingTaxCents
            totalAmountIncludingTaxCents
          }
          subscription {
            monthlyAmountCents
            currency
            orderedProducts {
              id
              title
              quantity
              totalAmountIncludingTaxCents
            }
          }
          errors {
            attribute
            message
            path
          }
        }
      }
    `,
    variables: {
      input: {
        productId: products[0].id,
        quantity: 1,
      },
    },
  }),
});

const body = await response.json();
const { orderedProduct, subscription, errors } = body.data.createOrderedProduct;
```

Use `orderedProduct` to show an added-to-cart confirmation. Use `subscription` to update the cart UI with totals and cart contents.

If you need to add metadata to ordered products, use a project access token with **Write** access and pass `metadata`. If multiple rows of the same product need different metadata, pass `ensureNewRecord: true`.

Use `createOrderedProductV2` when you need the same cart behavior with extra options such as `orderedProductType`. Set `orderedProductType` to `ONE_OFF_ADDON` when the product should only be included in the next order.

## Remove products from the cart

Use `destroyOrderedProduct` to remove a product from the cart.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation DestroyOrderedProduct($input: DestroyOrderedProductInput!) {
        destroyOrderedProduct(input: $input) {
          orderedProduct {
            id
          }
          subscription {
            monthlyAmountCents
            orderedProducts {
              id
              title
              quantity
            }
          }
        }
      }
    `,
    variables: {
      input: {
        id: orderedProduct.id,
      },
    },
  }),
});

const { data } = await response.json();
const { subscription } = data.destroyOrderedProduct;
```

## Update product quantities

Use `updateOrderedProductQuantity` to update quantities in the cart.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation UpdateOrderedProductQuantity($input: UpdateOrderedProductQuantityInput!) {
        updateOrderedProductQuantity(input: $input) {
          orderedProduct {
            id
            quantity
          }
          subscription {
            monthlyAmountCents
            orderedProducts {
              id
              title
              quantity
            }
          }
          errors {
            attribute
            message
            path
          }
        }
      }
    `,
    variables: {
      input: {
        orderedProduct: {
          id: orderedProduct.id,
          quantity: 2,
        },
      },
    },
  }),
});

const body = await response.json();
const { subscription, errors } = body.data.updateOrderedProductQuantity;
```

## Update the subscription plan

Use `updatePlan` to change the active plan for the draft subscription.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation UpdatePlan($planSlug: String!) {
        updatePlan(input: { planSlug: $planSlug }) {
          subscription {
            activePlan {
              id
              name
              slug
            }
            subscribedPlan {
              id
              name
              nextBillingDate
            }
          }
        }
      }
    `,
    variables: {
      planSlug: plans[0].slug,
    },
  }),
});

const body = await response.json();
const { subscription } = body.data.updatePlan;
```

## Fetch current cart information

Use `getSubscription` to fetch the current cart state. This is useful when customers return to the storefront or when they navigate to checkout.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      query GetSubscription($token: ID!) {
        getSubscription(token: $token) {
          amountForStartingSubscriptionCents
          monthlyAmountCents
          checkoutUrl
          currency
          name
          lastName
          email
          orderedProducts {
            id
            title
            quantity
            totalAmountIncludingTaxCents
          }
        }
      }
    `,
    variables: {
      token: subscriptionToken,
    },
  }),
});

const body = await response.json();
const subscription = body.data.getSubscription;
```

If you want to use the default Firmhouse checkout page, redirect customers to `subscription.checkoutUrl`.

## Update address details

Use `updateAddressDetails` to collect billing, invoice, and shipping details.

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation UpdateAddressDetails($input: UpdateAddressDetailsInput!) {
        updateAddressDetails(input: $input) {
          subscription {
            id
            name
            lastName
            email
            address
            city
            country
          }
          errors {
            attribute
            message
            path
          }
        }
      }
    `,
    variables: {
      input: {
        name: "John",
        lastName: "Doe",
        email: "johndoe@example.com",
        address: "Example street 1",
        city: "Rotterdam",
        country: "NL",
        termsAccepted: true,
      },
    },
  }),
});

const { data = {}, errors: requestErrors } = await response.json();
const { subscription, errors } = data.updateAddressDetails ?? {};
```

By default, Firmhouse requires `name`, `email`, `address`, `city`, and `country` before payment. You can change the required fields in **Checkout > Preferences > Customer fields** in your Firmhouse project.

## Generate a payment link

Use `createSubscriptionFromCart` to create a `paymentUrl`. Redirect the customer to that URL to complete payment.

<Note>
  The supported headless checkout flow hands off payment to a hosted payment page: Firmhouse-hosted for Adyen projects, or the payment provider's own checkout page for other providers such as Mollie. The GraphQL API does not expose Adyen session data or an embedded Adyen Drop-in configuration for rendering the payment-method picker in your own frontend. Your `returnUrl` still determines where the customer goes after completing payment.
</Note>

```javascript theme={null}
const response = await fetch("https://portal.firmhouse.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
    "X-Subscription-Token": subscriptionToken,
  },
  body: JSON.stringify({
    query: `
      mutation CreateSubscriptionFromCart($input: CreateSubscriptionFromCartInput!) {
        createSubscriptionFromCart(input: $input) {
          paymentUrl
          returnUrl
          errors {
            attribute
            message
            path
          }
        }
      }
    `,
    variables: {
      input: {
        returnUrl: "https://yourdomain.com/return",
        paymentPageUrl: "https://yourdomain.com/checkout",
      },
    },
  }),
});

const body = await response.json();
const { paymentUrl, returnUrl, errors } = body.data.createSubscriptionFromCart;
```

If required fields are missing, `paymentUrl` is `null` and `errors` explains what still needs to be collected.

Submit one checkout request at a time per cart. An overlapping request returns the existing cart and subscription alongside a checkout-in-progress error, with no payment or payment link; use the original request's response to continue. A later checkout request rebuilds the payment link from the current cart. If a request times out or checkout remains in progress, do not automatically retry payment creation: contact the store so it can confirm the previous attempt's outcome.

<Warning>
  If you have not added a payment provider to your Firmhouse project, creating a payment link will fail.
</Warning>

## Related

* [GraphQL API introduction](https://developer.firmhouse.com/graphql-api/introduction)
* [API Reference](https://developer.firmhouse.com/graphql-api/api-reference)
* [Generate API access tokens](/integrations/api-access-tokens)
