dow-prod-tracker/INTEGRATION.md
DJP 03cd99b56c Add INTEGRATION.md — handover spec for upstream teams
Single-document spec for teams pushing Briefs, Projects, and
Deliverables. Each of the three gets its own section with:
- Webhook URL + signature header + HMAC signing example
  (bash/python/node)
- REST equivalent (auth'd user path) for human-operator cases
- Annotated JSON body with every field, which are required, and
  what they map to on our side
- Idempotency behaviour (externalId, omgJobNumber, project+name
  natural keys)
- Error responses with the exact JSON shape they'll see

The doc is pitched at a dev at an upstream system, not a producer,
so it's spec-first: curl-ready examples, enum values spelled out,
header names called out in a table.
2026-04-21 13:29:44 -04:00

14 KiB

Dow Jones Studio Tracker — Upstream Integration Guide

For teams pushing data into the Studio Tracker. You can create Briefs, Projects, and Deliverables either via signed webhooks (push — recommended) or via REST calls made by an authenticated user account.

This document is the spec you can work against directly; a worked example curl + signing snippet is included for each endpoint.


Base URL

https://optical-dev.oliver.solutions/dow-prod-tracker

Every URL in this doc is relative to that base.


At-a-glance

Resource Push (webhook) Pull-equivalent REST
Brief POST /api/webhooks/briefs POST /api/briefs
Project POST /api/webhooks/omg POST /api/projects
Deliverable POST /api/webhooks/deliverables POST /api/projects/:projectId/deliverables

Recommendation: use the webhooks. They're designed for machine-to- machine integration with HMAC signing, they're idempotent on replay, and they don't require your system to manage user accounts + session cookies. REST endpoints are provided for cases where a human operator is making the call (e.g., internal tooling, back-office corrections).


Webhook authentication (all three)

All three webhooks use the same HMAC-SHA256 signing scheme. Only the secret and the signature header name differ per endpoint.

Resource Secret env var Signature header
Brief BRIEF_WEBHOOK_SECRET X-Brief-Signature
Project OMG_WEBHOOK_SECRET X-OMG-Signature
Deliverable DELIVERABLE_WEBHOOK_SECRET X-Deliverable-Signature

Signature format

<Header>: sha256=<hex_digest>

where hex_digest is the lowercase hex of HMAC-SHA256(secret, request_body_bytes).

Sign the raw request body, byte-for-byte. Do NOT re-serialise the JSON before signing — any whitespace or key-order change will break the signature.

Signing examples

Bash

BODY='{"title":"New brief","priority":"HIGH"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$BRIEF_WEBHOOK_SECRET" | awk '{print $2}')

curl -X POST https://optical-dev.oliver.solutions/dow-prod-tracker/api/webhooks/briefs \
  -H "Content-Type: application/json" \
  -H "X-Brief-Signature: sha256=$SIG" \
  -d "$BODY"

Python

import hmac, hashlib, json, requests

body = json.dumps({"title": "New brief", "priority": "HIGH"}, separators=(",", ":"))
sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()

requests.post(
    "https://optical-dev.oliver.solutions/dow-prod-tracker/api/webhooks/briefs",
    headers={"Content-Type": "application/json", "X-Brief-Signature": f"sha256={sig}"},
    data=body,   # pass the SAME string we signed; don't re-serialise
)

Node.js

import crypto from "node:crypto";

const body = JSON.stringify({ title: "New brief", priority: "HIGH" });
const sig = crypto.createHmac("sha256", secret).update(body).digest("hex");

await fetch("https://optical-dev.oliver.solutions/dow-prod-tracker/api/webhooks/briefs", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Brief-Signature": `sha256=${sig}` },
  body,
});

Error responses

Status Meaning
401 { error: "missing signature header" } No signature sent
401 { error: "signature mismatch" } Signature doesn't match body
401 { error: "BRIEF_WEBHOOK_SECRET not configured" } Our end has no secret — contact us
400 { error: "invalid JSON body" } Body isn't valid JSON
400 { error: "invalid payload", issues: [...] } Valid JSON, wrong shape — see issues
404 { error: "parent project not found" } (deliverables webhook) Project doesn't exist
500 { error: "upsert failed", detail: "..." } Our side broke — include the detail when reporting

1. Briefs

A brief is an incoming request that has not yet been accepted as a project. Briefs land in the /briefs inbox and a producer decides whether to promote them. The original brief stays as audit trail after promotion, with convertedProjectId pointing at the new project.

Webhook — POST /api/webhooks/briefs

Auth: X-Brief-Signature: sha256=<hex> (HMAC of raw body with BRIEF_WEBHOOK_SECRET).

Body

