REST API · v1

API Documentation

Programmatically manage your boards and pins via HTTPS. JSON in, JSON out. API access is part of the Enterprise plan.

Authentication

Generate an API key from Settings → API keys. Send it with every request as a Bearer token:

Authorization: Bearer ptp_live_xxxxxxxxxxxxxxxxxxxxxxxx

Each key has a set of scopes that limit what it can do:

  • boards:read — list your boards
  • boards:write — create / update boards
  • pins:read — read pins on your boards
  • pins:write — create / update / delete pins

Base URL

https://corkboard.social/api/public/v1

Boards

GET/api/public/v1/boards
scope: boards:read
List your boards. Newest first. Use ?limit=25 (max 100).
curl -H "Authorization: Bearer ptp_live_..." \
  https://corkboard.social/api/public/v1/boards?limit=25
{
  "boards": [
    {
      "id": "uuid", "title": "...", "slug": "...",
      "description": "...", "visibility": "public",
      "pins_count": 12, "links_count": 4,
      "views_count": 320,
      "created_at": "...", "updated_at": "..."
    }
  ]
}
POST/api/public/v1/boards
scope: boards:write
Create a new board.

Request body:

{
  "title": "My board",          // string, 1–120, required
  "description": "Optional",    // string, ≤500, optional
  "visibility": "public"        // "public" | "unlisted" | "private", default "public"
}
curl -X POST \
  -H "Authorization: Bearer ptp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"My board","visibility":"public"}' \
  https://corkboard.social/api/public/v1/boards

Response (201):

{
  "board": {
    "id": "uuid",
    "title": "My board",
    "slug": "my-board",
    "description": null,
    "visibility": "public",
    "created_at": "2026-06-21T10:00:00.000Z"
  }
}

Pins

GET/api/public/v1/pins?board_id=<uuid>
scope: pins:read
List pins on a board you own. Use &limit=50 (max 200).
curl -H "Authorization: Bearer ptp_live_..." \
  "https://corkboard.social/api/public/v1/pins?board_id=BOARD_UUID&limit=50"

Response (200):

{
  "pins": [
    {
      "id": "uuid",
      "board_id": "uuid",
      "slug": "pin-lxyz",
      "name": "My pin",
      "photo_url": "https://...",
      "short_desc": "Short text",
      "badges": ["new"],
      "pos_x": 120, "pos_y": 80, "rotation": 0,
      "pin_variant": "pin-01",
      "style": null,
      "likes_count": 0,
      "comments_count": 0,
      "created_at": "2026-06-21T10:00:00.000Z",
      "updated_at": "2026-06-21T10:00:00.000Z"
    }
  ]
}
POST/api/public/v1/pins
scope: pins:write
Create a new pin on one of your boards.

Request body:

{
  "board_id":   "BOARD_UUID",   // required
  "name":       "My pin",        // required, 1–120 chars
  "slug":       "my-pin",        // optional; auto-generated if omitted
  "photo_url":  "https://...",   // optional, string | null
  "short_desc": "Description",   // optional, ≤500 chars
  "badges":     ["new","hot"],   // optional, up to 12 strings
  "pos_x":      120,             // optional, number (default 0)
  "pos_y":      80,              // optional, number (default 0)
  "rotation":   0,               // optional, number (default 0)
  "pin_variant":"pin-01",        // optional, one of "pin-01" … "pin-10" (see Pin variants)
  "style":      { /* Pin style object — see Styles & widgets below */ }

}
curl -X POST \
  -H "Authorization: Bearer ptp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "board_id":"BOARD_UUID",
    "name":"My pin",
    "photo_url":"https://example.com/p.jpg",
    "badges":["new","featured"],
    "pos_x":120, "pos_y":80
  }' \
  https://corkboard.social/api/public/v1/pins

Response (201):

{
  "pin": {
    "id": "uuid",
    "board_id": "uuid",
    "slug": "my-pin",
    "name": "My pin",
    "photo_url": "https://example.com/p.jpg",
    "short_desc": null,
    "badges": ["new","featured"],
    "pos_x": 120, "pos_y": 80, "rotation": 0,
    "pin_variant": "pin-01",
    "style": null,
    "likes_count": 0,
    "comments_count": 0,
    "created_at": "2026-06-21T10:00:00.000Z",
    "updated_at": "2026-06-21T10:00:00.000Z"
  }
}
GET/api/public/v1/pins/:pinId
scope: pins:read
Fetch a single pin by id.
curl -H "Authorization: Bearer ptp_live_..." \
  https://corkboard.social/api/public/v1/pins/PIN_UUID

Response (200): { pin: Pin } — see schema below.

PATCH/api/public/v1/pins/:pinId
scope: pins:write
Partial update. Only send the fields you want to change; omit the rest.

Request body — any subset of:

{
  "name":        "Renamed",       // 1–120 chars
  "photo_url":   "https://...",   // string | null (send null to clear)
  "short_desc":  "New desc",      // string | null, ≤500 chars
  "badges":      ["sale"],        // replaces the array, up to 12
  "pos_x":       300,             // number
  "pos_y":       120,             // number
  "rotation":    15,              // number (degrees)
  "pin_variant": "pin-03",        // "pin-01" … "pin-10"
  "style":       { /* Pin style object, see below */ } // or null to clear

}
curl -X PATCH \
  -H "Authorization: Bearer ptp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Renamed","pos_x":300,"pos_y":120}' \
  https://corkboard.social/api/public/v1/pins/PIN_UUID

Response (200): { pin: Pin } — the full updated pin.

DELETE/api/public/v1/pins/:pinId
scope: pins:write
Delete a pin you own.
curl -X DELETE \
  -H "Authorization: Bearer ptp_live_..." \
  https://corkboard.social/api/public/v1/pins/PIN_UUID

