Integration Guide

Data Sync Setup

Connect your app to Copper Analytics using authenticated pull. Copper Analytics periodically fetches data from your site's /api/copper endpoint using a read-only token.

How it works

Your site exposes a private GET /api/copper endpoint that returns data as JSON using the Copper Data API envelope format. Copper Analytics calls this endpoint on a schedule using a read-only token you provide, and caches the response for display in the dashboard.

# Copper Analytics sends:GET https://yoursite.com/api/copperAuthorization: Bearer <your-read-token># Your site responds with:{ "site": "yoursite.com", "fetchedAt": "...", "version": 1, "modules": { "roadmaps": [...] } }

This model keeps things simple: your site is a stateless data source, and Copper Analytics handles all the orchestration, caching, and display logic. The modules object lets you expose multiple data types (roadmaps, blog, etc.) from a single endpoint.

Prerequisites

  • A site registered in Copper Analytics (the domain you want to sync roadmaps for)
  • Roadmap data formatted per the Copper Roadmap Format spec, served via the Copper Data API envelope
  • Access to your site's Settings page at Dashboard > [your site] > Settings

Note

All code examples use gray-matter for parsing Markdown frontmatter. Install it with pnpm add gray-matter or npm install gray-matter.

Setup

  1. Create a GET /api/copper endpoint on your site that returns data in the Copper Data API format (see code examples below).
  2. Generate an API key or token for Copper Analytics to use.
  3. Open your site's Settings in the Copper Analytics dashboard and enable Authenticated Pull.
  4. Enter the endpoint URL and your API key. Choose the auth mode (Bearer, Custom header, or Query parameter).
  5. Click Save settings, then Sync now to verify the connection.

Tip

Copper Analytics will poll your endpoint on a schedule (approximately every 15 minutes). You can trigger an immediate sync anytime by clicking Sync now in the settings.

Auth modes

Copper Analytics supports three ways to send the read-only credential to your endpoint:

ModeHow Copper Analytics sends the credentialWhen to use
Bearer tokenAuthorization: Bearer <credential>Standard REST APIs (default)
Custom headerx-api-key: <credential> (header name is configurable)APIs that expect a named header
Query parameter?api_key=<credential> (param name is configurable)Simple integrations, serverless functions

Code examples

Your endpoint should accept all three auth modes so it works regardless of which mode the user configures in Copper Analytics. The examples below check Bearer token, custom header, and query parameter in that order.

Next.js (App Router)

src/app/api/copper/route.ts
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const API_KEY = process.env.COPPER_API_KEY!

export async function GET(request: NextRequest) {
  // Verify the API key — supports Bearer token, custom header, and query param
  const authHeader = request.headers.get('authorization')
  const apiKeyHeader = request.headers.get('x-api-key')
  const queryKey = request.nextUrl.searchParams.get('api_key')

  const providedKey =
    authHeader?.replace('Bearer ', '') ||
    apiKeyHeader ||
    queryKey

  if (providedKey !== API_KEY) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Read and parse roadmap files
  const dir = path.join(process.cwd(), 'roadmaps')
  const roadmaps = []
  if (fs.existsSync(dir)) {
    const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'))
    for (const file of files) {
      const raw = fs.readFileSync(path.join(dir, file), 'utf-8')
      const { data, content } = matter(raw)
      const slug = file.replace(/\.md$/, '')
      const trimmed = content.trim()

      roadmaps.push({
        slug,
        title: data.title || slug,
        status: data.status || 'proposed',
        date: data.date ? String(data.date) : slug.slice(0, 10),
        priority: data.priority || undefined,
        phase: typeof data.phase === 'number' ? data.phase : undefined,
        tags: Array.isArray(data.tags) ? data.tags : undefined,
        description: trimmed,
        summary: trimmed.slice(0, 500),
      })
    }
    roadmaps.sort((a, b) => b.date.localeCompare(a.date))
  }

  return NextResponse.json({
    site: 'yoursite.com',
    fetchedAt: new Date().toISOString(),
    version: 1,
    modules: {
      roadmaps,
    },
  })
}

Express / Node.js

routes/copper.js
const fs = require('fs')
const path = require('path')
const matter = require('gray-matter')

const API_KEY = process.env.COPPER_API_KEY

module.exports = function (app) {
  app.get('/api/copper', (req, res) => {
    // Check all supported auth methods
    const authHeader = req.headers.authorization
    const apiKeyHeader = req.headers['x-api-key']
    const queryKey = req.query.api_key

    const providedKey =
      (authHeader && authHeader.replace('Bearer ', '')) ||
      apiKeyHeader ||
      queryKey

    if (providedKey !== API_KEY) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    const dir = path.join(process.cwd(), 'roadmaps')
    const roadmaps = []
    if (fs.existsSync(dir)) {
      const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'))
      for (const file of files) {
        const raw = fs.readFileSync(path.join(dir, file), 'utf-8')
        const { data, content } = matter(raw)
        const slug = file.replace(/\.md$/, '')
        const trimmed = content.trim()

        roadmaps.push({
          slug,
          title: data.title || slug,
          status: data.status || 'proposed',
          date: data.date ? String(data.date) : slug.slice(0, 10),
          priority: data.priority,
          phase: data.phase,
          tags: data.tags,
          description: trimmed,
          summary: trimmed.slice(0, 500),
        })
      }
      roadmaps.sort((a, b) => b.date.localeCompare(a.date))
    }

    res.json({
      site: 'yoursite.com',
      fetchedAt: new Date().toISOString(),
      version: 1,
      modules: {
        roadmaps,
      },
    })
  })
}

Warning

Store your COPPER_API_KEY as an environment variable — never commit it to source control.

Troubleshooting

Sync shows an error

  • Click Sync now and check the error message in the settings panel.
  • Verify the endpoint URL is reachable from the public internet (Copper Analytics's servers must be able to reach it).
  • Double-check the auth mode matches how your endpoint expects the credential.
  • Ensure the credential value is correct and hasn't been rotated on the site side.

Sync returns 401 Unauthorized

  • Verify the credential in Copper Analytics matches the COPPER_API_KEY on your site.
  • If using Bearer mode, make sure your endpoint checks the Authorization header.
  • If using Custom header mode, check that the header name matches (default: x-api-key).
  • If using Query parameter mode, check that the param name matches (default: api_key).

Sync succeeds but shows 0 items

  • Visit your endpoint URL directly (with the API key) to confirm it returns valid JSON.
  • Ensure the response contains a modules object with a roadmaps array.
  • Check that your endpoint responds within 10 seconds (Copper Analytics applies a timeout).

Roadmap items appear but have missing fields

  • Each item must have at least title, status, and date. See the Frontmatter Fields reference.
  • Status must be one of: proposed, in-progress, completed, paused. Unknown values default to proposed.