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.
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
}
}
Success — 200 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
}
}
Success — 200 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"
}
}
Success — 200 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
projectOmgJobNumberdoesn't resolve, you'll get404 { 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
}
Recommended integration flow
For a typical upstream system sending many briefs/jobs/deliverables per day:
- Create a brief (
POST /api/webhooks/briefswithexternalId). A producer triages it in the/briefsinbox. - 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. - If you create a Project directly (bypassing the brief intake):
POST /api/webhooks/omgwithjob.numberset. The Project is created and all its pipeline stages are scaffolded. - For each deliverable under that project:
POST /api/webhooks/deliverablesreferencingprojectOmgJobNumber. 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:
- Dave Porter — daveporter@oliver.agency
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}.