Developer Portal

API Documentation

Build custom websites and applications with The Digital Fair CMS. Our REST API gives you full access to your content.

Getting Started

The Digital Fair CMS provides a headless CMS API that allows you to build custom websites and applications using your content. With our REST API, you can:

  • Retrieve portfolio/project content - Projects, artwork, case studies
  • Retrieve blog posts - Articles, news, updates
  • Retrieve links page data - Page configuration and links
  • Access media attachments - Images and files associated with content

Key Concepts

Tenant

A user/organization with their own content namespace

API Key

Unique key for authenticating API requests

Content Item

A single piece of content (project, blog post, etc.)

Content Type

Category: project, blog, event, book, page, etc.

Authentication

All external API endpoints require an API Key passed as a query parameter:

?api_key=your_api_key_here

API Key Types

Each tenant has two API keys for different use cases:

Key TypePurposeContent Access
Production API KeyFor production websitesPublished content only (default)
Preview API KeyFor staging/development environmentsAll content (draft, published, archived)

Preview Key Warning

The Preview API Key exposes unpublished content (drafts and archived items).

  • Do not use the preview key in production environments
  • Responses include a preview_mode: true flag when using the preview key
  • Use it only for staging sites, development, and content preview

Getting Your API Keys

Both API keys are available in your tenant dashboard under Settings > API Configuration. Each key can be regenerated independently without affecting the other.

Security Notes:

  • Production keys provide read-only access to published content by default
  • Preview keys expose unpublished content and should never be used in production
  • Never expose API keys in client-side code (use server-side fetching)
  • API keys are scoped to a single tenant

Base URLs

Production

https://thedigitalfair.com/api/v1

Development

http://localhost:3000/api/v1

Content API

GET/api/v1/content

Retrieve all content items for a tenant with optional filtering.

Query Parameters

ParameterTypeDescription
api_key*stringTenant's API key
typestringFilter by content type: project, blog, event, book, page
statusstringContent status: published (default), draft, archived
series_idstringFilter by series UUID
limitnumberNumber of items to return (default: 50, max: 50)
offsetnumberPagination offset (default: 0)

Example Request

curl "https://thedigitalfair.com/api/v1/content?api_key=your_api_key_here&type=project&status=published&limit=10"

Example Response

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "tenant_id": "tenant-uuid",
        "content_type": "project",
        "title": "Website Redesign for Acme Corp",
        "slug": "acme-corp-redesign",
        "subtitle": "A complete brand refresh",
        "description": "Led the complete redesign of Acme Corp's digital presence...",
        "content_body": "<p>Full HTML content here...</p>",
        "featured_image": "https://storage.supabase.co/media/tenant-id/image.jpg",
        "status": "published",
        "date_published": "2024-01-15T10:00:00Z",
        "sort_order": 10,
        "tags": ["web-design", "branding", "case-study"],
        "project_link": "https://acme-corp.com",
        "created_at": "2024-01-10T08:00:00Z",
        "updated_at": "2024-01-15T10:00:00Z"
      }
    ],
    "pagination": {
      "limit": 50,
      "offset": 0,
      "total": 25
    }
  }
}
GET/api/v1/content/{id_or_slug}

Retrieve a specific content item by ID or slug. Includes media attachments.

Path Parameters

ParameterTypeDescription
id_or_slug*stringEither the UUID or the slug of the content item

Example Request

// By slug
curl "https://thedigitalfair.com/api/v1/content/acme-corp-redesign?api_key=your_api_key_here"

// By UUID
curl "https://thedigitalfair.com/api/v1/content/550e8400-e29b-41d4-a716-446655440000?api_key=your_api_key_here"

Example Response

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Website Redesign for Acme Corp",
    "slug": "acme-corp-redesign",
    "content_body": "<p>Full HTML content with rich text formatting...</p>",
    "featured_image": "https://storage.supabase.co/media/tenant-id/hero.jpg",
    // ... all content fields
    "media": [
      {
        "id": "media-uuid-1",
        "media_type": "image",
        "url": "https://storage.supabase.co/media/tenant-id/screenshot1.jpg",
        "alt_text": "Homepage design",
        "sort_order": 0
      },
      {
        "id": "media-uuid-2",
        "media_type": "image",
        "url": "https://storage.supabase.co/media/tenant-id/screenshot2.jpg",
        "alt_text": "Mobile view",
        "sort_order": 1
      }
    ]
  }
}