{
  // Optional: your stable ID for this brief. Providing it enables
  // idempotent retries — the same externalId always updates the same
  // Brief row (unique per organisation). Omit it and each call creates
  // a new brief.
  "externalId": "INTAKE-2026-00042",

  // Required
  "title": "New homepage takeover for Q2 launch",

  // Optional — Markdown is fine; rendered on the /briefs page
  "description": "Need 3 variants for A/B test, dark mode required.",

  // Optional
  "requestorName": "Alex Smith",
  "requestorEmail": "alex.smith@dowjones.com",

  "priority": "HIGH",                     // LOW | MEDIUM | HIGH | URGENT
  "requestedDueDate": "2026-05-15T17:00:00Z",  // ISO-8601

  // Optional — the Dow-side team this brief is for. Must match a
  // ClientTeam slug (currently: brand, events, b2b, content,
  // briefing-team, performance). Unknown slugs are silently ignored
  // (the brief is still created, just with no team assigned).
  "clientTeamSlug": "events",

  // Optional — source tag ("mkto", "jira", "slack-bot"…). We prefix
  // with "webhook:" so the brief's source field will read
  // "webhook:mkto".
  "system": "mkto",

  // Optional — any extra fields you want preserved, passed through
  // verbatim into Brief.rawPayload. Useful when your system has data
  // we don't map to a first-class field yet.
  "rawPayload": {
    "campaignId": "c-8829",
    "internalBudget": 12500
  }
}

Success200 OK

{
  "ok": true,
  "briefId": "clxy12abc...",
  "status": "PENDING"
}

Idempotency: when externalId is set, a second call with the same externalId updates the existing brief rather than inserting a new one. If you're syncing from a source-of-truth upstream system, always send externalId so retries are safe.

REST — POST /api/briefs

Same JSON body as the webhook, but authenticated via the signed-in user (session cookie). The producer hitting this endpoint needs the PROJECT_CREATE permission (ADMIN or PRODUCER role).

Use this when a human operator is creating a brief from an internal tool that already has a user session; use the webhook otherwise.


2. Projects

A project is an accepted piece of work with deliverables attached. The canonical key is omgJobNumber (the OMG tracking number) — all intake paths (XLSX upload, webhook, REST) upsert on it.

Webhook — POST /api/webhooks/omg

Auth: X-OMG-Signature: sha256=<hex> (HMAC of raw body with OMG_WEBHOOK_SECRET).

Body

{
  // Optional metadata
  "event": "job.updated",        // job.created | job.updated | job.assigned | job.status_changed | job.completed
  "timestamp": "2026-04-22T14:00:00Z",

  // Required. Canonical project key.
  "job": {
    "number": "OMG-12345",           // REQUIRED. Project.omgJobNumber (unique)
    "name": "Q2 Launch Homepage Takeover",  // Human-readable project name

    // Optional
    "client": "Dow Jones",
    "team": "Events",                // Mapped to ClientTeam slug (case-insensitive)
    "category": "Display",           // Dow Project Category — stored on Project.businessUnit
    "priority": "high",              // "priority" | "high" | "medium" | "low"
    "status": "In Production",       // Free-form — mapped via internal STATUS_MAP:
                                     //   "brief in review" → PIPELINE
                                     //   "amends" | "in production" | "client review" → ACTIVE
                                     //   "on hold" → ON_HOLD
                                     //   Anything unmapped defaults to ACTIVE
    "outputs": 3,                    // # of deliverables this project generates
    "notes": "Priority — board sign-off required.",

    // Optional — first email here becomes Project.requestor
    "assignees": [
      { "email": "alex.smith@dowjones.com", "role": "Project Lead" },
      { "email": "bob.jones@oliver.agency" }
    ],

    // Optional — ISO-8601
    "dates": {
      "accepted": "2026-04-20T09:00:00Z",   // Project.startDate
      "externalDeadline": "2026-05-15T17:00:00Z"  // Project.dueDate
    }
  },

  // Optional — anything extra lands on Project.customFields JSON
  "raw": {
    "costCenter": "CC-4821",
    "legalReviewRequired": true
  }
}

Success200 OK

{
  "ok": true,
  "projectId": "clyz34def...",
  "action": "created"     // or "updated"
}

Idempotency: project is upserted on job.number (→ Project.omgJobNumber). Replaying the same payload is safe — second call updates the same project, no duplicates.

REST — POST /api/projects

Auth via session cookie (requires PROJECT_CREATE). Full field list in API.md — essentials:

{
  "projectCode": "DJ-2026-0042",   // REQUIRED, unique
  "name": "Q2 Launch Homepage",    // REQUIRED
  "status": "ACTIVE",              // REQUIRED: PIPELINE | ACTIVE | ON_HOLD | COMPLETED | CANCELED | ARCHIVED
  "priority": "HIGH",              // REQUIRED: LOW | MEDIUM | HIGH | URGENT
  "omgJobNumber": "OMG-12345",     // Optional but strongly recommended — intake key
  "clientTeamId": "cm123teamid",   // Optional — ClientTeam.id (not slug)
  "businessUnit": "Display",       // Dow Project Category
  "description": "...",
  "startDate": "2026-04-20",
  "dueDate": "2026-05-15",
  "requestor": "alex.smith@dowjones.com"
}

