To add Terpli to a native mobile app or custom website, build your own screens and call the Terpli Widget API directly. The website widget is a JavaScript bundle that runs only in a browser, so it is not the right fit inside a native app.
The Widget API is the same backend the Terpli website widget uses. Anything the website does, your app can do with the endpoints below:
Recommendations: a short quiz (category, effects, potency, terpenes, price) that returns ranked products from a store's live menu.
Locations: the stores a shopper can pick, sorted by distance when you send coordinates.
Loyalty rewards: a one-time points gift for a shopper's first recommendation, plus a signup link.
Product reviews: shoppers verify by SMS or email code, then review products they bought.
Events: impressions, clicks and conversions, which power Terpli analytics and attribution in the AIQ dashboard.
The interactive reference (Swagger) is at https://lab.alpineiq.com/widget/swagger. This guide explains how the endpoints fit together into a working in-app experience.
Before you start
Every endpoint lives under one base URL and needs no API key. Only the review endpoints need a token, and the shopper gets it by verifying a code.
Base URL:
https://lab.alpineiq.com/api/v1/widget
IDs you will use
Value | What it is | Where you get it |
| The retailer's AIQ account | AIQ settings ID |
| One store's menu ID, used to score that store's live menu |
|
| The store's numeric ID, used for store-level settings |
|
|
|
|
| A stable ID for this install, which you create. UUID format preferred. | Generate a UUID on first launch and save it (Keychain on iOS, SharedPreferences or DataStore on Android). Send the same value on every call. |
| One app session, which you create. UUID format preferred. | Generate a UUID when you see fit for a new session. Generally, each time the app starts. |
| The shopper's AIQ loyalty contact ID | Your loyalty login. Optional. |
⚠️ deviceId matters. The API uses it to link a recommendation to later clicks, rewards and purchases. A new value on every launch breaks attribution and lets one shopper claim the first-recommendation reward more than once.
Response format
Every response is wrapped in the same JSON envelope. Read your payload from data.
{ "code": 200, "success": true, "data": { } }Errors use the HTTP status code and an errors array:
{ "code": 400, "success": false, "errors": [ { "message": "deviceId is required" } ] }Rate limits
15 requests per second, 800 per minute and 8,000 per hour, per IP address. Call the API from the shopper's device, not from one shared server, or all your shoppers will share one limit.
Sending review codes is stricter: 1 per second, 5 per minute and 40 per hour per IP, and one code per phone or email every 30 seconds.
Recommendation calls can take a few seconds. Set a client timeout of at least 30 seconds and show a loading state.
How the calls fit together
Recommendation flow
Load settings —
GET /settingsPick a store —
POST /locationsRun the quiz —
POST /recommendShow results —
POST /eventClaim reward (if the points gift is on) —
GET+POST /rewards
Review flow (its own entry point)
Send code —
POST …/auth/sendVerify, get token —
POST …/auth/verifyList purchases —
GET /reviews/pendingSubmit review —
POST /reviews
Step 1: Load settings when the app starts
GET /settings/{uid}/{menuType}
This returns the retailer's Terpli settings from the AIQ dashboard. Use it to decide which features to show. Cache it for the session.
enabled: Terpli is on for this account. When this isfalse, hide the entry point.recommendationSettings.enabled: show the recommendation quiz.reviewSettings.enabledandreviewSettings.purchaseLookbackHours: show reviews, and how far back purchases can be reviewed.loyaltySettings: the program name, the first-recommendation points gift and the review rewards.
Add ?storeID={storeID}&merge=true after the shopper picks a store to apply that store's overrides.
Branding (colors, logo) lives in GET /user-settings/{uid}. A native app can use it or apply its own design.
Step 2: Let the shopper pick a store
POST /locations?locType=ecom
{
"uids": ["YOUR_UID"],
"coords": { "latitude": 39.7392, "longitude": -104.9903 }
}coords is optional. When you send it, stores are sorted nearest first and distanceInMiles is filled in. For each store in ecomLocations[], keep:
globalID: required for recommendations.storeID: used for store-level settings and reviews.subEcomLocations[]: one entry per menu (rec or med), withavailableCategories(what is in stock now) andedibleSubCategories. Only offer categories that appear here, or the quiz may return no products.
If enabled is false or ecomLocations is empty, Terpli is not set up for any store yet.
Step 3: Run the quiz and get recommendations
POST /recommend/{recommendationType}/{uid}/{globalID}/{menuType}
recommendationType is inhalable, scent or edible. Each has its own request body.
inhalable (Flower, Preroll, Vape Cartridge, Concentrates & Extracts):
{
"deviceId": "6f1c2a7e-0b5d-4c1e-9a43-2f8e7d6c5b4a",
"filters": {
"category": ["Flower"],
"effects": ["relaxed", "euphoric"],
"strength": 3,
"terpenes": ["myrcene", "limonene"],
"price": { "min": 10, "max": 60 }
}
}scent uses the same body, but replaces effects and terpenes with one scent value. Use it for a simpler quiz.
{
"deviceId": "6f1c2a7e-…",
"filters": { "category": ["Flower"], "scent": "citrusy", "strength": 2 }
}edible:
{
"deviceId": "6f1c2a7e-…",
"filters": { "edibleCategory": ["gummies"], "dosage": "medium", "basicEffect": "night" }
}Field | Allowed values |
| Flower, Preroll, Vape Cartridge, Concentrates & Extracts |
| calm, focused, euphoric, energized, creative, stimulated, sedated, sleepy, relaxed, uplifted |
| caryophyllene, pinene, linalool, myrcene, limonene, terpinolene |
| 1 Mild to 5 Extra Strong. Flower and Preroll: <13%, 13–18%, 18–22%, 22–28%, >28% THC. Vape and Concentrates: <66%, 66–75%, 75–85%, 85–90%, >90% THC. |
|
|
| hoppy, citrusy, spicy, fruity, floral, earthy |
| chocolates, drinks, gummies, tinctures, tablets, snacks, other |
| low (5 mg), medium (10 mg), high (25 mg) |
| day, balanced, night |
The response holds a recommendationId and products ranked best first:
{
"code": 200,
"success": true,
"data": {
"recommendationId": "8821",
"products": [
{
"product_uid": "prod-1",
"product_url": "https://shop.example.com/products/blue-dream",
"strain": "Blue Dream",
"imageURL": "https://cdn.example.com/blue-dream.jpg",
"brand": "Example Brand",
"category": "Flower",
"posId": "pos-1001",
"sponsored": false,
"rank": 0.92,
"THC": "22%",
"CBD": "0.4%",
"cannabinoids": [{ "type": "THC", "value": "22%" }],
"terpenes": [{ "name": "Myrcene", "type": "myrcene", "presence": 0.62, "weight": 0.8 }],
"prices": [{ "price": 35, "weight": "3.5g", "discounted_price": null }]
}
]
}
}Keep recommendationId. Send it in the details of later events and the reward call so AIQ can attribute sales to Terpli.
product_urllinks to the product on the store's online menu. Open it in your in-app browser, or matchposId/product_uidto your own catalog to add to cart natively.Edible products return
effectanddosageinstead of THC, CBD and THCA.Label products where
sponsoredistrue, as the website does.An empty
productsarray means nothing on the menu matched. Suggest loosening price or strength.
Loyalty rewards
Both loyalty rewards and product reviews tie a shopper to their AIQ loyalty contact by phone or email. The shopper must already be a loyalty member, so send non-members to the signup link.
First-recommendation reward
A shopper can earn a one-time points gift for completing their first recommendation. Only show it when loyaltySettings.firstRecPointsGift.enabled is true in settings.
1. Check eligibility after showing results:
GET /rewards/{uid}?rewardType=rec&deviceId={deviceId}
You can send contactID instead of deviceId if your app already knows the shopper.
rec.userEligible: trueandrec.points: show "Claim {points} points".userEligible: false: this shopper already got the reward. Hide the offer.An
eligibilityobject withisEligible: false: the reward is turned off for this account.signupLink: open it in an in-app browser for shoppers who are not members yet.
2. Grant the reward when the shopper enters a phone number or email:
POST /rewards/{uid}
{
"rewardType": "rec",
"phone": "3035551234",
"deviceId": "6f1c2a7e-…",
"globalId": "gid-store-1",
"details": { "recommendationId": "8821" }
}Send phone or email, not both.
Status | Meaning | What to show |
| Points granted. | Success message |
| No loyalty member has that phone or email | The signup link |
| This shopper already got the reward | "Already claimed" |
| The reward is turned off for this account | Hide the offer |
| Bad phone number, both or neither of phone and email, or | Fix the input |
Product reviews
Shoppers can review products they bought within reviewSettings.purchaseLookbackHours (for example, 168 hours = 7 days). Reviews need a short-lived token, which the shopper gets with a one-time code.
In the paths below, {pii} is the shopper's phone, email or AIQ contact ID. URL-encode it, because emails contain @. Use the same value in every review call for that shopper. Add ?storeID={storeID}&menuType={menuType} to each call so store-level review settings apply.
1. Send a code
POST /reviews/auth/send/{uid}/{pii}
Body: { "channel": "sms" } or { "channel": "email" }. If you leave out channel, it is chosen from pii. The code expires after 15 minutes. You can request one code per phone or email every 30 seconds. A 429 response includes Retry-After in seconds.
2. Verify it
POST /reviews/auth/verify/{uid}/{pii}
Body: { "code": "482193" }. The response holds token and expiresAt. A code allows 5 attempts.
3. Use the token
Send the token on every later review call as Authorization: Bearer {token}. It lasts 24 hours. Store it in the Keychain or Keystore. While it is still valid, POST /reviews/auth/refresh gives a new one, so returning shoppers can skip the code.
4. List what to review
GET /reviews/pending/{uid}/{pii} returns products[] (name, brand, image, purchasedAt, transactionID) and the configured reviewRewards. GET /reviews/completed/{uid}/{pii} returns past reviews.
5. Submit a review
POST /reviews/{uid}/{pii}
{
"productId": "prod-1",
"rating": 5,
"effects": ["relaxed", "sleepy"],
"positiveExperiences": ["flavor", "effects"],
"negativeExperiences": [],
"potency": 4,
"reviewComment": "Smooth and relaxing evening flower."
}rating and potency are 1 to 5. Only products from the pending list can be reviewed. If the response has pointsAwarded above 0, tell the shopper they earned points.
Status | Meaning | What to do |
| Token missing or expired | Send a new code |
| Reviews are off for this account | Hide reviews |
|
| Show the message to the shopper |
Send events for analytics and attribution
POST /event?globalId={globalID}&menuType={menuType}
Events feed the Terpli dashboard in AIQ: impressions, quiz starts, product clicks, and the revenue Terpli drove. Without them, the retailer cannot see what the in-app experience is worth. Send events without blocking the UI, and do not retry more than once.
{
"uids": ["YOUR_UID"],
"event_type": "internal",
"action": "view_product",
"deviceId": "6f1c2a7e-…",
"sessionId": "b2e4…",
"contactID": "",
"mode": "fpe",
"url": "app://yourapp/terpli/results",
"os": "iOS",
"route": "recommendation/results",
"details": { "recommendationId": "8821", "product_id": "prod-1" }
}Required:
uids,event_type(internalorthirdParty),action(letters, numbers,_,.,-, up to 100 characters),deviceId,sessionId,modeandurl.mode: usefpe(full-page experience) for in-app screens.url: an app deep link or screen name works.details: up to 4 KB of JSON.
Use the same action names as the website widget so app and web data line up. If you create new event types or think extra data would help analytics, contact AIQ Support with sample events so we can parse them correctly.