Supercommerce API Docs
Admin API

Dynamic Link Module — Admin

HTTP surface for managing dynamic link groups (CMS-style tile collections — e.g. homepage banner slots) and the dynamic links inside each group. Groups are addressable by slug and…

HTTP surface for managing dynamic link groups (CMS-style tile collections — e.g. homepage banner slots) and the dynamic links inside each group. Groups are addressable by slug and read by the public storefront; this admin surface is the only writer.

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

A group is a named collection of links rendered as an ordered list (carousel, tile grid, feature row). A link is one renderable item with optional image / url / text and an order for sorting within its parent group.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

Endpoint groupPermission
GET /admin/dynamic-link-groups, GET /admin/dynamic-link-groups/:iddynamicLinkGroup: read
POST /admin/dynamic-link-groups, POST /admin/dynamic-link-groups/:id/duplicatedynamicLinkGroup: create
PUT /admin/dynamic-link-groups/:iddynamicLinkGroup: update
DELETE /admin/dynamic-link-groups/:iddynamicLinkGroup: delete
GET /admin/dynamic-link-groups/:groupId/linksdynamicLink: read
POST /admin/dynamic-link-groups/:groupId/links, POST /admin/dynamic-link-groups/:groupId/links/:linkId/duplicatedynamicLink: create
PUT /admin/dynamic-link-groups/:groupId/links/:linkId, PATCH /admin/dynamic-link-groups/:groupId/links/reorderdynamicLink: update
DELETE /admin/dynamic-link-groups/:groupId/links/:linkIddynamicLink: delete

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409UNIQUE_VIOLATION (slug collision)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Groups and links are hard-deleted (no soft-delete). Deleting a group cascades to its links via the database foreign key.


Domain types

DynamicLinkGroupResponse

type DynamicLinkGroupResponse = {
  id: string;
  title: string;                                 // 1..255
  slug: string;                                  // lowercase alnum + hyphens, 1..255
  metadata: Record<string, unknown> | null;
  createdAt: string;                             // ISO
  updatedAt: string;
};

type DynamicLinkGroupWithLinksResponse = DynamicLinkGroupResponse & {
  links: DynamicLinkResponse[];                  // sorted by order ASC
};

DynamicLinkResponse

A link must have at least one of image, url, or text — the rest can be null.

type DynamicLinkResponse = {
  id: string;
  groupId: string;
  image: string | null;                          // max 2048 chars
  url: string | null;                            // max 2048 chars
  text: string | null;                           // max 1024 chars
  order: number;                                 // ASC sort within group, >= 0
  metadata: Record<string, unknown> | null;
  createdAt: string;
  updatedAt: string;
};

Base path: /admin/dynamic-link-groups.

Required permission: dynamicLinkGroup: read. Standard QueryDto (page / limit / search / filters[]).

Response 200 — paginated envelope of DynamicLinkGroupResponse[].


Required permission: dynamicLinkGroup: read. Returns the group with nested links[] sorted by order ASC.

Response 200DynamicLinkGroupWithLinksResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-link-groups — Create a group

Required permission: dynamicLinkGroup: create.

Body

{
  "title": "Homepage Hero Tiles",
  "slug": "homepage-hero-tiles",
  "metadata": { "platform": "WEB" }
}
FieldTypeConstraints
titlestring1..255
slugstring1..255, lowercase alnum + hyphens
metadataRecord<string, unknown> | null?Free-form

Response 201DynamicLinkGroupResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
409UNIQUE_VIOLATIONslug already in use

Required permission: dynamicLinkGroup: update. Partial body.

Response 200DynamicLinkGroupResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409UNIQUE_VIOLATIONslug taken by another group

Required permission: dynamicLinkGroup: delete. Hard delete; cascades to child links via DB FK.

Response 204 No Content.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-link-groups/:id/duplicate — Duplicate a group

Required permission: dynamicLinkGroup: create. Clones the source group with a fresh title + slug. Source metadata is copied as-is; every child link is cloned with its order, image, url, text, and metadata.

Body

{ "title": "Homepage Hero Tiles (v2)", "slug": "homepage-hero-tiles-v2" }

Response 201DynamicLinkGroupWithLinksResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDSource group not found
409UNIQUE_VIOLATIONNew slug taken

Base path: /admin/dynamic-link-groups/:groupId/links.

Required permission: dynamicLink: read. Returns the full ordered list (no pagination — link counts per group are small by design).

Response 200DynamicLinkResponse[] sorted by order ASC.

Errors

StatusCodeWhen
404NOT_FOUNDGroup not found

POST .../links — Create a link

Required permission: dynamicLink: create. At least one of image, url, text must be present (whitespace-only inputs are normalized to null before the check).

Body

{
  "image": "tiles/promo-summer.png",
  "url":   "/sale/summer",
  "text":  "Summer Sale",
  "order": 0,
  "metadata": { "campaign": "Q2" }
}
FieldTypeConstraints
imagestring | null?max 2048; trimmed; empty → null
urlstring | null?max 2048
textstring | null?max 1024
orderint>= 0. Default 0
metadataRecord<string, unknown> | null?Free-form

Response 201DynamicLinkResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORAll three of image/url/text are empty, or body otherwise invalid
404NOT_FOUNDGroup not found

Required permission: dynamicLink: update. Partial; the "at least one renderable" rule re-runs against the merged state.

Response 200DynamicLinkResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDLink not in this group

Required permission: dynamicLink: delete. Hard delete.

Response 204 No Content.

Errors

StatusCodeWhen
404NOT_FOUNDLink not in this group

POST .../links/:linkId/duplicate — Duplicate a link

Required permission: dynamicLink: create. Clones the link within the same group with order = max(order) + 1.

Response 201 — cloned DynamicLinkResponse.

Errors

StatusCodeWhen
404NOT_FOUNDLink not in this group

PATCH .../links/reorder — Bulk reorder

Required permission: dynamicLink: update. Reapplies order for every supplied id. The literal route segment reorder is declared before :linkId so it cannot collide with a UUID.

Body

{
  "items": [
    { "linkId": "01J9aaaa...", "order": 0 },
    { "linkId": "01J9bbbb...", "order": 1 },
    { "linkId": "01J9cccc...", "order": 2 }
  ]
}
FieldTypeConstraints
items[].linkIdstringRequired
items[].orderint>= 0
itemsarrayMin 1 entry

Response 200 — reordered DynamicLinkResponse[] in their new order.

Errors

StatusCodeWhen
400VALIDATION_ERROREmpty items, missing linkId, etc.
404NOT_FOUNDGroup not found (links not in group are silently ignored at the service layer)

  • admin-rbac — gates every endpoint via dynamicLinkGroup:* / dynamicLink:*. See admin-rbac.md.
  • store/dynamic-link — unauthenticated storefront read of a group by slug.

On this page