Event Timezone Handling

For event content types, the event_date is stored in UTC. Use the event_timezone field (IANA timezone identifier) to display the event in the correct local time.

// Example response for an event
{
  "event_date": "2025-01-15T20:00:00.000Z",  // UTC
  "event_timezone": "America/New_York",       // IANA timezone
  "location": "Barnes & Noble, 5th Avenue, New York, NY"
}

// Display as: "Jan 15, 2025 3:00 PM EST"

Content Types

The Digital Fair CMS supports multiple content types, each designed for specific use cases. All content types share a common set of fields, with additional type-specific fields for specialized data.

Content Type URL Paths

When building URLs for your website, use these user-friendly paths instead of the raw content_type values from the API:

Database content_typeURL PathExample URL
blog_post/blogyoursite.com/blog/my-article
book/booksyoursite.com/books/my-novel
series/seriesyoursite.com/series/my-series
event/eventsyoursite.com/events/book-signing
artwork/artworkyoursite.com/artwork/landscape-1
project/projectsyoursite.com/projects/client-work
page(root)yoursite.com/about

Helper Function (TypeScript)

const CONTENT_TYPE_PATHS: Record<string, string> = {
  blog_post: 'blog',
  book: 'books',
  series: 'series',
  event: 'events',
  artwork: 'artwork',
  project: 'projects',
  page: '',
}

function getContentTypePath(contentType: string): string {
  return CONTENT_TYPE_PATHS[contentType] ?? contentType
}

// Usage
const item = await fetchContent('my-post')
const url = `https://yoursite.com/${getContentTypePath(item.content_type)}/${item.slug}`
// For blog_post: https://yoursite.com/blog/my-post

Important: Always use these user-friendly paths in your website URLs rather than the raw content_type value from the API. This ensures consistency with The Digital Fair CMS social media sharing and preview links.

Common Fields (All Content Types)

These fields are available on every content item, regardless of type:

FieldTypeDescription
idstring (UUID)Unique identifier for the content item
tenant_idstring (UUID)ID of the tenant who owns this content
content_typestringType of content (see Content Types section)
titlestringDisplay title of the content
slugstringURL-safe identifier used in URLs
subtitlestring | nullOptional subtitle or tagline
descriptionstring | nullShort description or excerpt (typically shown in lists)
content_bodystring | nullFull HTML content (rich text)
featured_imagestring | nullURL to the main/hero image
statusstringContent status: "draft", "published", or "archived"
sort_ordernumberDisplay order (higher values appear first)
tagsstring[] | nullArray of tag strings for categorization
core_attributesobjectStandard attributes specific to the content type
custom_attributesobjectUser-defined custom fields
meta_titlestring | nullSEO title (falls back to title if not set)
meta_descriptionstring | nullSEO description (falls back to description if not set)
created_attimestampWhen the content was created (ISO 8601)
updated_attimestampWhen the content was last updated (ISO 8601)

Blog Post

Articles, news updates, and written content. Ideal for sharing thoughts, tutorials, announcements, and regular updates.

type=blogtype=blog_post

Type-Specific Fields

FieldTypeDescription
published_datedateOriginal publication date (YYYY-MM-DD format)
content_updated_datetimestampAuto-set when content is edited after publishing

Example Response

{
  "content_type": "blog",
  "title": "10 Tips for Better Web Design",
  "slug": "10-tips-better-web-design",
  "description": "Learn the essential principles of effective web design...",
  "content_body": "<p>Full article HTML content...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/blog-hero.jpg",
  "published_date": "2024-06-15",
  "tags": ["web-design", "tips", "ux"],
  "meta_title": "10 Tips for Better Web Design | Your Site",
  "meta_description": "Discover the essential principles..."
}

Project

Portfolio pieces, case studies, and work samples. Perfect for showcasing completed work with details about the process and outcomes.

type=project

Type-Specific Fields