3. Deliverables

A deliverable is a single unit of work attached to a project — e.g., "Homepage desktop hero variant A". Stages (Pipeline → New → ... → Completed) are auto-created the first time a deliverable is inserted.

Webhook — POST /api/webhooks/deliverables

Auth: X-Deliverable-Signature: sha256=<hex> (HMAC of raw body with DELIVERABLE_WEBHOOK_SECRET).

Body

{
  "event": "deliverable.updated",     // Optional
  "timestamp": "2026-04-22T14:30:00Z", // Optional

  "deliverable": {
    // REQUIRED. Links the deliverable to an existing Project by its
    // OMG job number. If no project with that number exists, the call
    // returns 404. Create the project first (via the OMG webhook).
    "projectOmgJobNumber": "OMG-12345",

    // REQUIRED. Natural key within the project — same (project, name)
    // on a retry updates the existing deliverable rather than inserting.
    "name": "Homepage desktop hero — variant A",

    // Optional
    "priority": "HIGH",                // LOW | MEDIUM | HIGH | URGENT
    "status": "IN_PROGRESS",           // NOT_STARTED | IN_PROGRESS | IN_REVIEW | APPROVED | ON_HOLD
    "dueDate": "2026-05-10T17:00:00Z", // ISO-8601
    "notes": "Must match Q2 launch brand guidelines v3.",
    "cmfSku": "CMF-8829-A",            // Internal SKU if you track one
    "assetCount": 5                    // Asset files expected for this deliverable
  },

  // Optional — lands on Deliverable.customFields JSON (pass-through)
  "raw": {
    "figmaFrameId": "fig-12:345",
    "copywriter": "patrick.mccarthy@oliver.agency"
  }
}

Success200 OK

{
  "ok": true,
  "deliverableId": "clab56ghi...",
  "action": "created"     // or "updated"
}

Idempotency: keyed on (projectId, deliverable.name) within a project. Replays with the same name update the existing deliverable.

Parent project must exist first. If projectOmgJobNumber doesn't resolve, you'll get 404 { error: "parent project not found", hint: "Project must be created via POST /api/webhooks/omg first." }. The typical integration flow is: OMG sends the project webhook on job creation, then fires deliverable webhooks for each asset as they're spun up.

REST — POST /api/projects/:projectId/deliverables

Auth via session cookie (requires DELIVERABLE_CREATE). Note the URL shape: deliverables are nested under a specific project by its internal id (not omgJobNumber).

{
  "name": "Homepage desktop hero — variant A",  // REQUIRED
  "priority": "HIGH",                            // REQUIRED
  "dueDate": "2026-05-10",
  "notes": "...",
  "cmfSku": "CMF-8829-A",
  "assetCount": 5
}

For a typical upstream system sending many briefs/jobs/deliverables per day:

  1. Create a brief (POST /api/webhooks/briefs with externalId). A producer triages it in the /briefs inbox.
  2. When the brief is accepted, the producer promotes it to a project inside the UI — we create the Project and link back to the Brief via convertedProjectId. You don't need to do anything here.
  3. If you create a Project directly (bypassing the brief intake): POST /api/webhooks/omg with job.number set. The Project is created and all its pipeline stages are scaffolded.
  4. For each deliverable under that project: POST /api/webhooks/deliverables referencing projectOmgJobNumber. On first call the deliverable is created and its 11 pipeline stages are auto-populated; subsequent calls with the same (project, name) update the existing deliverable.

Retries on any of the three are safe as long as you send a stable external key (externalId / job.number / (project, name)).


Requesting secrets

Each webhook secret is generated per environment and shared out-of-band (not committed, not in email). To get yours, contact:

Include:

  • Which environment you're integrating with (currently only optical-dev.oliver.solutions; prod URL TBD)
  • Which of the three webhooks you need access to (briefs / projects / deliverables — usually all three)
  • A rough idea of your expected call volume (helps us size alerting)

Testing against the live endpoint

Every webhook has a dev-mode bypass for local/stub testing. We will NOT enable it against shared environments, but if you're building a client locally you can start your own instance with BRIEF_WEBHOOK_ALLOW_INSECURE=true (or the matching var) — then omit the signature header for unsigned POSTs. Never enable this in shared environments.

For integration testing against our shared dev environment, use the real HMAC and a low-volume test batch. Every call is logged with the externalId/job number, so we can tell genuine replays from duplicates.


Changelog

  • 2026-04-22 — initial document. Three webhooks live on /dow-prod-tracker/api/webhooks/{omg,deliverables,briefs}.