Manage Content
Draft, schedule and publish LinkedIn content — the consolidated content writer.
Objective content (kind='objective_content', id = objective UUID): First fetch type='marketing' subtype='objective_content' id=<objective>.
-
prepare: internally author a sourced LinkedIn text draft. payload = {author_user_id, follow_up_owner_user_id, direction?, publication_notes?}. Uses current objective/offer/shared voice, never private customer stories. Returns typed content, rationale, exact citations and context_fingerprint. It sends nothing and does not overwrite saved edits.
-
draft: save internal or harness copy. payload = returned preparation fields plus client_request_id (reuse on retry). To revise, add post_id and expected_revision; revision cancels prior approval and schedule. Harnesses can author from the fetched context with exact offer/publication_notes quotes.
-
decide: after the person reviews full copy, author, sources and timing, payload={post_id, expected_revision, expected_version: action_version, decision: publish|schedule|reject, scheduled_at?}. Scheduling requires a future ISO datetime. Reuse exact payload after an uncertain response; an unknown provider outcome NEVER authorizes a replacement publication.
-
handoff: assign reviewed captured interest. payload={post_id, engagement_id, owner_user_id, reason, next_check_at}. Reuses existing person/objective work. It sends nothing. Publication creates owned response review; neither engagement nor follow-up completion establishes pipeline or revenue.
-
response_prepare: assess an assigned responder and prepare a cited email or hold. payload={action_id, expected_version}. First read fetch type=marketing subtype=content_response id=<objective> filters={action_id}. This purchases one model response; it does not save, approve or send.
-
response_draft: save internal/harness preparation or edits. payload contains action_id, expected_version, client_request_id, context_fingerprint, decision (contact|hold), rationale, citations [{source_key,quote}], missing_facts, subject and body (null for hold). Reuse the complete request on retry. Contact uses manage_outreach objective_opportunity_send with the saved exact subject/body and returned version, after separate human approval.
Content actions (default kind; id = post UUID unless noted):
- draft: create a post draft (no id; payload = {content, topic_tags?, scheduled_at?, author_user_id?, author_identity?, external_link?, attachment_creative_id?}); bulk via items.
- edit: update a draft's content/tags/identity/link/attachment (payload = the fields to change: content?, topic_tags?, author_identity?, external_link? — empty-string external_link clears it; attachment_creative_id? — explicit null DETACHES, absent key leaves it untouched).
- schedule: set the publish time (payload = {scheduled_at}). A row-persisted attachment publishes with the scheduled post.
- publish: publish the post now (no payload; the retired publish-time creative_id param is rejected with the pointer to edit-attach).
- delete: remove a draft from the calendar (drafts only — published posts are not deletable here).
- draft_post: never generates prose — backend generation is removed (VRU-676, permanently). Always returns author-in-harness guidance: write the post yourself and submit via draft, then schedule/publish (no id; payload = {topic?, content_type?, author_user_id?}).
Attachments (VRU-726) — document (PDF carousel), image, or video, ONE contract, persisted on the row (schedulable + reviewable). Golden path for a PDF, entirely via MCP:
- manage_content kind='ad' action='store_creative' payload={filename:'my-deck.pdf', size_bytes:<n>, content_type:'application/pdf'} → returns upload_url
- curl --fail-with-body -T my-deck.pdf '<upload_url>'
- store_creative again with payload={creative_id:'<id>'} → validates (small assets can skip 1-3: payload={asset_base64:'<b64>'})
- manage_content action='draft' payload={content:'<caption>', attachment_creative_id:'<id>'}
- get_content_review post_ids=['<post_id>'] → OPEN attachment_url and review the actual file (it publishes under the author's identity)
- manage_content action='publish' id='<post_id>' The stored filename becomes the rendered LinkedIn document title (override at store time via filename). Rules: one attachment per post; not on repost_commentary; caption text required; stored assets are public-at-upload (public bucket, unguessable URL). Media transfers inline at publish — expect tens of seconds for large files. Attachment failures revert the post to draft with a structured {code, message, fix} 422 and never burn post budget on a failed fetch; an ambiguous provider timeout stays terminal failed (publish_outcome_unknown) — check LinkedIn before retrying, a blind retry can double-post.
Identities (VRU-659): author_identity is 'member' (the author's personal profile — default) or 'organization' (the Company Page). Organization publish requires a resolvable Company Page on the author's LinkedIn account: exactly one Page on the account, or a selected Page (manage_content kind='ad' action='set_page'); the member account still authenticates and must hold admin rights on the Page.
external_link (http/https, <=2000 chars) renders as the LinkedIn preview card; UTM params are stamped when the link is SAVED (missing utm_* added, existing ones never clobbered), so the approved draft is exactly what publishes. Combines fine with an attachment.
Bulk: edit/schedule/publish/delete take id arrays (server-side loop, per-item status — partial failure reported per item, never silent; max 100). Payloads are validated by the underlying route's schema.
Prose gate: draft/edit lint the content and schedule/publish re-lint it server-side; blocked calls return error_code='prose_gate_blocked' with structured failures[].fix. payload accepts override_reason (taste override — proceeds despite block failures, logged to the corpus; honored only for owner/operator roles on the tenant, otherwise ignored and the gate blocks normally) and client_rules_version (responses flag rules_changed when the server's rules differ). Pre-check drafts cheaply with check_prose.
Ad actions (kind='ad'; id = ad-campaign UUID): reject, pause — payload carries the action's fields (e.g. {rejection_reason} for reject). This tool never starts, resumes or raises paid spend: a workspace owner or operator approves a drafted boost in the Vruum app's Work list, or approves, resumes or re-budgets it with the Vruum CLI (vruum ad approve | resume | budget). boost (no id — there's no campaign yet): prepare a paid LinkedIn campaign as a DRAFT; it pushes nothing and spends nothing, and it appears in the Work list for an owner to approve or decline. payload carries {content_post_id (sponsor an EXISTING published post) XOR creative_id (a stored VIDEO creative → Direct Sponsored Content, METRICS-ONLY — no engager bridge), vehicle?, objective?, budget:{daily_budget_cents/total_budget_cents}, audience (see below), duration_days?, integration_id?}.
Boost audience — exactly one of:
- {facets}: the PRIMARY form. LinkedIn-native firmographic targeting: a map of facet key (titles, seniorities, industries, locations, staff_count_ranges) → list of LinkedIn entity URNs. 'locations' is REQUIRED (LinkedIn rejects campaigns without location targeting). Values must be entity URNs, never display names — resolve names FIRST via fetch type='ads' subtype='targeting_entities' filters={facet, q} (that resolve-then-boost flow is the happy path).
- {matched_audience_id}: a PRE-EXISTING uploaded matched audience only.
- {criteria}: partner-gated — rejected 400 with code matched_audience_partner_gated (LinkedIn's audience-member API needs partner access); the error's fix shows the facets form to use instead.
Vehicle/objective/format legality (enforced server-side BEFORE any spend; illegal combos 400 with the legal alternatives enumerated):
| vehicle | objectives | source | destination CTA |
|---|---|---|---|
| thought_leader | BRAND_AWARENESS, ENGAGEMENT only | member post only | NOT allowed |
| page_sponsored | all (BRAND_AWARENESS, ENGAGEMENT, | org post or video | required for |
| WEBSITE_VISIT, WEBSITE_CONVERSION, | creative_id (DSC) | WEBSITE_VISIT / | |
| LEAD_GENERATION, VIDEO_VIEW) | WEBSITE_CONVERSION | ||
| Destination = the post's external_link (set at draft/edit via this | |||
| action's content kind) or the video creative's cta_destination_url. | |||
| Vehicle is inferred from the post's publish identity when omitted | |||
| (member ⇒ thought_leader, organization ⇒ page_sponsored). Objectives use | |||
| LinkedIn's enum verbatim — do not lowercase. |
store_creative (no id — there's no campaign yet): store a harness creative as a DRAFT (zero spend; approval is separate). Stored assets serve ads AND organic post attachments (attach via this action's content kind draft/edit payload.attachment_creative_id — VRU-726). BASE64 payload (small PNG/JPEG/PDF; format decided by server-side sniff): {asset_base64 (raw base64, no data: URL — renamed from image_base64), generation_prompt (required for images only), filename?, campaign_id? (owned draft warming campaign to attach), generation_provenance? ({model, tool, generated_at, notes} allow-list, string values)}. PRESIGNED payload (the primary path for real files — mp4 ≤200MB 3s–30min, or PDF ≤25MB): {filename, size_bytes, content_type ('video/mp4' | 'application/pdf')} → returns a presigned upload_url; curl --fail-with-body -T <file> '<upload_url>', then call store_creative again with {creative_id} to validate/finalize (idempotent; a validation failure returns the failed row with ok=false and the restart step; unfinalized rows are swept after 24h). VIDEO also accepts {media_url (public https — fetched + validated in the background)} and optional {thumbnail_base64}. filename becomes the stored file_name — for documents it is the rendered LinkedIn carousel TITLE. media_url video stores are ASYNC — poll fetch type='ads' subtype='creative' id=<creative_id> until upload_status leaves 'uploading' ('failed' + error_message starting with a probe code means re-export/re-store; otherwise retriable).
Identity setup (kind='ad', no id — VRU-659):
- set_page: payload = {organization_urn? (urn:li:organization:<digits>), integration_id?}. Omit organization_urn to DISCOVER the candidate Pages from the connected LinkedIn accounts, then call again with one. Required before page_sponsored campaigns and video ads.
- authorize_author: payload = {author_user_id, auto_approve?, integration_id?} — records the operator ATTESTATION that this team member consented to Thought Leader Ad sponsorship (required before a thought_leader boost; LinkedIn still enforces its own permission). Response lists the currently authorized authors.
- revoke_author: payload = {author_user_id, integration_id?}.
Ad bulk: id arrays loop with per-item status (partial failure is reported per item, never silent; max 100). Payloads are validated by the underlying route's own schema.
Request
- Base URL: https://api.vruum.ai
- URL: https://api.vruum.ai/api/facade/manage_content
- Auth: HTTPBearer (a scheme the document does not define)
Headers
Optional. Send a unique key (a UUID) to make this call safe to retry: an exact retry of a SUCCESSFUL call replays the original response instead of re-running the action, so a dropped connection cannot double-send or double-spend. A deterministic 4xx refusal releases the key — the same key may retry once the refusing condition clears; only an ambiguous failure (>=500 or timeout) pins the key to a 409 conflict. Reusing a key with a DIFFERENT request body is rejected with 409. Keys are scoped to your company and expire after 24 hours.
Request body
Response
Successful Response
Changes
No recorded changes to this operation across all 1 revision of this API.