Response (200):

{ "ok": true, "deleted": "PIN_UUID" }

What the API can not do

To keep boards fair, the public API is intentionally scoped to content you own. The following are not exposed via API — and we won't add them:

  • Likes / reactions on boards, pins, links or comments. Engagement counts can't be inflated through the API — they only change when real users interact through the app.
  • View counts (views_count, likes_count, comments_count) — read-only.
  • Follows, comments, votes, or notifications on other people's content.
  • Other users' boards, pins, or profile data — even if public. Use the regular website to browse those.
  • Admin / moderation actions (bans, reports, plan changes, audit logs).

If a request looks like an attempt to write to one of these, you'll get 403 or 404.

Pin variants, widgets & styles

Pin variants (pin_variant)
The visual shape/sticker of the pin. Pick one of these exact strings.
"pin-01" | "pin-02" | "pin-03" | "pin-04" | "pin-05"
"pin-06" | "pin-07" | "pin-08" | "pin-09" | "pin-10"

Default is "pin-01". Unknown values fall back to pin-01 when rendered.

style object — full schema
Controls paper texture, torn edges, attachment hardware, opacity, tint, the embedded widget, and explicit width/height. All fields optional; send only what you want to set.
{
  paper:      "plain" | "lined" | "grid" | "aged" | "kraft",     // default "plain"
                                                                  // "grid","aged","kraft" require Premium pin styles

  tornEdges:  { top: boolean, right: boolean,                     // default all false
                bottom: boolean, left: boolean },

  attachment: {
    type:  "pushpin" | "tape" | "clip" | "none",                  // default "pushpin"
                                                                  // "tape" & "clip" require Premium pin styles
    color: "#RRGGBB" | null                                       // hex, optional
  },

  opacity:    number,            // 0.9 – 1.0, default 1
  tint:       "#RRGGBB" | null,  // overlay tint, optional

  widget: {                      // optional embedded widget (see below)
    type: "image" | "text" | "youtube" | "spotify" | "audio"
        | "link"  | "quote" | "code"    | "map"     | "checklist" | "murder-mystery",
    data: { /* widget-specific payload */ }
  } | null,

  width:  number | null,         // px, optional explicit size
  height: number | null
}

Plan-gated fields (premium paper textures, tape/clip attachments) are accepted in the request but will only render to viewers on a plan that allows them. The board owner's plan is what counts, not the API caller's.

Widget types — style.widget
Each widget has its own data shape. Send only the fields listed for that type.
// image — extra image inside the pin
{ type: "image",     data: { url: string, alt?: string } }

// text — rich text block
{ type: "text",      data: { content: string, align?: "left"|"center"|"right" } }

// youtube — embedded video
{ type: "youtube",   data: { videoId: string, start?: number } }

// spotify — embedded track/playlist/album
{ type: "spotify",   data: { url: string } }

// audio — direct audio file
{ type: "audio",     data: { url: string, title?: string } }

// link — preview card
{ type: "link",      data: { url: string, title?: string, description?: string, image?: string } }

// quote — pull quote
{ type: "quote",     data: { text: string, author?: string } }

// code — code snippet
{ type: "code",      data: { code: string, language?: string } }

// map — pinned location
{ type: "map",       data: { lat: number, lng: number, label?: string, zoom?: number } }

// checklist — interactive checklist
{ type: "checklist", data: { items: { text: string, checked?: boolean }[] } }

// murder-mystery — Katil Kim? multiplayer lobby launcher
// maxPlayers: integer 4–12 (default 8), discussionSeconds: integer 30–300 (default 90)
{ type: "murder-mystery", data: { maxPlayers: 8, discussionSeconds: 90 } }
// Game sessions are created from the saved card. Do not put session tokens or roles in pin data.

Some widget types (audio, code, map, checklist, embeds) are considered advanced widgets and require a plan with can_advanced_widgets. Unauthorized widgets are rejected with 400.

Example: create a pin with a YouTube widget
curl -X POST \
  -H "Authorization: Bearer ptp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "board_id":"BOARD_UUID",
    "name":"Demo video",
    "pos_x":200, "pos_y":160, "rotation":-3,
    "pin_variant":"pin-04",
    "style": {
      "paper":"lined",
      "attachment":{"type":"tape","color":"#facc15"},
      "tornEdges":{"top":false,"right":false,"bottom":true,"left":false},
      "widget":{
        "type":"youtube",
        "data":{"videoId":"dQw4w9WgXcQ","start":0}
      }
    }
  }' \
  https://corkboard.social/api/public/v1/pins

Schemas

Board
{
  id:           string  // uuid
  title:        string
  slug:         string
  description:  string | null
  visibility:   "public" | "unlisted" | "private"
  pins_count:   number
  links_count:  number
  views_count:  number
  created_at:   string  // ISO 8601
  updated_at:   string  // ISO 8601
}
Pin
{
  id:             string  // uuid
  board_id:       string  // uuid
  slug:           string
  name:           string
  photo_url:      string | null
  short_desc:     string | null
  badges:         string[]
  pos_x:          number
  pos_y:          number
  rotation:       number  // degrees
  pin_variant:    string  // "pin-01" … "pin-10"
  style:          PinStyle | null  // see Styles & widgets section
  likes_count:    number
  comments_count: number
  created_at:     string  // ISO 8601
  updated_at:     string  // ISO 8601
}
Error
{ "error": "Human-readable message" }

Errors & limits

Errors come back as { error: string } with these status codes:

  • 400 — invalid input
  • 401 — missing or invalid Bearer token
  • 403 — token missing required scope
  • 404 — resource not found, or you don't own it
  • 500 — unexpected server error

A key only sees resources owned by the user who created it. Revoked keys stop working immediately.