FieldTypeDescription
project_linkstring | nullExternal URL to the live project or demo
core_attributes.client_namestringName of the client (if applicable)
core_attributes.project_yearstringYear the project was completed
core_attributes.servicesstring[]List of services provided (e.g., ["Web Design", "Branding"])

Example Response

{
  "content_type": "project",
  "title": "Website Redesign for Acme Corp",
  "slug": "acme-corp-redesign",
  "subtitle": "A complete brand refresh",
  "description": "Led the complete redesign of Acme Corp's digital presence...",
  "content_body": "<p>Detailed case study...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/project-hero.jpg",
  "project_link": "https://acme-corp.com",
  "core_attributes": {
    "client_name": "Acme Corp",
    "project_year": "2024",
    "services": ["Web Design", "Branding", "Development"]
  },
  "tags": ["web-design", "branding", "case-study"],
  "media": [
    { "url": "...", "alt_text": "Homepage design", "sort_order": 0 },
    { "url": "...", "alt_text": "Mobile view", "sort_order": 1 }
  ]
}

Book

Published books, novels, and written works. Includes support for series organization and purchase links.

type=book

Type-Specific Fields

FieldTypeDescription
subtitlestring | nullBook subtitle or tagline
published_datedatePublication date of the book
purchase_linkstring | nullURL to purchase the book (Amazon, bookstore, etc.)
series_idstring | nullUUID of the parent series (if part of a series)
series_numbernumber | nullPosition within the series (1, 2, 3, etc.)
custom_attributes.isbnstringISBN number
custom_attributes.page_countnumberNumber of pages
custom_attributes.publisherstringPublisher name

Example Response

{
  "content_type": "book",
  "title": "The Great Adventure",
  "slug": "the-great-adventure",
  "subtitle": "A Tale of Discovery",
  "description": "An epic journey through uncharted territories...",
  "content_body": "<p>Book synopsis and details...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/book-cover.jpg",
  "published_date": "2024-03-15",
  "purchase_link": "https://amazon.com/dp/B0123456789",
  "series_id": "series-uuid-here",
  "series_number": 1,
  "custom_attributes": {
    "isbn": "978-3-16-148410-0",
    "page_count": 350,
    "publisher": "Acme Publishing"
  },
  "tags": ["fiction", "adventure", "fantasy"]
}

Series

Book series or collections. Use this to group related books together and provide series-level information.

type=series

Books linked to a series will have the series_id field set to this item's ID. Query books in a series using the series_id filter parameter.

Type-Specific Fields

FieldTypeDescription
subtitlestring | nullSeries subtitle or tagline

Example Response

{
  "content_type": "series",
  "title": "The Adventure Chronicles",
  "slug": "adventure-chronicles",
  "subtitle": "An Epic Fantasy Saga",
  "description": "Follow our heroes across five thrilling novels...",
  "content_body": "<p>Series overview and reading order...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/series-banner.jpg",
  "tags": ["fantasy", "adventure", "series"]
}

// To get all books in this series:
// GET /api/v1/content?type=book&series_id={series-uuid}

Event

Upcoming and past events, appearances, signings, and performances. Includes timezone-aware date handling.

type=event

The event_date is stored in UTC. Use the event_timezone field to display the event in the correct local time. For example, event_date "2025-01-15T20:00:00Z" with event_timezone "America/New_York" should display as "Jan 15, 2025 3:00 PM EST".

Type-Specific Fields

FieldTypeDescription
event_datetimestampEvent date and time stored in UTC (ISO 8601 format)
event_timezonestringIANA timezone identifier (e.g., "America/New_York")
locationstring | nullVenue name and address

Example Response

{
  "content_type": "event",
  "title": "Book Signing at Barnes & Noble",
  "slug": "book-signing-barnes-noble-2025",
  "description": "Join me for a signing of my latest novel...",
  "content_body": "<p>Event details and what to expect...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/event.jpg",
  "event_date": "2025-01-15T20:00:00.000Z",
  "event_timezone": "America/New_York",
  "location": "Barnes & Noble, 5th Avenue, New York, NY",
  "tags": ["book-signing", "nyc", "meet-and-greet"]
}

Artwork

