sedifex

Sedifex Integration Quickstart (Next.js + WordPress)

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.

What you get

After setup, Website A can fetch and render:

Customer 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

TikTok OAuth server params (required for “Connect TikTok account”)

If you want store owners to connect TikTok from Account Overview, set these Cloud Functions params:

How to set them:

  1. From project root, run: firebase deploy --only functions
  2. Firebase CLI will prompt for any missing defineString(...) params and write them to functions/.env.<projectId>.
  3. Redeploy after updates so new values are available at runtime.

TIKTOK_REDIRECT_URI must exactly match the callback URL configured in your TikTok developer app, for example:

FAQ

Sedifex Market / BuySedifex

If you are integrating the dedicated Sedifex market frontend (buysedifex repo), follow the cross-repo communication plan in:

It defines a pull + webhook model, contract versioning, reliability, and rollout sequencing so both repos can ship independently without API drift.

Prerequisites

  1. Sedifex Firebase project configured (Firestore + Functions).
  2. You have either:
    • a configured master key (SEDIFEX_INTEGRATION_API_KEY), or
    • an active store integration key from Account overview → Integrations → Website integrations.
  3. Your website runtime can make HTTPS requests.

Integration flow

Base URL:

Steps used in the integration flow

  1. Load required Sedifex environment config
    • SEDIFEX_API_BASE_URL
    • SEDIFEX_STORE_ID
    • SEDIFEX_INTEGRATION_API_KEY (or legacy SEDIFEX_INTEGRATION_KEY)
    • SEDIFEX_CONTRACT_VERSION (defaults to 2026-04-13)
  2. Build authenticated GET requests
    • x-api-key
    • X-Sedifex-Contract-Version
    • Accept: application/json
    • Use Next.js revalidation: next: { revalidate: 60 }
  3. Fetch products, promo, and gallery in parallel
    • In getHomePageData(), request all three endpoints with Promise.all(...):
      • GET /v1IntegrationProducts?storeId=<storeId>
      • GET /v1IntegrationPromo?storeId=<storeId>
      • GET /integrationGallery?storeId=<storeId>
    • (This endpoint set is also listed in the root README and this quickstart.)
  4. Normalize and clean each payload
    • Products: normalize image fields, dedupe products, and filter to service-type products when available.
    • Promo: search nested payloads and map flexible key variants (promoTitle, promo_title, etc.) into a unified promo object.
    • Gallery: normalize image/alt fields, keep published items only, and sort by sortOrder.
  5. Apply resilience fallback logic
    • If config is missing, fetch fails, or data is incomplete, fall back to local curated data:
      • fallbackProducts
      • fallbackPromo
      • fallbackGallery
  6. Expose merged data to pages/components
    • getHomePageData() powers:
      • Home page (products + promo + gallery)
      • Gallery page (gallery)
      • Services page (products)

Additional available integration endpoints

Booking/registration note:

Common 404 fix (important)

If your app logs a 404 such as:

your 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:

Do not build routes like /<contractVersion>/products.

Shared integration types

Import shared interfaces from shared/integrationTypes.ts in both Sedifex and Buy Sedifex projects to avoid field drift:

If you publish these to npm, keep the package version aligned with the contract header date (X-Sedifex-Contract-Version).


1) Server fetch with dedupe + fallback

// 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>
  )
}

2) Cache strategy (important)

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:

4) Top-selling products endpoint (new)

Use this endpoint when you want to render “best sellers” on your public website:

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"
    }
  ]
}

5) Optional live refresh with SWR

Use SWR on top of server-rendered data for near-live stock while preserving fast first paint.


WordPress tutorial

If your storefront is WordPress, continue with:

Use the same dedupe key, fallback data pattern, and cache guidance from this quickstart.

Security checklist

Operational checklist

FAQ

Can Website A read products for multiple stores?

Yes, but each authenticated context must only access stores that user is authorized for.

What if external fetch fails?

Return static fallback products so your UI keeps rendering instead of crashing.

Why deduplicate by 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.

Product photos: one vs multiple images