Supercommerce API Docs
Store API

Dynamic Link Module — Storefront

HTTP surface for reading dynamic link groups by slug. Dynamic link groups are CMS-style ordered collections of {image, text, url} cards — used for the storefront home grid, promo…

HTTP surface for reading dynamic link groups by slug. Dynamic link groups are CMS-style ordered collections of {image, text, url} cards — used for the storefront home grid, promo tiles, "explore" rows, and any other content slot whose layout is data-driven rather than hard-coded.

Source: api-modules/dynamic-link/src/controllers/public-dynamic-link-group.controller.ts.

Admin CRUD for groups and links is in docs/separated/admin/dynamic-link.md. The storefront uses one read endpoint to fetch a group + its ordered links in a single call.


Conventions

Authentication

EndpointAuth
GET /store/dynamic-link-groups/slug/:slugnone (public)

Dynamic links are public marketing content — no session, no PII.

Response envelope

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200
}

Error envelope

statusCodeerrorCode examples
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Domain types

DynamicLinkResponse

type DynamicLinkResponse = {
  id: string;
  groupId: string;
  image: string | null;                // storage keyresolve via storage CDN
  url: string | null;                  // click destination
  text: string | null;                 // caption / label
  order: number;                       // sort keylinks are emitted in ASC order
  isActive: boolean;                   // always true hereinactive links are filtered out server-side
  platform: "APP" | "WEB" | "BOTH";    // default BOTH; the requested platform filter, when any, always matches
  startsAt: string | null;             // ISO; inclusive window start, null = no start bound
  endsAt: string | null;               // ISO; exclusive window end, null = no end bound
  metadata: Record<string, unknown> | null;
  createdAt: string;                   // ISO
  updatedAt: string;                   // ISO
};

A link carries an optional promotional window. Both ends are independent and nullable, so a link with neither is always live. This endpoint already applies the window, so every link in links is showing right now — startsAt / endsAt are exposed only so a client can label a limited-time banner.

DynamicLinkGroupWithLinksResponse

type DynamicLinkGroupWithLinksResponse = {
  id: string;
  title: string;
  slug: string;                        // lowercase alnum + hyphens, e.g. "home-grid"
  metadata: Record<string, unknown> | null;
  createdAt: string;                   // ISO
  updatedAt: string;                   // ISO
  links: DynamicLinkResponse[];        // sorted by `order` ASC
  nextTransitionAt: string | null;     // ISO; when `links` next changes, null = nothing scheduled
};

nextTransitionAt is the earliest schedule boundary still ahead of now across the group's active, platform-matching links — the moment this payload stops being correct. Clients should refresh exactly then rather than poll; null means nothing is scheduled and the payload holds until an operator edits it.


Endpoints

Returns the group plus its nested links array, sorted by order ASC. Only links with isActive: true are included — links an admin has toggled off are silently omitted. Links outside their promotional window are omitted too: startsAt is inclusive and endsAt exclusive, so a window of [10:00, 12:00) shows the link at exactly 10:00:00 and drops it at exactly 12:00:00; a null boundary leaves that side unbounded. Optionally filter further by platform via ?platform=APP|WEB or the x-platform header (the query param wins if both are sent); a link is included when its platform is BOTH or matches the requested one. Used by the storefront to populate a named content slot in a single round-trip.

The response is time-dependent, so a cached copy goes stale with no write behind it. Two mechanisms keep consumers honest: nextTransitionAt tells a client when to refresh, and the API pushes the dynamic-links cache tag to the storefront's /api/revalidate/tags when a boundary passes (see Scheduling & cache invalidation below).

Path params

NameNotes
slugGroup slug (lowercase alnum + hyphens)

Query params

NameNotes
platformAPP / WEB / BOTH, optional. Absent or BOTH returns links for every platform.

Headers

NameNotes
x-platformAPP / WEB / BOTH, optional fallback used only when the platform query param is omitted.

Response 200DynamicLinkGroupWithLinksResponse.

{
  "data": {
    "id": "01J9...",
    "title": "Home Grid",
    "slug": "home-grid",
    "metadata": null,
    "createdAt": "2026-04-01T08:00:00.000Z",
    "updatedAt": "2026-05-01T08:00:00.000Z",
    "links": [
      {
        "id": "01J9...",
        "groupId": "01J9...",
        "image": "dlinks/2026-05/promo-1.jpg",
        "url": "/sale",
        "text": "Summer Sale",
        "order": 0,
        "isActive": true,
        "platform": "BOTH",
        "startsAt": null,
        "endsAt": "2026-05-08T18:30:00.000Z",
        "metadata": null,
        "createdAt": "2026-05-01T08:00:00.000Z",
        "updatedAt": "2026-05-01T08:00:00.000Z"
      }
    ],
    "nextTransitionAt": "2026-05-08T18:30:00.000Z"
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
404NOT_FOUNDNo group matches the slug

Scheduling & cache invalidation

A scheduled link starts and stops rendering with no write behind it, so nothing emits a domain event at the boundary. Three layers cover the gap:

  1. The endpoint is authoritative. The window is applied per request, so a direct caller is always correct — including the mobile app, which holds no Next.js cache.
  2. The API sweeps for boundaries. A worker-side repeatable job (dynamic-link-schedule-sweep, every 60s) looks for a startsAt / endsAt that has just passed and pushes the dynamic-links tag to the storefront's /api/revalidate/tags. The lookback is deliberately wider than the interval so one dropped tick still flushes; a duplicate flush is idempotent.
  3. Clients refresh at nextTransitionAt. A tag flush cannot reach a browser tab that is already open, so the storefront hook drives both its stale window and its refetch timer off this field.

A gap longer than the sweep's lookback — a worker outage — falls through to the storefront's own TTL on the tagged fetch (5 minutes). That is the worst-case staleness for the web; the endpoint itself never serves an out-of-window link.


  • storage — resolves image keys to CDN URLs on the client.
  • banner — adjacent CMS surface for taxonomy-pinned promotional banners. See banner.md.

On this page