Visual art pieces, illustrations, photography, and creative works. Ideal for artists showcasing their portfolio.

type=artwork

Type-Specific Fields

FieldTypeDescription
published_datedateDate the artwork was created or published
purchase_linkstring | nullURL to purchase prints or originals
custom_attributes.mediumstringArt medium (e.g., "Oil on canvas", "Digital")
custom_attributes.dimensionsstringPhysical dimensions (e.g., "24x36 inches")
custom_attributes.availablebooleanWhether the piece is available for purchase

Example Response

{
  "content_type": "artwork",
  "title": "Sunset Over Mountains",
  "slug": "sunset-over-mountains",
  "description": "A vibrant landscape capturing the golden hour...",
  "featured_image": "https://storage.supabase.co/media/tenant-id/artwork.jpg",
  "published_date": "2024-08-20",
  "purchase_link": "https://shop.example.com/prints/sunset-mountains",
  "custom_attributes": {
    "medium": "Oil on canvas",
    "dimensions": "24x36 inches",
    "available": true
  },
  "tags": ["landscape", "oil-painting", "nature"],
  "media": [
    { "url": "...", "alt_text": "Detail shot", "sort_order": 0 },
    { "url": "...", "alt_text": "Framed view", "sort_order": 1 }
  ]
}

Page

Static pages like About, Contact, or custom landing pages. General-purpose content for any page on your site.

type=page

Example Response

{
  "content_type": "page",
  "title": "About Me",
  "slug": "about",
  "description": "Learn more about who I am and my creative journey.",
  "content_body": "<p>Full page content with rich formatting...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/about-hero.jpg",
  "meta_title": "About | Jane Smith",
  "meta_description": "Learn about Jane Smith, an award-winning author..."
}

Music / Performance

Albums, singles, performances, and musical works. For musicians and performers showcasing their audio content.

type=musictype=albumtype=single

Type-Specific Fields

FieldTypeDescription
published_datedateRelease date
purchase_linkstring | nullURL to streaming service or purchase page
custom_attributes.spotify_urlstringSpotify album/track link
custom_attributes.apple_music_urlstringApple Music link
custom_attributes.durationstringTrack/album duration

Example Response

{
  "content_type": "music",
  "title": "Midnight Dreams",
  "slug": "midnight-dreams-album",
  "subtitle": "Debut Studio Album",
  "description": "A collection of 12 original songs exploring...",
  "content_body": "<p>Track listing and album notes...</p>",
  "featured_image": "https://storage.supabase.co/media/tenant-id/album-cover.jpg",
  "published_date": "2024-09-01",
  "purchase_link": "https://open.spotify.com/album/...",
  "custom_attributes": {
    "spotify_url": "https://open.spotify.com/album/...",
    "apple_music_url": "https://music.apple.com/album/...",
    "duration": "45:32"
  },
  "tags": ["indie", "folk", "acoustic"]
}

Media Attachments

When fetching a single content item by ID or slug, the response includes a media array containing all attached images and files. Media is not included in list responses to reduce payload size.

// Media array structure (included in single-item responses)
"media": [
  {
    "id": "media-uuid-1",
    "content_id": "content-uuid",
    "tenant_id": "tenant-uuid",
    "media_type": "image",           // 'image' | 'video' | 'document'
    "url": "https://storage.supabase.co/media/tenant-id/image.jpg",
    "alt_text": "Description for accessibility",
    "size_bytes": 245000,
    "metadata": {},                  // Additional metadata (dimensions, etc.)
    "sort_order": 0,                 // Display order (lower = first)
    "created_at": "2024-01-10T08:00:00Z"
  }
]

Custom Fields

Beyond the standard fields, tenants can define custom fields for any content type. These are stored in the custom_attributes object and can contain any JSON-compatible data.

// Example custom_attributes for a book
"custom_attributes": {
  "isbn": "978-3-16-148410-0",
  "page_count": 350,
  "publisher": "Acme Publishing",
  "awards": ["Hugo Award 2024", "Nebula Finalist"],
  "reading_level": "adult"
}

// Custom fields are defined per-tenant in the admin dashboard
// and can be of types: text, number, boolean, date, or select

