Getting started
QuickstartAuthenticationConceptsAPIs
Delivery APIIngest webhookOutbound webhooksErrorsLibraries
TypeScript SDKField mapperQuickstart
Two credentials, doing opposite jobs. A delivery key reads content out. A webhook url lets a partner push content in. Both are shown once.
v1Stable
1. Issue a delivery key
On the Delivery keys screen, pick the site the key should read. A key is bound to that site when it is issued, which is why no site appears in the url.
ck_a41f9c2e77b1.8f2c4e1d9a03b7c6e5f4a2d1c0b9a8e7.k3Rd…
2. Read posts
import { createClient } from '@coppercms/sdk' const copper = createClient({ apiKey: process.env.COPPER_KEY }) const { data, nextCursor } = await copper.listPosts({ limit: 20 }) const post = await copper.getPost('a-new-office-in-lisbon')
3. Or call it directly
GET https://api.coppercms.com/v1/wire/posts?limit=20 authorization: Bearer ck_a41f… 200 OK cache-control: private, max-age=60
4. Take a feed from a partner
Create a source and Copper CMS issues one url. Hand it over as it is. There is nothing else for the partner to configure.
POST https://api.coppercms.com/v1/hook/8f2c…c1a9.k3Rd… content-type: application/json { "title": "…", "body": "…" }
Responses
| Code | Meaning |
|---|---|
| 200 | The request succeeded. Listing and single-post reads both answer 200, and so does the ingest webhook, on a new article and on a re-send alike. |
| 400 | The field map produced no title, the body was not valid JSON, or a cursor was not one this endpoint issued. |
| 401 | The credential is unknown, revoked, tampered with or malformed. All four answer the same way. |
| 402 | The account is over a plan limit, or it is suspended. |
| 403 | The credential is real, but the source is not active or the key does not reach that site. |
| 404 | No post with that slug is published to the site the key names. |
| 413 | The payload is larger than the ingest webhook accepts. |
Verifying an outbound webhook
The signature covers the timestamp and the raw body joined by a dot. Signing the body alone would leave the replay window decorative.
const signed = `${timestamp}.${rawBody}` const expected = createHmac('sha256', secret).update(signed).digest('hex') // compare in constant time, and reject a timestamp // more than five minutes old
