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.
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
gray-matter for parsing Markdown frontmatter. Install it with pnpm add gray-matter or npm install gray-matter.Setup
- Create a
GET /api/copperendpoint on your site that returns data in the Copper Data API format (see code examples below). - Generate an API key or token for Copper Analytics to use.
- Open your site's Settings in the Copper Analytics dashboard and enable Authenticated Pull.
- Enter the endpoint URL and your API key. Choose the auth mode (Bearer, Custom header, or Query parameter).
- Click Save settings, then Sync now to verify the connection.
Tip
Auth modes
Copper Analytics supports three ways to send the read-only credential to your endpoint:
| Mode | How Copper Analytics sends the credential | When to use |
|---|---|---|
| Bearer token | Authorization: Bearer <credential> | Standard REST APIs (default) |
| Custom header | x-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)
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
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
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_KEYon your site. - If using Bearer mode, make sure your endpoint checks the
Authorizationheader. - 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
modulesobject with aroadmapsarray. - 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, anddate. See the Frontmatter Fields reference. - Status must be one of:
proposed,in-progress,completed,paused. Unknown values default toproposed.