Quick Start Guide
Let’s Cook Up Some Personalization!
Welcome to Growcado — your new favorite way to show the right content to the right people (without losing your mind). In this quick start, we’re gonna create a recipe that can shapeshift depending on who’s looking at it.
Imagine this: You’ve got a tasty lasagna recipe. But some folks are into veggies 🌱, while others dream about cheese-pulling meaty goodness 🍖. With Growcado, you can create one recipe and serve the perfect version to each person. Neat, right?
👨🍳 What We’re Cooking
We’re building a Recipe content type, which lets us add all the tasty details about a dish. Then we’ll use Growcado's personalization magic to serve:
A vegan lasagna to customers who prefer plant-based meals 🥬
A classic meat lasagna to those who love the carnivore life 🐮
No forks required yet — we’re just prepping the ingredients.
📋 Prerequisites
Before we dive in, make sure you have these ready:
Growcado Account & Studio Access
You'll need access to Growcado Studio to create content types and set up audience segments.You'll Don't have an account? Fill out our form or send us an email and we'll create one for you ASAP! 🚀
Development Environment
Any environment that can make HTTP requests — whether you're building web apps with React/Vue/Angular, mobile apps with React Native/Flutter, server-side projects with Node.js, or adding personalization to an existing project.
All you need: A way to consume APIs and handle JSON responses. That's pretty much everything these days! ✨
🧱 1- Content Type Structure (a.k.a. Our Recipe Blueprint)
Before we dive in, let’s clear something up: a content type is like a blueprint — it defines what kind of data you’ll store and how it’s structured. Think of it as the mold for your content. For example, a “Recipe” content type might have fields like name, image, and ingredients. Once you’ve got that set, you can start adding actual content, like a lasagna recipe, a chocolate cake recipe, or your grandma’s mystery stew. The content type doesn’t hold specific recipes — it just tells the system what every recipe should look like.

Here’s how our recipe content is structured:
Name(text): What are we cooking? “Lasagna”, obviously.Image(media): A delicious photo to make people drool.Ingredients(reference): A list of ingredients, like pasta, tomato sauce, or vegan cheese.
Inside each Ingredient, we’ve got:
Name(text): Like "Tofu" or "Mozzarella".Amount(text): Maybe "200g" or “a sprinkle.”
This setup gives us superpowers — we can mix and match different ingredient sets (like meat vs. vegan), and plug them into the same recipe layout.
🛠️ Step-by-Step: Building the Content Types
Let’s roll up our sleeves and set up the content types we need: Recipe and Ingredient. You'll only have to do this once — think of it like prepping your pantry before the cooking starts.
🧄 Step 1: Create the Ingredient content type
Ingredient content typeHead to the Content Type Builder and click “+ New”.
Model Name:
IngredientDescription: "Stores individual ingredients with their name and amount. Used inside recipes to build full ingredient lists."
Model Identifier:
ingredient(automatically filled)
➡️ Don’t forget to click Save once you're done filling it out.

Ingredient content type with name, description, and identifier.✍️ Step 2: Add your first field — Name
NameClick Add Field, and you’ll see a list of field types.
Choose the
TextfieldSet Field Name to
NameMark it as required (because even the humblest of onions needs a name)

Text field type — the first field we’ll use.
Name field as a required text input.➡️ Once done, click Save to lock in the field.
🧪 Step 3: Add the Amount field
Amount fieldAdd another Text field called Amount.
Example values: “200g”, “2 cups”, “a dash”
Same flow as the previous one
➡️ Hit Save when finished.
🥘 Step 4: Create the Recipe content type
Recipe content typeWith your ingredients ready, let’s create a new content type: Recipe.
Model Name:
RecipeDescription: "Use this to create recipes with name, image, and a list of ingredients."
Model Identifier:
recipe
➡️ Yep, you guessed it — click Save.

Recipe content type with a description and model identifier.✏️ Step 5: Add the Name field
Name fieldJust like you did for Ingredient, add a Text field called Name.
🖼️ Step 6: Add the Image field
Image fieldChoose the Media field to add a photo to your recipe.
Field Name:
ImageYes, it’s required — people eat with their eyes first.
➡️ Click Save when you're done.

🔗 Step 7: Link the ingredients
Choose the
ReferencefieldName it
IngredientsLimit accepted content types to
Ingredientso only real food shows up here

✅ Step 8: Final check
At the end, your Recipe should have:
Name(Text)Image(Media)Ingredients(Reference toIngredient)

Recipe content type with all fields in place.🥘 2- Creating the Content Entries
Now that your content types are set, it’s time to create some real entries — like filling your digital kitchen with actual food. We'll start with adding ingredients and then create the classic version of our Lasagna recipe (no personalization yet — we’re saving the fun stuff for the next round 😏).
🧄 Step 1: Add Your Ingredients
Head to the Content section in the sidebar and select Ingredient. Click the big, beautiful + Add Entry button.