Press Kit API

Retrieve a tenant's press kit data to build custom press kit pages on your own domain. Press kits include bios, images, media embeds, and section-specific content.

GET/api/v1/press-kit

Retrieve the complete press kit including bios, images, embeds, and all section content.

Query Parameters

ParameterTypeDescription
api_key*stringTenant's API key
passwordstringPassword for password-protected press kits

Example Request

curl "https://thedigitalfair.com/api/v1/press-kit?api_key=your_api_key_here"

Example Response

{
  "success": true,
  "data": {
    "pressKit": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "tenantId": "tenant-uuid",
      "title": "Jane Smith",
      "slug": "press",
      "isPublished": true,
      "isPasswordProtected": false,
      "allowIndexing": true,
      "bios": {
        "short": "Jane Smith is a bestselling author...",
        "medium": "Jane Smith is a bestselling author known for...",
        "full": "Jane Smith began writing at age twelve..."
      },
      "contactEmail": "press@janesmith.com",
      "socialLinks": [
        { "platform": "instagram", "url": "https://instagram.com/janesmith" }
      ],
      "interviewQuestions": [
        "What inspired you to become a writer?",
        "How do you develop your characters?"
      ],
      "images": [
        {
          "id": "image-uuid",
          "caption": "Author headshot",
          "originalUrl": "https://storage.supabase.co/...",
          "thumbnailUrl": "https://storage.supabase.co/...",
          "width": 2400,
          "height": 3200
        }
      ],
      "embeds": [
        {
          "id": "embed-uuid",
          "platform": "youtube",
          "embedUrl": "https://youtube.com/watch?v=...",
          "embedHtml": "<iframe ...>",
          "title": "Book Trailer"
        }
      ]
    },
    "tenant": {
      "subdomain": "janesmith",
      "name": "Jane Smith",
      "siteType": "author"
    },
    "enabledSections": ["bios", "images", "embeds", "interview_questions", "contact"]
  },
  "preview_mode": false
}

Password-Protected Response

If the press kit is password-protected and no password is provided:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Jane Smith",
    "slug": "press",
    "isPasswordProtected": true,
    "requiresPassword": true
  }
}

Section-Specific Fields

Different site types have different sections available:

Site TypeSpecial Sections
AuthorsampleChapterUrl, comparisonTitles, speakingTopics
ArtistartistStatement, exhibitionHistory, commissionInfo
MusiciantechnicalRider, stagePlotUrl, genreDescription, notableVenues
ProfessionalCommon sections only (bios, images, embeds, contact)

Analytics & Tracking

Track user engagement on your custom website using Umami analytics. This enables you to see purchase link clicks and other custom events in your tenant analytics dashboard.

Setting Up Umami Tracking

If your tenant has analytics enabled, you'll receive an Umami Website ID from your admin. Add the Umami tracking script to your website's <head> section:

<!-- Add to your HTML <head> section -->
<script
  defer
  src="https://cloud.umami.is/script.js"
  data-website-id="YOUR_WEBSITE_ID"
></script>

Replace YOUR_WEBSITE_ID with the Website ID provided in your tenant settings.

Framework Integration

Next.js (App Router)

// app/layout.tsx
import Script from 'next/script'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <head>
        <Script
          defer
          src="https://cloud.umami.is/script.js"
          data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

React (Vite/CRA)

// index.html
<!DOCTYPE html>
<html>
  <head>
    <script
      defer
      src="https://cloud.umami.is/script.js"
      data-website-id="YOUR_WEBSITE_ID"
    ></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Tracking Purchase Clicks

To track when users click purchase links (e.g., links to Amazon, bookstores, etc.), use the purchase_click event:

// Track a purchase link click
window.umami?.track('purchase_click', {
  retailer: 'Amazon',           // The retailer name (e.g., 'Amazon', 'Barnes & Noble')
  book: 'The Great Adventure',  // The book/product title
})

This data will appear in your Analytics Dashboard under "Purchase Clicks", showing total clicks, clicks by retailer, and clicks by book.

React Component Example

interface PurchaseLink {
  retailer: string
  url: string
}

interface BookCardProps {
  title: string
  purchaseLinks: PurchaseLink[]
}

