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.
Use this guide when you want an external workflow to change the promotion on a subscription after each order. The example campaign gives customers a larger discount on their next orders when they keep their subscription active.
The example campaign works like this:
- The customer starts with discount code
ORDER2EARN for 10% off.
- After the first confirmed order, the next order gets 20% off.
- After the second confirmed order, the next order gets 30% off.
- After the third order, no more campaign discounts are added.
How it works
Regular discount codes apply one promotion until they expire or hit their limits. To change the discount over time, use a webhook and the GraphQL API:
- Firmhouse sends an Order Confirmed webhook.
- Your webhook handler checks whether the subscription used the campaign discount code.
- Your handler fetches the subscription with the GraphQL API.
- Your handler checks which campaign promotions have already been applied.
- Your handler applies the next promotion with
applyPromotionToSubscription.
Requirements
- A project access token with Write access.
- Three promotions in Firmhouse, for example 10%, 20%, and 30%.
- A discount code that applies the first promotion.
- A deployed HTTP endpoint that can receive Firmhouse webhooks.
Create three discounts in Firmhouse, for example:
- First order discount: 10%
- Second order discount: 20%
- Third order discount: 30%
Set Limit discount per customer to 1 for each promotion so the active promotion is disabled after it has been used.
Then create a discount code, for example ORDER2EARN, that applies the first discount.
Create an Order Confirmed webhook in Firmhouse. Include the subscription token and applied discount codes in the payload.
{% assign codes = subscription.applied_promotions | map: "discount_code" | map: "code" | compact %}
{% assign codes_string = codes | join: '","' %}
{
"event": "order_confirmed",
"subscription": {
"token": "{{ subscription.token }}",
"codes": {% if codes.size > 0 %}["{{ codes_string }}"]{% else %}[]{% endif %}
}
}
Use Order Confirmed because Firmhouse updates promotion limits when an order is confirmed. When your handler receives the event, it can check whether the current promotion is still active and decide whether to apply the next one.
Firmhouse sends webhook payloads as text/plain. If your framework depends on Content-Type to parse JSON automatically, parse the body manually with JSON.parse.
Read the webhook payload
The exact code depends on your runtime. In a JavaScript handler, extract the payload and project access token first.
const PROJECT_ACCESS_TOKEN = process.env.PROJECT_ACCESS_TOKEN;
const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
const payload = body.subscription;
Exit early when the campaign code is not present.
const DISCOUNT_CODE = "ORDER2EARN";
if (!payload.codes.includes(DISCOUNT_CODE)) {
return;
}
Collect the promotion IDs in the order they should be applied. You can find a promotion ID in the URL when editing a discount in Firmhouse:
/projects/<PROJECT_ID>/order_discount_promotions/<PROMOTION_ID>/edit
Then configure the IDs in your handler:
const PROMOTION_IDS = ["116728", "116729", "116730"];
The first promotion ID is the promotion applied by the discount code. Keeping it in the list lets the handler determine what has already been applied.
Fetch the subscription
Fetch the subscription and its applied promotions with getSubscription.
async function getSubscription(projectAccessToken, subscriptionToken) {
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 GetSubscription($token: ID!) {
getSubscription(token: $token) {
id
appliedPromotions {
active
promotion {
id
percentDiscount
title
}
discountCode {
code
expired
}
}
}
}
`,
variables: {
token: subscriptionToken,
},
}),
});
const body = await response.json();
return body.data.getSubscription;
}
If one of the campaign promotions is still active, do not apply another promotion.
const subscription = await getSubscription(PROJECT_ACCESS_TOKEN, payload.token);
const campaignPromotionIds = new Set(PROMOTION_IDS);
const { appliedPromotions } = subscription;
const activePromotion = appliedPromotions.find((ap) => {
return ap.active && campaignPromotionIds.has(ap.promotion.id);
});
if (activePromotion) {
console.log(
`Subscription ${subscription.id} already has active promotion ${activePromotion.promotion.id}`
);
return;
}
Find the first promotion in the campaign list that has not been applied yet.
const appliedPromotionIds = new Set(
appliedPromotions.map((ap) => ap.promotion.id)
);
const nextPromotionId = PROMOTION_IDS.find((id) => {
return !appliedPromotionIds.has(id);
});
if (!nextPromotionId) {
console.log("There is no campaign promotion left to add");
return;
}
Apply the next promotion with applyPromotionToSubscription.
async function applyPromotionToSubscription(subscriptionId, promotionId, projectAccessToken) {
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 ApplyPromotionToSubscription($input: ApplyPromotionToSubscriptionInput!) {
applyPromotionToSubscription(input: $input) {
errors {
message
attribute
path
}
}
}
`,
variables: {
input: {
subscriptionId,
promotionId,
},
},
}),
});
const body = await response.json();
const { errors } = body.data.applyPromotionToSubscription;
if (errors.length > 0) {
console.log(errors);
}
}
await applyPromotionToSubscription(
subscription.id,
nextPromotionId,
PROJECT_ACCESS_TOKEN
);
Secure the webhook endpoint
Firmhouse supports basic authentication for webhooks. Configure a username and password in the webhook settings, then validate the Authorization header in your handler.
function validBasicAuth(authorizationHeader, username, password) {
const encoded = Buffer.from(`${username}:${password}`).toString("base64");
return authorizationHeader === `Basic ${encoded}`;
}
If the credentials do not match, return early without applying a promotion.
Test and deploy
- Deploy your webhook endpoint.
- Configure the Firmhouse webhook in Apps > Webhooks.
- Create a subscription using the campaign discount code.
- Complete the first payment so the order is confirmed.
- Check your webhook logs.
- Open the customer in Firmhouse and verify the next discount was applied.
- Deploy the workflow for production events.