Start with your first ingredient, for example:
Name: Lasagna Noodles
Amount: 9 sheets
Don’t forget to hit Save & Publish when done!

Repeat this process to add the rest:
Tomato Sauce – 500ml
Mozzarella Cheese – 200g
Ground Beef – 300g
Vegan Ricotta – 250g
Vegan Mozzarella – 200g

🍽️ Step 2: Create the Lasagna Recipe
Now that we’ve stocked the pantry, it’s time to cook.
Head back to Content, switch to the Recipe tab, and hit + Add Entry.
Name: Lasagna
Image: Upload a delicious photo of lasagna (bonus points if it makes your users hungry)
Here's a neat trick: we can change the auto-generated Content ID to something more readable — like lasagna — by clicking the Content ID field in the right menu. We did exactly that so it’s easier to reference this content later on.

Step 3: Add Ingredients to the Recipe
Scroll down to the Ingredients field and click Add Existing Content. This will open a list of all the ingredients you just created. Select the ones that belong in this version of the recipe.

➡️ Finally, click Save & Publish. Boom — you’ve got your first recipe online!
📡 3- Getting Your Content via API
Alright, your content is cooked, published, and ready to serve — but how do you actually get it into your app or website? That’s where Growcado’s API comes in.
When you’re viewing a content entry (like our Lasagna recipe), just look at the sidebar on the right. Under the “Link” section, you’ll find the full API URL already built for you — including your tenant, model, and identifier.
Copy it, paste it, call it. Boom. 🍝
Let’s take a look at how to fetch the classic Lasagna recipe using a simple GET request.
curl 'https://api.growcado.io/cms/tenant/{tenantId}/published/{modelIdentifier}/{identifier}'tenantId
✅
Your project’s unique tenant ID
modelIdentifier
✅
The content type you’re fetching — for our case, recipe
identifier
✅
The unique content ID you set earlier — in our case, lasagna
🔍 What You Get Back
When you call the API, here's exactly what the response looks like for our Lasagna recipe: Don't panic when you see this response — it might look like a lot at first glance, but it's actually super straightforward once you know what you're looking at.
{
"id": "32226494780751473240104532705280",
"modelIdentifier": "recipe",
"identifier": "lasagna",
"fields": [
{
"identifier": "name",
"type": "TEXT",
"value": ["Lasagna"]
},
{
"identifier": "image",
"type": "MEDIA",
"value": ["https://api.growcado.io/storage/api/184549109729170412"]
},
{
"identifier": "ingredients",
"type": "REFERENCE",
"value": [
{
"id": "32226492677280317212325595578368",
"modelIdentifier": "ingredient",
"identifier": "58a42cb8-48b1-42",
"fields": [
{
"identifier": "name",
"type": "TEXT",
"value": ["Mozzarella Cheese"]
},
{
"identifier": "amount",
"type": "TEXT",
"value": ["200g"]
}
]
},
{
"id": "32226492453750652613221511331840",
"modelIdentifier": "ingredient",
"identifier": "e105ac68-9b40-47",
"fields": [
{
"identifier": "name",
"type": "TEXT",
"value": ["Ground Beef"]
},
{
"identifier": "amount",
"type": "TEXT",
"value": ["300g"]
}
]
},
{
"id": "32226491725652835616848097574912",
"modelIdentifier": "ingredient",
"identifier": "95270311-4f17-47",
"fields": [
{
"identifier": "name",
"type": "TEXT",
"value": ["Tomato Sauce"]
},
{
"identifier": "amount",
"type": "TEXT",
"value": ["500ml"]
}
]
},
{
"id": "32226484768957203194547015778304",
"modelIdentifier": "ingredient",
"identifier": "bb4b4597-a34a-49",
"fields": [
{
"identifier": "name",
"type": "TEXT",
"value": ["Lasagna Noodles"]
},
{
"identifier": "amount",
"type": "TEXT",
"value": ["9 sheets"]
}
]
}
]
}
]
}🧠 Understanding the Response Structure
Notice how the response is structured:
Top level: Contains the main content metadata (id, modelIdentifier, identifier)
Fields array: Each field has an identifier, type, and value array
Reference fields: When you reference other content (like ingredients), the full nested objects come back automatically
Values are arrays: Even single values come wrapped in arrays — this keeps the structure consistent
💻 Quick Integration Example
async function fetchRecipe(recipeId) {
const response = await fetch(`https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/${recipeId}`);
const recipe = await response.json();
// Get the recipe name
const name = recipe.fields.find(f => f.identifier === 'name').value[0];
// Get the image URL
const imageUrl = recipe.fields.find(f => f.identifier === 'image').value[0];
// Parse ingredients with their name and amount
const ingredientsData = recipe.fields.find(f => f.identifier === 'ingredients').value;
const ingredients = ingredientsData.map(ingredient => {
const name = ingredient.fields.find(f => f.identifier === 'name').value[0];
const amount = ingredient.fields.find(f => f.identifier === 'amount').value[0];
return { name, amount };
});
return { name, imageUrl, ingredients };
}
// Example usage:
fetchRecipe('lasagna').then(recipe => {
console.log(recipe.name); // "Lasagna"
console.log(recipe.ingredients);
// [
// { name: "Mozzarella Cheese", amount: "200g" },
// { name: "Ground Beef", amount: "300g" },
// { name: "Tomato Sauce", amount: "500ml" },
// { name: "Lasagna Noodles", amount: "9 sheets" }
// ]
});import Foundation
struct Ingredient {
let name: String
let amount: String
}
struct Recipe {
let name: String
let imageUrl: String
let ingredients: [Ingredient]
}
func fetchRecipe(recipeId: String) async throws -> Recipe {
let url = URL(string: "https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/\(recipeId)")!
let (data, _) = try await URLSession.shared.data(from: url)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let fields = json["fields"] as! [[String: Any]]
// Get recipe name
let nameField = fields.first { $0["identifier"] as! String == "name" }!
let name = (nameField["value"] as! [String])[0]
// Get image URL
let imageField = fields.first { $0["identifier"] as! String == "image" }!
let imageUrl = (imageField["value"] as! [String])[0]
// Parse ingredients
let ingredientsField = fields.first { $0["identifier"] as! String == "ingredients" }!
let ingredientsData = ingredientsField["value"] as! [[String: Any]]
let ingredients = ingredientsData.map { ingredientData in
let fields = ingredientData["fields"] as! [[String: Any]]
let nameField = fields.first { $0["identifier"] as! String == "name" }!
let amountField = fields.first { $0["identifier"] as! String == "amount" }!
return Ingredient(
name: (nameField["value"] as! [String])[0],
amount: (amountField["value"] as! [String])[0]
)
}
return Recipe(name: name, imageUrl: imageUrl, ingredients: ingredients)
}
// Example usage:
Task {
let recipe = try await fetchRecipe(recipeId: "lasagna")
print(recipe.name) // "Lasagna"
print(recipe.ingredients)
// [Ingredient(name: "Mozzarella Cheese", amount: "200g"), ...]
}import kotlinx.coroutines.*
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
data class Ingredient(
val name: String,
val amount: String
)
data class Recipe(
val name: String,
val imageUrl: String,
val ingredients: List<Ingredient>
)
suspend fun fetchRecipe(recipeId: String): Recipe = withContext(Dispatchers.IO) {
val url = URL("https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/$recipeId")
val connection = url.openConnection() as HttpURLConnection
val response = connection.inputStream.bufferedReader().readText()
val json = JSONObject(response)
val fields = json.getJSONArray("fields")
// Get recipe name
val nameField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "name" }
val name = nameField.getJSONArray("value").getString(0)
// Get image URL
val imageField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "image" }
val imageUrl = imageField.getJSONArray("value").getString(0)
// Parse ingredients
val ingredientsField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "ingredients" }
val ingredientsData = ingredientsField.getJSONArray("value")
val ingredients = (0 until ingredientsData.length()).map { i ->
val ingredient = ingredientsData.getJSONObject(i)
val ingredientFields = ingredient.getJSONArray("fields")
val nameField = (0 until ingredientFields.length()).map { ingredientFields.getJSONObject(it) }
.first { it.getString("identifier") == "name" }
val amountField = (0 until ingredientFields.length()).map { ingredientFields.getJSONObject(it) }
.first { it.getString("identifier") == "amount" }
Ingredient(
name = nameField.getJSONArray("value").getString(0),
amount = amountField.getJSONArray("value").getString(0)
)
}
Recipe(name, imageUrl, ingredients)
}
// Example usage:
runBlocking {
val recipe = fetchRecipe("lasagna")
println(recipe.name) // "Lasagna"
println(recipe.ingredients)
// [Ingredient(name=Mozzarella Cheese, amount=200g), ...]
}import requests
from typing import List, Dict, Any
def fetch_recipe(recipe_id: str) -> Dict[str, Any]:
url = f"https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/{recipe_id}"
response = requests.get(url)
data = response.json()
fields = data["fields"]
# Get recipe name
name_field = next(f for f in fields if f["identifier"] == "name")
name = name_field["value"][0]
# Get image URL
image_field = next(f for f in fields if f["identifier"] == "image")
image_url = image_field["value"][0]
# Parse ingredients
ingredients_field = next(f for f in fields if f["identifier"] == "ingredients")
ingredients_data = ingredients_field["value"]
ingredients = []
for ingredient_data in ingredients_data:
fields = ingredient_data["fields"]
name_field = next(f for f in fields if f["identifier"] == "name")
amount_field = next(f for f in fields if f["identifier"] == "amount")
ingredients.append({
"name": name_field["value"][0],
"amount": amount_field["value"][0]
})
return {
"name": name,
"image_url": image_url,
"ingredients": ingredients
}
# Example usage:
recipe = fetch_recipe("lasagna")
print(recipe["name"]) # "Lasagna"
print(recipe["ingredients"])
# [
# {"name": "Mozzarella Cheese", "amount": "200g"},
# {"name": "Ground Beef", "amount": "300g"},
# ...
# ]import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
public class GrowcadoClient {
public static class Ingredient {
public String name;
public String amount;
public Ingredient(String name, String amount) {
this.name = name;
this.amount = amount;
}
}
public static class Recipe {
public String name;
public String imageUrl;
public List<Ingredient> ingredients;
public Recipe(String name, String imageUrl, List<Ingredient> ingredients) {
this.name = name;
this.imageUrl = imageUrl;
this.ingredients = ingredients;
}
}
public static Recipe fetchRecipe(String recipeId) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/" + recipeId))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = mapper.readValue(response.body(), Map.class);
List<Map<String, Object>> fields = (List<Map<String, Object>>) data.get("fields");
// Get recipe name
Map<String, Object> nameField = fields.stream()
.filter(f -> "name".equals(f.get("identifier")))
.findFirst().orElseThrow();
String name = ((List<String>) nameField.get("value")).get(0);
// Get image URL
Map<String, Object> imageField = fields.stream()
.filter(f -> "image".equals(f.get("identifier")))
.findFirst().orElseThrow();
String imageUrl = ((List<String>) imageField.get("value")).get(0);
// Parse ingredients
Map<String, Object> ingredientsField = fields.stream()
.filter(f -> "ingredients".equals(f.get("identifier")))
.findFirst().orElseThrow();
List<Map<String, Object>> ingredientsData = (List<Map<String, Object>>) ingredientsField.get("value");
List<Ingredient> ingredients = new ArrayList<>();
for (Map<String, Object> ingredientData : ingredientsData) {
List<Map<String, Object>> ingredientFields = (List<Map<String, Object>>) ingredientData.get("fields");
Map<String, Object> nameField2 = ingredientFields.stream()
.filter(f -> "name".equals(f.get("identifier")))
.findFirst().orElseThrow();
Map<String, Object> amountField = ingredientFields.stream()
.filter(f -> "amount".equals(f.get("identifier")))
.findFirst().orElseThrow();
ingredients.add(new Ingredient(
((List<String>) nameField2.get("value")).get(0),
((List<String>) amountField.get("value")).get(0)
));
}
return new Recipe(name, imageUrl, ingredients);
}
// Example usage:
public static void main(String[] args) throws Exception {
Recipe recipe = fetchRecipe("lasagna");
System.out.println(recipe.name); // "Lasagna"
System.out.println(recipe.ingredients);
// [Ingredient(name=Mozzarella Cheese, amount=200g), ...]
}
}🎯 4- Time for the Magic: Adding Personalization
Now comes the fun part! Remember how we promised you could serve vegan lasagna to plant-lovers and meaty goodness to carnivores? Let's make that happen.
The secret sauce is audience segments — these let you define rules that automatically sort your visitors into groups based on how they found you, what device they're using, or other criteria. When someone visits your site, Growcado checks if they match any of your segment rules and serves them the right content version.
Here's what we are going to build: if someone clicks a link from a vegan blog to reach your lasagna recipe, they'll automatically see the plant-based ingredient list. Everyone else gets the classic meat-and-cheese version.
Ready to get personal? Let's cook! 👨🍳
👥 Create Your First Audience
We're going to start by creating a segment for visitors who come from vegan websites. These are folks who probably found your site through vegan recipe blogs, plant-based cooking sites, or health-focused referrers.
🏷️ Setting Up the "Vegans" Segment
Head over to the Audience section in your sidebar and click + Add Segment.
You'll see a friendly 2-step process:
Step 1: Metadata (Set name and description)
Segment Name:
Plant-Based VisitorsDescription:
Visitors who arrive from vegan recipe blogs and plant-based websites
Keep the description clear — future you (and your team) will thank you when you have 20+ segments!
➡️ Hit Next when you're ready to get specific.