function BookCard({ title, purchaseLinks }: BookCardProps) {
  const handlePurchaseClick = (retailer: string) => {
    // Track the click before navigation
    if (typeof window !== 'undefined' && window.umami) {
      window.umami.track('purchase_click', {
        retailer,
        book: title,
      })
    }
  }

  return (
    <div className="book-card">
      <h3>{title}</h3>
      <div className="purchase-links">
        {purchaseLinks.map((link) => (
          <a
            key={link.retailer}
            href={link.url}
            target="_blank"
            rel="noopener noreferrer"
            onClick={() => handlePurchaseClick(link.retailer)}
          >
            Buy on {link.retailer}
          </a>
        ))}
      </div>
    </div>
  )
}

TypeScript Declaration

Add this type declaration for proper TypeScript support:

// types/umami.d.ts
interface UmamiTracker {
  track: (eventName: string, eventData?: Record<string, string | number>) => void
}

declare global {
  interface Window {
    umami?: UmamiTracker
  }
}

export {}

Supported Events

Event NamePropertiesDescription
purchase_clickretailer, bookUser clicked a purchase/buy link

Additional events may be added in future versions. Check the changelog for updates.

TypeScript Types

Content Item Type

interface ContentItem {
  id: string;                          // UUID
  tenant_id: string;                   // UUID of the tenant
  content_type: string;                // 'project' | 'blog' | 'event' | 'book' | 'page'
  title: string;                       // Display title
  slug: string;                        // URL-safe identifier
  subtitle: string | null;             // Optional subtitle
  description: string | null;          // Short description/excerpt
  content_body: string | null;         // Full HTML content
  featured_image: string | null;       // URL to main image
  status: 'draft' | 'published' | 'archived';
  date_published: string | null;       // ISO 8601 timestamp
  event_date: string | null;           // For events - UTC ISO 8601
  event_timezone: string | null;       // For events - IANA timezone
  sort_order: number;                  // Display order (higher = first)
  tags: string[] | null;               // Array of tag strings
  core_attributes: Record<string, any>;
  custom_attributes: Record<string, any>;
  project_link: string | null;         // External project URL
  purchase_link: string | null;        // Buy/purchase URL
  series_id: string | null;            // UUID of parent series
  series_number: number | null;        // Order within series
  location: string | null;             // For events - location
  meta_title: string | null;           // SEO title
  meta_description: string | null;     // SEO description
  created_at: string;                  // ISO 8601 timestamp
  updated_at: string;                  // ISO 8601 timestamp
}

Content Media Type

interface ContentMedia {
  id: string;                          // UUID
  content_id: string;                  // UUID of parent content item
  tenant_id: string;                   // UUID of tenant
  media_type: string;                  // 'image' | 'video' | 'document'
  url: string;                         // Public URL to media file
  alt_text: string | null;             // Accessibility text
  size_bytes: number | null;           // File size
  metadata: Record<string, any>;       // Additional metadata
  sort_order: number;                  // Display order
  created_at: string;                  // ISO 8601 timestamp
}

API Response Types

// List response with pagination
interface ContentListResponse {
  success: true;
  data: {
    items: ContentItem[];
    pagination: {
      limit: number;
      offset: number;
      total: number;
    };
  };
}

// Single item response (includes media)
interface ContentItemResponse {
  success: true;
  data: ContentItem & {
    media: ContentMedia[];
  };
}

// Error response
interface ErrorResponse {
  success: false;
  error: string;
  code?: string;
  details?: string;
}

Code Examples

JavaScript / TypeScript

const API_KEY = process.env.CMS_API_KEY; // Server-side only!
const BASE_URL = 'https://thedigitalfair.com/api/v1';

async function fetchProjects() {
  const response = await fetch(
    `${BASE_URL}/content?api_key=${API_KEY}&type=project&status=published`
  );

  if (!response.ok) {
    throw new Error(`API Error: ${response.status}`);
  }

  const data = await response.json();

  if (!data.success) {
    throw new Error('Failed to fetch projects');
  }

  return data.data.items;
}

// Usage
const projects = await fetchProjects();
projects.forEach(project => {
  console.log(`${project.title}: ${project.description}`);
});

