facade

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:

  1. manage_content kind='ad' action='store_creative' payload={filename:'my-deck.pdf', size_bytes:<n>, content_type:'application/pdf'} → returns upload_url
  2. curl --fail-with-body -T my-deck.pdf '<upload_url>'
  3. store_creative again with payload={creative_id:'<id>'} → validates (small assets can skip 1-3: payload={asset_base64:'<b64>'})
  4. manage_content action='draft' payload={content:'<caption>', attachment_creative_id:'<id>'}
  5. get_content_review post_ids=['<post_id>'] → OPEN attachment_url and review the actual file (it publishes under the author's identity)
  6. 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):

vehicleobjectivessourcedestination CTA
thought_leaderBRAND_AWARENESS, ENGAGEMENT onlymember post onlyNOT allowed
page_sponsoredall (BRAND_AWARENESS, ENGAGEMENT,org post or videorequired 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.

post/api/facade/manage_content

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

Idempotency-Keystring nullable

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

actionstring required

The operation to perform — see the tool description for the action vocabulary and each action's id/payload semantics.

payloadobject nullable

Action-specific fields, validated by the underlying route's own schema (a bad field 422s with that route's precise errors). See the tool description per action.

itemsobject[] nullable

For create-like actions only: an array of per-item payloads processed in order with per-item results (max 100). Mutually exclusive with id and payload.

kindstring nullable

Content family: 'content' (default) for LinkedIn posts, 'objective_content' for sourced objective posts and response follow-up (prepare, draft, decide, handoff, response_prepare, response_draft, reply_prepare, reply_decide), or 'ad' to manage ad campaigns (actions: reject, pause, boost (always a draft), store_creative, set_page, authorize_author, revoke_author).

Response

Successful Response

unknown required

Changes

No recorded changes to this operation across all 1 revision of this API.