Use this guide to auto-load products from Sedifex into either:
This quickstart follows the current versioned Sedifex downstream contract based on /v1IntegrationProducts and related integration endpoints (/v1IntegrationPromo, /integrationGallery, /integrationCustomers, /integrationTopSelling, /integrationTikTokVideos, and /integrationGoogleMerchantFeed), plus the product shape documented in the root README.
After setup, Website A can fetch and render:
idstoreIdnamecategorydescriptionpricestockCountitemTypeimageUrl (primary image)imageUrls (optional array for multiple product photos)imageAltupdatedAtCustomer data fields (for import/export template)
Required
name — Primary customer name.
Optional
display_name — Preferred display name.
phone — Phone number (country code recommended).
email — Customer email.
birthdate — Format: YYYY-MM-DD.
notes — Preferences or notes.
tags — Comma-separated labels/tags.
Promo data fields (store promo profile)
promoTitle
promoSummary
promoStartDate
promoEndDate
promoSlug
promoWebsiteUrl
Also used with:
displayName / name (store name fallback shown on promo page).
The same promo field set is defined in the account/store profile typing too, so these are the right canonical keys to use
Promo gallery data fields (stores/{storeId}/promoGallery/{itemId})
url
alt
caption
sortOrder
isPublished
createdAt
updatedAt
TikTok feed data fields (stores/{storeId}/tiktokVideos/{videoId})
videoId
embedUrl
permalink
caption
thumbnailUrl
duration
viewCount
likeCount
commentCount
shareCount
sortOrder
isPublished
publishedAt
createdAt
updatedAt
If you want store owners to connect TikTok from Account Overview, set these Cloud Functions params:
TIKTOK_CLIENT_KEYTIKTOK_CLIENT_SECRETTIKTOK_REDIRECT_URITIKTOK_SUCCESS_REDIRECT_URL (optional, but recommended)TIKTOK_ERROR_REDIRECT_URL (optional, but recommended)How to set them:
firebase deploy --only functionsdefineString(...) params and write them to functions/.env.<projectId>.TIKTOK_REDIRECT_URI must exactly match the callback URL configured in your TikTok developer app, for example:
https://us-central1-<your-project-id>.cloudfunctions.net/tiktokOAuthCallbackFAQ
TIKTOK_CLIENT_KEY / TIKTOK_CLIENT_SECRET.TIKTOK_CLIENT_KEY?
tiktokOAuthCallback URL to TikTok app redirect/callback settings before testing OAuth.user.info.basic and video.list).If you are integrating the dedicated Sedifex market frontend (buysedifex repo), follow the cross-repo communication plan in:
docs/sedifex-buysedifex-integration-plan.mdIt defines a pull + webhook model, contract versioning, reliability, and rollout sequencing so both repos can ship independently without API drift.
SEDIFEX_INTEGRATION_API_KEY), orBase URL:
https://us-central1-sedifex-web.cloudfunctions.net as the production base URL.SEDIFEX_API_BASE_URL.SEDIFEX_INTEGRATION_API_BASE_URL, set it to the same value (https://us-central1-sedifex-web.cloudfunctions.net).SEDIFEX_API_BASE_URLSEDIFEX_STORE_IDSEDIFEX_INTEGRATION_API_KEY (or legacy SEDIFEX_INTEGRATION_KEY)SEDIFEX_CONTRACT_VERSION (defaults to 2026-04-13)x-api-keyX-Sedifex-Contract-VersionAccept: application/jsonnext: { revalidate: 60 }getHomePageData(), request all three endpoints with Promise.all(...):
GET /v1IntegrationProducts?storeId=<storeId>GET /v1IntegrationPromo?storeId=<storeId>GET /integrationGallery?storeId=<storeId>promoTitle, promo_title, etc.) into a unified promo object.sortOrder.fallbackProductsfallbackPromofallbackGallerygetHomePageData() powers:
products + promo + gallery)gallery)products)GET /integrationCustomers?storeId=<storeId>GET /integrationTopSelling?storeId=<storeId>&days=30&limit=10GET /integrationTikTokVideos?storeId=<storeId>GET /v1IntegrationAvailability?storeId=<storeId>&serviceId=<serviceId>&from=<ISO>&to=<ISO>GET /v1IntegrationBookings?storeId=<storeId>POST /v1IntegrationBookings?storeId=<storeId>Booking/registration note:
attributes in the booking payload.branchLocationId, eventLocation, customerStayLocation, and paymentAmount) plus a full request example, see docs/integration-api-guide.md under POST /v1IntegrationBookings.If your app logs a 404 such as:
/2026-04-13/productsyour URL builder is likely treating the contract version as a URL path segment. In Sedifex, 2026-04-13 is the contract header value, not an endpoint path. Use:
GET /v1IntegrationProducts?storeId=<storeId> (authenticated integration feed), orGET /v1/products (public marketplace feed).Do not build routes like /<contractVersion>/products.
Import shared interfaces from shared/integrationTypes.ts in both Sedifex and Buy Sedifex projects to avoid field drift:
IntegrationProductIntegrationPromoIntegrationProductsResponseIntegrationPromoResponseIf you publish these to npm, keep the package version aligned with the contract header date (X-Sedifex-Contract-Version).
// app/menu/page.tsx (server component)
type Product = {
id: string
storeId: string
name: string
category?: string | null
description?: string | null
price: number
stockCount?: number
imageUrl?: string | null
imageUrls?: string[]
}
const FALLBACK_PRODUCTS: Product[] = [
{
id: 'fallback-1',
storeId: 'fallback',
name: 'Sample Jollof Rice',
category: 'Meals',
description: 'Classic Ghana-style rice with tomato stew and spices.',
price: 45,
stockCount: 10,
},
{
id: 'fallback-2',
storeId: 'fallback',
name: 'Sample Orange Juice',
category: 'Drinks',
description: 'Freshly squeezed orange juice served chilled.',
price: 12,
stockCount: 25,
},
]
function dedupeProducts(products: Product[]): Product[] {
const seen = new Set<string>()
const unique: Product[] = []
for (const p of products) {
const key = `${p.id}|${p.storeId}|${p.name}|${p.price}`
if (seen.has(key)) continue
seen.add(key)
unique.push(p)
}
return unique
}
async function fetchSedifexProducts(): Promise<Product[]> {
try {
const response = await fetch(
`${process.env.SEDIFEX_API_BASE_URL}/v1IntegrationProducts?storeId=${encodeURIComponent(
process.env.SEDIFEX_STORE_ID ?? ''
)}`,
{
headers: {
'x-api-key': `${process.env.SEDIFEX_INTEGRATION_API_KEY ?? process.env.SEDIFEX_INTEGRATION_KEY}`,
'X-Sedifex-Contract-Version': process.env.SEDIFEX_CONTRACT_VERSION ?? '2026-04-13',
Accept: 'application/json',
},
// ISR cache strategy (choose based on your catalog behavior)
next: { revalidate: 60 },
}
)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const payload = await response.json()
const products = Array.isArray(payload?.products) ? payload.products : []
return dedupeProducts(products)
} catch {
return FALLBACK_PRODUCTS
}
}
function groupByCategory(products: Product[]) {
return products.reduce<Record<string, Product[]>>((acc, product) => {
const category = product.category?.trim() || 'Uncategorized'
if (!acc[category]) acc[category] = []
acc[category].push(product)
return acc
}, {})
}
export default async function MenuPage() {
const products = await fetchSedifexProducts()
const grouped = groupByCategory(products)
return (
<main>
<h1>Menu</h1>
{Object.entries(grouped).map(([category, items]) => (
<section key={category}>
<h2>{category}</h2>
<ul>
{items.map(item => (
<li key={`${item.id}-${item.storeId}`}>
<strong>{item.name}</strong> — {item.price}
{item.description ? <p>{item.description}</p> : null}
</li>
))}
</ul>
</section>
))}
</main>
)
}
revalidate: 30-120 seconds.revalidate: 3600 (1 hour) or longer.For promo + gallery integrations, use the same 30–120 second polling interval initially. If you later need sub-minute pushes, move to webhook-triggered cache invalidation.
Teams usually struggle here for three reasons: missing auth header, incorrect endpoint (/integrationPromo instead of /v1IntegrationPromo), or fetching from a Client Component with a secret key.
Use a server-only helper so your integration key never reaches the browser bundle:
// lib/sedifexPromo.ts
import 'server-only'
const BASE_URL = process.env.SEDIFEX_API_BASE_URL ?? 'https://us-central1-sedifex-web.cloudfunctions.net'
const STORE_ID = process.env.SEDIFEX_STORE_ID ?? ''
const API_KEY = process.env.SEDIFEX_INTEGRATION_API_KEY ?? process.env.SEDIFEX_INTEGRATION_KEY ?? ''
const CONTRACT = process.env.SEDIFEX_CONTRACT_VERSION ?? '2026-04-13'
type PromoPayload = {
storeId: string
promo: {
enabled: boolean
title?: string | null
summary?: string | null
startDate?: string | null
endDate?: string | null
websiteUrl?: string | null
imageUrl?: string | null
imageAlt?: string | null
}
}
type GalleryPayload = {
storeId: string
gallery: Array<{
id: string
url: string
alt?: string | null
caption?: string | null
sortOrder?: number
isPublished?: boolean
}>
}
export async function fetchPromoAndGallery() {
const headers = {
'x-api-key': API_KEY,
'X-Sedifex-Contract-Version': CONTRACT,
Accept: 'application/json',
}
const [promoRes, galleryRes] = await Promise.all([
fetch(`${BASE_URL}/v1IntegrationPromo?storeId=${encodeURIComponent(STORE_ID)}`, {
headers,
next: { revalidate: 60 },
}),
fetch(`${BASE_URL}/integrationGallery?storeId=${encodeURIComponent(STORE_ID)}`, {
headers,
next: { revalidate: 60 },
}),
])
if (!promoRes.ok) throw new Error(`Promo request failed: ${promoRes.status}`)
if (!galleryRes.ok) throw new Error(`Gallery request failed: ${galleryRes.status}`)
const promoJson = (await promoRes.json()) as PromoPayload
const galleryJson = (await galleryRes.json()) as GalleryPayload
const publishedGallery = (galleryJson.gallery ?? [])
.filter(item => item?.isPublished !== false && item?.url)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
return { promo: promoJson.promo, gallery: publishedGallery }
}
Then render it in a Server Component page:
// app/promo/page.tsx
import { fetchPromoAndGallery } from '@/lib/sedifexPromo'
export default async function PromoPage() {
const { promo, gallery } = await fetchPromoAndGallery()
return (
<main>
<h1>{promo?.title ?? 'Latest promo'}</h1>
{promo?.summary ? <p>{promo.summary}</p> : null}
{promo?.imageUrl ? <img src={promo.imageUrl} alt={promo.imageAlt ?? 'Promo image'} /> : null}
<section>
<h2>Gallery</h2>
{gallery.length ? (
<ul>
{gallery.map(item => (
<li key={item.id}>
<img src={item.url} alt={item.alt ?? 'Gallery image'} />
{item.caption ? <p>{item.caption}</p> : null}
</li>
))}
</ul>
) : (
<p>No published gallery items yet.</p>
)}
</section>
</main>
)
}
Quick troubleshooting checklist for promo/gallery:
v1IntegrationPromo and integrationGallery.x-api-key and X-Sedifex-Contract-Version.storeId is not empty in your runtime env.NEXT_PUBLIC_ prefix).isPublished !== false and sort by sortOrder.Use this endpoint when you want to render “best sellers” on your public website:
GET /integrationTopSelling?storeId=<storeId>&days=30&limit=10x-api-key: <master_or_store_integration_key>days (optional, default 30, min 1, max 365)limit (optional, default 10, min 1, max 50)Response shape:
{
"storeId": "store_123",
"windowDays": 30,
"generatedAt": "2026-04-06T10:00:00.000Z",
"topSelling": [
{
"productId": "abc",
"name": "Jollof Rice",
"category": "Meals",
"imageUrl": "https://...",
"imageUrls": ["https://...", "https://.../side-angle.jpg"],
"imageAlt": "Plate of jollof rice",
"itemType": "product",
"qtySold": 84,
"grossSales": 3780,
"lastSoldAt": "2026-04-06T08:10:11.000Z"
}
]
}
Use SWR on top of server-rendered data for near-live stock while preserving fast first paint.
If your storefront is WordPress, continue with:
docs/wordpress-install-guide.mddocs/wordpress-plugin/sedifex-sync.phpUse the same dedupe key, fallback data pattern, and cache guidance from this quickstart.
teamMembers) and storeId assignments accurate.Yes, but each authenticated context must only access stores that user is authorized for.
Return static fallback products so your UI keeps rendering instead of crashing.
id|storeId|name|price?It removes repeated rows when multiple sources return the same product representation.
If you need this in another format (REST proxy endpoint, WordPress plugin, or server-side Node worker), keep the same product contract and tenant-scoped authorization model.
imageUrl remains the primary/legacy photo field.imageUrls can contain 1..n URLs when a merchant wants 2-3 product photos on downstream websites.imageUrls[0] when present, then fall back to imageUrl.
For all-store admin pulls, call
v1IntegrationProductswith the admin master key and omitstoreId.