🎯 Define Who Belongs
Now for the targeting magic! This is where you tell Growcado exactly who should be considered a "Vegan" visitor.
In the Context section, you'll build rules like this:
Who [are] [visiting through a] [UTM Source] [vegan.shop]Here's what this means in plain English:
Who are: We're defining people who match certain criteria
visiting through a: They came to your site via a specific channel
UTM Source: The tracking parameter that shows where they came from
vegan.shop: The specific source value (like a vegan recipe blog or plant-based store)

🔗 Real-World Example
Let's say you partner with a popular vegan blog called "PlantPowerRecipes.com". When they link to your lasagna recipe, the URL might look like:
https://yoursite.com/recipes/lasagna?utm_source=vegan.shopAnyone clicking that link gets automatically tagged as part of your "Plant-Based Visitors" segment. Smart, right?
🪄 Creating Content Variants
Now that you've got your "Vegans" audience segment set up, it's time for the real magic — creating different versions of your lasagna recipe that automatically show up for different people.
Think of variants like having multiple recipe cards for the same dish. Your default recipe has ground beef and mozzarella, but your vegan variant swaps those out for plant-based alternatives. Same delicious lasagna, different ingredients based on who's looking!
🔄 Step 1: Create a Vegan Variant
Head back to your Lasagna recipe in the Content section. You'll see your current ingredient list with all the meaty, cheesy goodness. At the bottom, there's a magical little button called + Add Variant.