Next.js Server-Side Fetching

// app/portfolio/page.tsx
import { Metadata } from 'next';

const API_KEY = process.env.CMS_API_KEY!; // Server-side only
const BASE_URL = 'https://thedigitalfair.com/api/v1';

async function getProjects() {
  const response = await fetch(
    `${BASE_URL}/content?api_key=${API_KEY}&type=project&status=published`,
    { next: { revalidate: 60 } } // Revalidate every 60 seconds
  );

  if (!response.ok) {
    throw new Error('Failed to fetch projects');
  }

  const data = await response.json();
  return data.data.items;
}

export default async function PortfolioPage() {
  const projects = await getProjects();

  return (
    <main className="container mx-auto py-12">
      <h1 className="text-4xl font-bold mb-8">Portfolio</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {projects.map((project) => (
          <a key={project.id} href={`/portfolio/${project.slug}`}>
            {project.featured_image && (
              <img
                src={project.featured_image}
                alt={project.title}
                className="w-full aspect-video object-cover rounded-lg"
              />
            )}
            <h2 className="text-xl font-semibold mt-4">{project.title}</h2>
            <p className="text-gray-600 mt-2">{project.description}</p>
          </a>
        ))}
      </div>
    </main>
  );
}

Python

import requests
from typing import List, Dict, Any

API_KEY = "your-api-key"
BASE_URL = "https://thedigitalfair.com/api/v1"

def fetch_projects(
    content_type: str = "project",
    status: str = "published",
    limit: int = 50,
    offset: int = 0
) -> List[Dict[str, Any]]:
    """Fetch content items from The Digital Fair CMS."""

    params = {
        "api_key": API_KEY,
        "type": content_type,
        "status": status,
        "limit": limit,
        "offset": offset
    }

    response = requests.get(f"{BASE_URL}/content", params=params)
    response.raise_for_status()

    data = response.json()

    if not data.get("success"):
        raise Exception("API request failed")

    return data["data"]["items"]

# Usage
if __name__ == "__main__":
    projects = fetch_projects()
    for project in projects:
        print(f"- {project['title']} ({project['slug']})"

Error Handling

HTTP Status Codes

CodeMeaningCommon Causes
200SuccessRequest completed successfully
400Bad RequestInvalid parameters, missing required fields
401UnauthorizedMissing or invalid API key
404Not FoundContent item doesn't exist, or links page not configured
500Server ErrorInternal server error

Error Response Format

{
  "success": false,
  "error": "Human-readable error message",
  "code": "ERROR_CODE",
  "details": "Additional details (optional)"
}

Common Error Codes

CodeDescription
INVALID_API_KEYThe provided API key is invalid or expired
NOT_CONFIGUREDLinks page has not been set up for this tenant
NOT_PUBLISHEDLinks page exists but is not published
CONTENT_NOT_FOUNDThe requested content item was not found

Error Handling Example

async function fetchWithErrorHandling(endpoint: string) {
  try {
    const response = await fetch(`${BASE_URL}${endpoint}?api_key=${API_KEY}`);

    if (!response.ok) {
      const errorData = await response.json();

      switch (response.status) {
        case 401:
          throw new Error('Invalid API key. Please check your credentials.');
        case 404:
          if (errorData.code === 'NOT_CONFIGURED') {
            throw new Error('This feature has not been set up yet.');
          }
          if (errorData.code === 'NOT_PUBLISHED') {
            throw new Error('This content is not publicly available.');
          }
          throw new Error('The requested resource was not found.');
        case 500:
          throw new Error('Server error. Please try again later.');
        default:
          throw new Error(errorData.error || 'An unknown error occurred.');
      }
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error('Network error. Please check your connection.');
    }
    throw error;
  }
}

Rate Limiting

Current Status: No rate limiting is currently implemented.

Best Practices

  • Cache responses on your server to reduce API calls
  • Use Incremental Static Regeneration (ISR) in Next.js
  • Avoid making API calls on every page load
  • Use server-side fetching to protect your API key

Need Help?

If you have questions about the API or need assistance with your integration, we're here to help. Reach out to our support team.

Contact Support