Skip to Content
APIDM Portal API

DM Portal API

API for internal digital marketing reviewers. Unlike the Dashboard API, DM endpoints are cross-tenant — a reviewer sees activities and approvals across all tenants assigned to them.

Related: API Overview | Auth | Dashboard API


Base Prefix

/dm/v1

Auth: Bearer token with role: reviewer | admin and appAccess: dm-portal

Tenant filtering: Most list endpoints accept an optional tenantId query param. Omitting it returns data across all tenants the reviewer is assigned to. Reviewers cannot access tenants they are not assigned to — attempting to do so returns 403.


Mission Control

GET /overview

Real-time summary across all assigned tenants — the P1 Mission Control screen data.

Response 200:

{ tenants: Array<{ tenantId: string; tenantName: string; pendingApprovals: number; highRiskApprovals: number; activeAgents: number; activitiesInProgress: number; activitiesBlocked: number; oldestPendingApprovalAgeH: number; // hours since oldest pending approval was created }>; totals: { pendingApprovals: number; highRiskApprovals: number; activitiesInProgress: number; activitiesBlocked: number; agentsInError: number; }; }

Activities

GET /activities

List activities across assigned tenants.

Query params:

{ tenantId?: string; filter?: { status?: string; agentRole?: string; type?: string; }; limit?: number; cursor?: string; }

Response 200: Paginated ActivitySummary[] with tenantId and tenantName fields added


GET /activities/:activityId

Full activity detail. Reviewer sees the same fields as the dashboard view plus:

{ // ...same as dashboard ActivityDetail... tenantId: string; tenantName: string; canIntervene: boolean; // false if activity is already completed/cancelled }

GET /activities/:activityId/stream

SSE: live agent output stream. Same event protocol as the Dashboard API stream endpoint.


POST /activities/:activityId/pause

Pause a running activity. The current agent run is allowed to complete its current turn, then no new runs are enqueued until the activity is resumed.

Request:

{ reason?: string; }

Response 202:

{ activityId: string; status: 'pausing'; }

Errors:

  • 422 UNPROCESSABLE — activity is not in_progress

POST /activities/:activityId/resume

Resume a paused activity. Re-enqueues the activity with wakeReason: 'unblocked'.

Request:

{ note?: string; }

Response 202:

{ activityId: string; runId: string; }

POST /activities/:activityId/retry

Force-retry a failed activity. Creates a new run.

Request: Empty body

Response 202:

{ activityId: string; runId: string; }

POST /activities/:activityId/cancel

Cancel an activity permanently. Sets status to failed, reason cancelled_by_reviewer. No further retries.

Request:

{ reason: string; }

Response 200:

{ activityId: string; status: 'failed'; }

POST /activities/:activityId/reassign

Reassign an activity to a different agent role or a specific human. The current activity is marked reassigned; a new activity is created for the target.

Request:

{ assigneeType: 'agent' | 'human'; assigneeId: string; // agent role (e.g. 'copywriter') or human user ref_id reason: string; note?: string; }

Response 201:

{ originalActivityId: string; newActivityId: string; }

POST /activities/:activityId/comments

Add a comment or intervention note. Same shape as the Dashboard API.


GET /activities/:activityId/comments

List comments. Same shape as the Dashboard API.


Approvals

GET /approvals

Cross-tenant approval queue, sorted by risk level (high first) then age (oldest first) by default.

Query params:

{ tenantId?: string; filter?: { status?: 'pending' | 'approved' | 'rejected' | 'expired'; type?: string; riskLevel?: 'low' | 'medium' | 'high'; }; sortBy?: 'risk_then_age' | 'age' | 'created_on'; // default 'risk_then_age' limit?: number; cursor?: string; }

Response 200: Paginated list (same shape as dashboard approval list with tenantId/tenantName added)


GET /approvals/:approvalId

Approval detail with full linked activity outputs for side-by-side review.


POST /approvals/:approvalId/resolve

Approve or reject with optional note. Same request/response shape as the dashboard endpoint.


POST /approvals/:approvalId/send-to-client

Forward the approval to the tenant admin for their decision. Creates a new content_review approval linked to the original, notifies the tenant admin.

Request:

{ note?: string; // message to the client urgency?: 'normal' | 'urgent'; }

Response 201:

{ forwardedApprovalId: string; // new approval created for client notificationSent: boolean; }

POST /approvals/:approvalId/escalate

Escalate an approval to a higher-risk level and reset the expiry window. Use when an approval is aging without response or carries higher actual risk than originally assessed.

Request:

{ newRiskLevel: 'medium' | 'high'; reason: string; }

Response 200:

{ approvalId: string; riskLevel: string; newExpiresAt: string; }

POST /approvals/bulk/resolve

Bulk-resolve multiple approvals with the same decision.

Request:

{ approvalIds: string[]; // max 50 resolution: 'approved' | 'rejected'; note?: string; // applies to all; required for 'rejected' }

Response 200:

{ resolved: number; failed: Array<{ approvalId: string; reason: string }>; reEnqueuedActivities: number; }

Blog Posts

DM reviewers are the first human gate in the blog post approval chain. After a reviewer approves, the post moves to the client for their own approval.

Status flow: dm_review(DM approves)client_review(client approves)client_approved

Rejection flow: DM rejects → blog-writer re-runs with reviewer feedback → new version back to dm_review

GET /blog

List blog posts for a tenant in any status.

Query params:

{ tenantId: string; // required status?: 'dm_review' | 'client_review' | 'client_approved' | 'rejected' | 'published'; page?: number; // default 1 limit?: number; // default 20, max 100 q?: string; // title search }

Response 200:

{ data: Array<{ id: string; title: string; slug: string; status: string; version: number; wordCount: number; metaDescription: string | null; dmApprovedBy: string | null; dmApprovedAt: string | null; clientApprovedBy: string | null; clientApprovedAt: string | null; dmRejectionNote: string | null; createdAt: string; updatedAt: string; activity: { id: string; label: string; deliverableType: string } | null; }>; total: number; page: number; limit: number; pages: number; }

GET /blog/:id

Single blog post with full Markdown content.

Response 200: Same as list item + content: string field.

Response 404: Post not found or not in the reviewer’s accessible tenants.


POST /blog/:id/dm-approve

DM reviewer approves the post → moves to client_review. Client is notified to review.

Request: (empty body)

Response 200:

{ ok: true; status: "client_review" }

Errors:

  • 400 BAD_REQUEST — post is not in dm_review status
  • 403 FORBIDDEN — reviewer does not have access to this tenant

POST /blog/:id/dm-reject

DM reviewer rejects the post with feedback. The blog-writer agent is re-queued with the feedback to generate a new version.

Request:

{ feedback: string; } // required — reason and specific revision instructions

Response 200:

{ ok: true; status: "rejected"; jobId: string }

The agent re-runs with wakeReason: "rejection" and reviewerFeedback injected into the prompt. The BlogPost version is incremented on the next generation.


Agents

GET /agents

Status of all agents across assigned tenants.

Query params:

{ tenantId?: string; filter?: { status?: 'active' | 'running' | 'error' | 'paused' | 'terminated'; role?: string; }; }

Response 200:

{ data: Array<{ tenantId: string; tenantName: string; role: string; status: string; runningCount: number; model: string; lastErrorAt?: string; lastErrorMessage?: string; queueDepth: number; // jobs waiting in BullMQ for this agent }>; }

GET /agents/:tenantId/:agentRole

Detailed agent status for a specific agent on a specific tenant.

Response 200: Full agent config + recent runs + current queue depth


POST /agents/:tenantId/:agentRole/pause

Pause an agent for a specific tenant. All new jobs are held; running jobs complete normally.

Request:

{ reason?: string; }

Response 200: Updated agent status


POST /agents/:tenantId/:agentRole/resume

Resume a paused agent.

Response 200: Updated agent status


POST /agents/:tenantId/:agentRole/clear-error

Clear the error status on an agent after investigating, restoring it to active.

Request:

{ note?: string; }

Response 200: Updated agent status


Reports

GET /reports

List reports for all assigned tenants.

Query params:

{ tenantId?: string; filter?: { status?: 'draft' | 'ready' | 'delivered'; type?: 'monthly_performance' | 'campaign_summary' | 'custom'; }; limit?: number; cursor?: string; }

Response 200: Paginated report list


POST /reports

Generate a new performance report for a tenant.

Request:

{ tenantId: string; type: 'monthly_performance' | 'campaign_summary' | 'custom'; periodStart: string; // ISO 8601 date periodEnd: string; campaignIds?: string[]; // for campaign_summary title?: string; notes?: string; }

Response 202:

{ reportId: string; status: 'generating'; }

Report generation is async. Poll GET /reports/:reportId for status.


GET /reports/:reportId

Report detail and content once generated.

Response 200:

{ id: string; tenantId: string; tenantName: string; type: string; status: 'generating' | 'ready' | 'delivered'; title: string; pdfUrl?: string; // pre-signed URL (valid 1h) when status is ready deliveredAt?: string; deliveredTo?: string[]; // email addresses createdBy: string; createdOn: string; }

POST /reports/:reportId/deliver

Email the report PDF to the client tenant admins and/or specified addresses.

Request:

{ to: string[]; // email addresses; tenant admins always included subject?: string; message?: string; }

Response 200:

{ delivered: true; sentTo: string[]; }

Team

GET /team

Cross-tenant principal list across assigned tenants.

Query params:

{ tenantId?: string; filter?: { type?: 'human' | 'agent'; role?: string }; }

Response 200: List with tenantId/tenantName added per principal


POST /team/:principalId/reassign-activities

Reassign all open activities from one principal to another (e.g. when a reviewer goes on leave).

Request:

{ toId: string; // target principal ref_id reason: string; }

Response 200:

{ reassigned: number; }

Events (SSE)

GET /events/stream

SSE stream for DM-relevant events across all assigned tenants.

Protocol: text/event-stream

Events:

event: approval_created data: {"approvalId":"01ARZ...","tenantId":"...","tenantName":"Acme Corp","riskLevel":"high"} event: approval_expiring data: {"approvalId":"01ARZ...","tenantId":"...","hoursLeft":3} event: agent_error data: {"tenantId":"...","agentRole":"copywriter","error":"All retries exhausted","activityId":"..."} event: activity_blocked data: {"activityId":"01ARZ...","tenantId":"...","blockerType":"channel_auth_expired"}

© 2026 Leadmetrics — Internal use only