Click that button — it's time to create some personalized magic! ✨
🎯 Step 2: Choose Your Target Audience
A modal will pop up with a simple 2-step process:
First, choose your content variant (vegan ingridients in this case)

Second, Choose Segment, this is where you pick who gets this variant
You'll see your "Vegans" segment right there, complete with the description you wrote earlier: "Visitors who arrive from vegan recipe sites and plant-based cooking blogs"
Select the Vegans segment and hit Save.

Beautiful! Now you've got a dedicated vegan version. Notice how Growcado has cleverly organized everything:

Variant 1: 👥 Vegans This is where you added the plant-based ingredients.
Vegan Mozzarella
Vegan Ricotta
Tomato Sauce (this one stays the same!)
Lasagna Noodles (also the same)
Default value: 🌐 For other users This shows your original ingredients that everyone else will see:
Mozzarella Cheese
Ground Beef
Tomato Sauce
Lasagna Noodles
🎉 What Just Happened?
You've just created personalized content! Here's how it works:
Vegan blog visitor clicks a link with
utm_source=vegan.shop→ Gets the plant-based ingredient listEveryone else visits normally → Gets the classic meat-and-cheese version
Same recipe, same API call → Growcado automatically serves the right version
🚀 Test It Out
Let's go back to our integration code from earlier and add personalization to it! Remember those multi-language examples? Now they can automatically serve different content:
async function fetchRecipe(recipeId, userContext = {}) {
const headers = {};
// Add UTM header based on user context (referrer, user preferences, etc.)
if (userContext.utmSource) {
const utmParams = new URLSearchParams();
if (userContext.utmSource) utmParams.append('source', userContext.utmSource);
if (userContext.utmCampaign) utmParams.append('campaign', userContext.utmCampaign);
if (userContext.utmMedium) utmParams.append('medium', userContext.utmMedium);
headers['X-UTM'] = utmParams.toString();
}
const response = await fetch(`https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/${recipeId}`, {
headers
});
const recipe = await response.json();
// Get the recipe name
const name = recipe.fields.find(f => f.identifier === 'name').value[0];
// Get the image URL
const imageUrl = recipe.fields.find(f => f.identifier === 'image').value[0];
// Parse ingredients with their name and amount
const ingredientsData = recipe.fields.find(f => f.identifier === 'ingredients').value;
const ingredients = ingredientsData.map(ingredient => {
const name = ingredient.fields.find(f => f.identifier === 'name').value[0];
const amount = ingredient.fields.find(f => f.identifier === 'amount').value[0];
return { name, amount };
});
return { name, imageUrl, ingredients };
}
// Real-world usage examples:
// 1. Regular visitor (gets default version)
const recipe = await fetchRecipe('lasagna');
console.log('Default ingredients:', recipe.ingredients);
// 2. User came from a vegan blog
const veganUser = {
utmSource: 'vegan.shop',
utmCampaign: 'recipe-share',
utmMedium: 'blog'
};
const veganRecipe = await fetchRecipe('lasagna', veganUser);
console.log('Vegan ingredients:', veganRecipe.ingredients);
// 3. Detect from current page URL parameters
const urlParams = new URLSearchParams(window.location.search);
const currentUserContext = {
utmSource: urlParams.get('utm_source'),
utmCampaign: urlParams.get('utm_campaign'),
utmMedium: urlParams.get('utm_medium')
};
const contextualRecipe = await fetchRecipe('lasagna', currentUserContext);import Foundation
struct Ingredient {
let name: String
let amount: String
}
struct Recipe {
let name: String
let imageUrl: String
let ingredients: [Ingredient]
}
struct UserContext {
let utmSource: String?
let utmCampaign: String?
let utmMedium: String?
}
func fetchRecipe(recipeId: String, userContext: UserContext = UserContext(utmSource: nil, utmCampaign: nil, utmMedium: nil)) async throws -> Recipe {
var request = URLRequest(url: URL(string: "https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/\(recipeId)")!)
// Add UTM header based on user context
if let utmSource = userContext.utmSource {
var utmParams = [String]()
utmParams.append("source=\(utmSource)")
if let campaign = userContext.utmCampaign { utmParams.append("campaign=\(campaign)") }
if let medium = userContext.utmMedium { utmParams.append("medium=\(medium)") }
request.setValue(utmParams.joined(separator: "&"), forHTTPHeaderField: "X-UTM")
}
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let fields = json["fields"] as! [[String: Any]]
// Get recipe name
let nameField = fields.first { $0["identifier"] as! String == "name" }!
let name = (nameField["value"] as! [String])[0]
// Get image URL
let imageField = fields.first { $0["identifier"] as! String == "image" }!
let imageUrl = (imageField["value"] as! [String])[0]
// Parse ingredients
let ingredientsField = fields.first { $0["identifier"] as! String == "ingredients" }!
let ingredientsData = ingredientsField["value"] as! [[String: Any]]
let ingredients = ingredientsData.map { ingredientData in
let fields = ingredientData["fields"] as! [[String: Any]]
let nameField = fields.first { $0["identifier"] as! String == "name" }!
let amountField = fields.first { $0["identifier"] as! String == "amount" }!
return Ingredient(
name: (nameField["value"] as! [String])[0],
amount: (amountField["value"] as! [String])[0]
)
}
return Recipe(name: name, imageUrl: imageUrl, ingredients: ingredients)
}
// Real-world usage examples:
Task {
// 1. Regular visitor (gets default version)
let defaultRecipe = try await fetchRecipe(recipeId: "lasagna")
print("Default ingredients: \(defaultRecipe.ingredients)")
// 2. User came from a vegan blog
let veganContext = UserContext(
utmSource: "vegan.shop",
utmCampaign: "recipe-share",
utmMedium: "blog"
)
let veganRecipe = try await fetchRecipe(recipeId: "lasagna", userContext: veganContext)
print("Vegan ingredients: \(veganRecipe.ingredients)")
// 3. User from health & wellness site
let healthContext = UserContext(utmSource: "wellness.com", utmCampaign: "healthy-recipes", utmMedium: "newsletter")
let healthRecipe = try await fetchRecipe(recipeId: "lasagna", userContext: healthContext)
print("Health-focused ingredients: \(healthRecipe.ingredients)")
}import kotlinx.coroutines.*
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
data class Ingredient(
val name: String,
val amount: String
)
data class Recipe(
val name: String,
val imageUrl: String,
val ingredients: List<Ingredient>
)
data class UserContext(
val utmSource: String? = null,
val utmCampaign: String? = null,
val utmMedium: String? = null
)
suspend fun fetchRecipe(recipeId: String, userContext: UserContext = UserContext()): Recipe = withContext(Dispatchers.IO) {
val url = URL("https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/$recipeId")
val connection = url.openConnection() as HttpURLConnection
// Add UTM header based on user context
userContext.utmSource?.let { source ->
val utmParams = mutableListOf("source=$source")
userContext.utmCampaign?.let { utmParams.add("campaign=$it") }
userContext.utmMedium?.let { utmParams.add("medium=$it") }
connection.setRequestProperty("X-UTM", utmParams.joinToString("&"))
}
val response = connection.inputStream.bufferedReader().readText()
val json = JSONObject(response)
val fields = json.getJSONArray("fields")
// Get recipe name
val nameField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "name" }
val name = nameField.getJSONArray("value").getString(0)
// Get image URL
val imageField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "image" }
val imageUrl = imageField.getJSONArray("value").getString(0)
// Parse ingredients
val ingredientsField = (0 until fields.length()).map { fields.getJSONObject(it) }
.first { it.getString("identifier") == "ingredients" }
val ingredientsData = ingredientsField.getJSONArray("value")
val ingredients = (0 until ingredientsData.length()).map { i ->
val ingredient = ingredientsData.getJSONObject(i)
val ingredientFields = ingredient.getJSONArray("fields")
val nameField = (0 until ingredientFields.length()).map { ingredientFields.getJSONObject(it) }
.first { it.getString("identifier") == "name" }
val amountField = (0 until ingredientFields.length()).map { ingredientFields.getJSONObject(it) }
.first { it.getString("identifier") == "amount" }
Ingredient(
name = nameField.getJSONArray("value").getString(0),
amount = amountField.getJSONArray("value").getString(0)
)
}
Recipe(name, imageUrl, ingredients)
}
// Real-world usage examples:
runBlocking {
// 1. Regular visitor (gets default version)
val defaultRecipe = fetchRecipe("lasagna")
println("Default ingredients: ${defaultRecipe.ingredients}")
// 2. User came from a vegan blog
val veganContext = UserContext(
utmSource = "vegan.shop",
utmCampaign = "recipe-share",
utmMedium = "blog"
)
val veganRecipe = fetchRecipe("lasagna", veganContext)
println("Vegan ingredients: ${veganRecipe.ingredients}")
// 3. Track user journey from analytics
val analyticsContext = UserContext(
utmSource = getAnalyticsReferrer(), // Your analytics function
utmCampaign = getCurrentCampaign(),
utmMedium = "social"
)
val personalizedRecipe = fetchRecipe("lasagna", analyticsContext)
println("Personalized ingredients: ${personalizedRecipe.ingredients}")
}import requests
from typing import List, Dict, Any, Optional
class UserContext:
def __init__(self, utm_source: Optional[str] = None, utm_campaign: Optional[str] = None, utm_medium: Optional[str] = None):
self.utm_source = utm_source
self.utm_campaign = utm_campaign
self.utm_medium = utm_medium
def fetch_recipe(recipe_id: str, user_context: UserContext = None) -> Dict[str, Any]:
url = f"https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/{recipe_id}"
headers = {}
# Add UTM header based on user context
if user_context and user_context.utm_source:
utm_params = [f"source={user_context.utm_source}"]
if user_context.utm_campaign:
utm_params.append(f"campaign={user_context.utm_campaign}")
if user_context.utm_medium:
utm_params.append(f"medium={user_context.utm_medium}")
headers['X-UTM'] = '&'.join(utm_params)
response = requests.get(url, headers=headers)
data = response.json()
fields = data["fields"]
# Get recipe name
name_field = next(f for f in fields if f["identifier"] == "name")
name = name_field["value"][0]
# Get image URL
image_field = next(f for f in fields if f["identifier"] == "image")
image_url = image_field["value"][0]
# Parse ingredients
ingredients_field = next(f for f in fields if f["identifier"] == "ingredients")
ingredients_data = ingredients_field["value"]
ingredients = []
for ingredient_data in ingredients_data:
fields = ingredient_data["fields"]
name_field = next(f for f in fields if f["identifier"] == "name")
amount_field = next(f for f in fields if f["identifier"] == "amount")
ingredients.append({
"name": name_field["value"][0],
"amount": amount_field["value"][0]
})
return {
"name": name,
"image_url": image_url,
"ingredients": ingredients
}
# Real-world usage examples:
# 1. Regular visitor (gets default version)
default_recipe = fetch_recipe("lasagna")
print("Default ingredients:", default_recipe["ingredients"])
# 2. User came from a vegan blog
vegan_context = UserContext(
utm_source="vegan.shop",
utm_campaign="recipe-share",
utm_medium="blog"
)
vegan_recipe = fetch_recipe("lasagna", vegan_context)
print("Vegan ingredients:", vegan_recipe["ingredients"])
# 3. Detect from Flask request (web framework example)
# from flask import request
# def get_user_context_from_request():
# return UserContext(
# utm_source=request.args.get('utm_source'),
# utm_campaign=request.args.get('utm_campaign'),
# utm_medium=request.args.get('utm_medium')
# )
#
# contextual_recipe = fetch_recipe("lasagna", get_user_context_from_request())import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
public class GrowcadoClient {
public static class Ingredient {
public String name;
public String amount;
public Ingredient(String name, String amount) {
this.name = name;
this.amount = amount;
}
@Override
public String toString() {
return "Ingredient(name=" + name + ", amount=" + amount + ")";
}
}
public static class Recipe {
public String name;
public String imageUrl;
public List<Ingredient> ingredients;
public Recipe(String name, String imageUrl, List<Ingredient> ingredients) {
this.name = name;
this.imageUrl = imageUrl;
this.ingredients = ingredients;
}
}
public static class UserContext {
public String utmSource;
public String utmCampaign;
public String utmMedium;
public UserContext() {}
public UserContext(String utmSource, String utmCampaign, String utmMedium) {
this.utmSource = utmSource;
this.utmCampaign = utmCampaign;
this.utmMedium = utmMedium;
}
}
public static Recipe fetchRecipe(String recipeId, UserContext userContext) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("https://api.growcado.io/cms/tenant/YOUR_TENANT_ID/published/recipe/" + recipeId));
// Add UTM header based on user context
if (userContext != null && userContext.utmSource != null) {
List<String> utmParams = new ArrayList<>();
utmParams.add("source=" + userContext.utmSource);
if (userContext.utmCampaign != null) utmParams.add("campaign=" + userContext.utmCampaign);
if (userContext.utmMedium != null) utmParams.add("medium=" + userContext.utmMedium);
requestBuilder.header("X-UTM", String.join("&", utmParams));
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = mapper.readValue(response.body(), Map.class);
List<Map<String, Object>> fields = (List<Map<String, Object>>) data.get("fields");
// Get recipe name
Map<String, Object> nameField = fields.stream()
.filter(f -> "name".equals(f.get("identifier")))
.findFirst().orElseThrow();
String name = ((List<String>) nameField.get("value")).get(0);
// Get image URL
Map<String, Object> imageField = fields.stream()
.filter(f -> "image".equals(f.get("identifier")))
.findFirst().orElseThrow();
String imageUrl = ((List<String>) imageField.get("value")).get(0);
// Parse ingredients
Map<String, Object> ingredientsField = fields.stream()
.filter(f -> "ingredients".equals(f.get("identifier")))
.findFirst().orElseThrow();
List<Map<String, Object>> ingredientsData = (List<Map<String, Object>>) ingredientsField.get("value");
List<Ingredient> ingredients = new ArrayList<>();
for (Map<String, Object> ingredientData : ingredientsData) {
List<Map<String, Object>> ingredientFields = (List<Map<String, Object>>) ingredientData.get("fields");
Map<String, Object> nameField2 = ingredientFields.stream()
.filter(f -> "name".equals(f.get("identifier")))
.findFirst().orElseThrow();
Map<String, Object> amountField = ingredientFields.stream()
.filter(f -> "amount".equals(f.get("identifier")))
.findFirst().orElseThrow();
ingredients.add(new Ingredient(
((List<String>) nameField2.get("value")).get(0),
((List<String>) amountField.get("value")).get(0)
));
}
return new Recipe(name, imageUrl, ingredients);
}
// Convenience method for default version
public static Recipe fetchRecipe(String recipeId) throws Exception {
return fetchRecipe(recipeId, null);
}
// Real-world usage examples:
public static void main(String[] args) throws Exception {
// 1. Regular visitor (gets default version)
Recipe defaultRecipe = fetchRecipe("lasagna");
System.out.println("Default ingredients: " + defaultRecipe.ingredients);
// 2. User came from a vegan blog
UserContext veganContext = new UserContext("vegan.shop", "recipe-share", "blog");
Recipe veganRecipe = fetchRecipe("lasagna", veganContext);
System.out.println("Vegan ingredients: " + veganRecipe.ingredients);
// 3. Spring Boot controller example
// @GetMapping("/recipe/{id}")
// public Recipe getRecipe(@PathVariable String id, HttpServletRequest request) {
// UserContext context = new UserContext(
// request.getParameter("utm_source"),
// request.getParameter("utm_campaign"),
// request.getParameter("utm_medium")
// );
// return fetchRecipe(id, context);
// }
}
}🌟 You've Just Scratched the Surface
Congratulations! You've just built your first personalized content system that automatically detects vegan-interested visitors and serves appropriate content without any code changes.
But here's where it gets really exciting — what we just did was the simplest possible example. We used basic UTM source detection to keep this intro digestible, but Growcado's audience targeting is incredibly sophisticated. You can create mind-blowingly precise segments using three powerful categories:
🎯 Customer Context (What we just used)
Beyond UTM parameters, target users based on:
Location: Show regional ingredients ("Show tikka masala spices to users in the UK")
Time intelligence: Different content for breakfast vs dinner hours, holidays vs regular days
Language & locale: Automatic localization without managing separate content
Custom headers: Send any data from your app to trigger personalization
👤 User Attributes (The real power)
Sync rich user data from anywhere and personalize based on demographics, subscription tiers, purchase history, interests, and more:
CRM systems: Salesforce, HubSpot, Pipedrive — sync customer profiles and subscription data
Analytics platforms: Segment, Mixpanel, Google Analytics — leverage behavioral insights
Data warehouses: Snowflake, BigQuery, Redshift — tap into your entire data ecosystem
E-commerce platforms: Shopify, WooCommerce, Stripe — use purchase history and preferences
Customer platforms: Intercom, Zendesk, Klaviyo — incorporate support and communication data
Any API or database: Custom integrations to pull user attributes from anywhere
🧠 User Behaviors (Coming Soon)
Target users based on their actions and patterns:
Search patterns: Mark users as "health-conscious" if they search for low-calorie recipes
Content engagement: Show advanced recipes to users who spend time reading detailed instructions
Navigation behavior: Detect recipe preferences from browsing patterns
Cross-session learning: Build user profiles that get smarter over time
🔥 Beyond Variants: One-to-One Hyper Personalization
But wait, there's more! Beyond swapping entire ingredient lists, you can make any text field hyper-personalized with live JavaScript expressions:
Recipe titles:
"${profile.firstName}'s Perfect ${profile.dietaryPreference} Lasagna"Serving suggestions:
"This recipe serves ${profile.familySize} people perfectly"Personal touches:
"Welcome back ${profile.firstName}! Since you loved our ${profile.lastFavoriteRecipe}, you'll love this one"
Every piece of content becomes dynamically personal — in real-time! 🤯
🚀 Real-World Examples
Imagine these scenarios — all possible with the same simple API you just learned:
// Same fetchRecipe() function, but now it can:
// Show premium wine pairings to Gold subscribers from your CRM
const premiumUser = { userTier: 'gold', interests: ['wine'] };
// Display halal ingredients to Muslim users in the Middle East
const regionalUser = { location: 'UAE', dietaryNeeds: ['halal'] };
// Recommend quick recipes to busy parents on weekday evenings
const timeContext = { timeOfDay: 'evening', dayOfWeek: 'tuesday', hasKids: true };The best part? Your integration code stays exactly the same. One API call, infinite personalization possibilities.
Hungry for more? Our complete documentation serves up advanced targeting, enterprise integrations, and real-world case studies that'll transform how you think about personalization. Bon appétit! 👨🍳
🚀 Where to Go Next
If you're building with JavaScript/TypeScript, check out our official SDK that handles all the UTM tracking, referrer detection, and header management automatically. What took us several lines of manual code becomes a simple GrowcadoSDK.getContent() call with built-in personalization, automatic customer journey tracking, SSR support, and zero configuration. The SDK doesn't just make integration easier — it makes advanced personalization effortless! 🍽️✨
Last updated