Skill Tracer
Skills › Knowledge Work Plugins › social-content-engine › publishing.md

publishing.md

166 lines · small-business/skills/social-content-engine/reference/publishing.md at 2574e29

Open on GitHub
1# Publishing and staging
2
3Two paths, in order of preference. Whichever one runs, the post is **staged,
4never published** — the owner controls go-live and can still cancel or edit.
5
6| Path | When | What the owner gets |
7|---|---|---|
8| HubSpot social | Marketing Hub Professional or higher | A scheduled post in the HubSpot campaign queue |
9| Scheduling CSV | No HubSpot, or a non-Professional HubSpot plan | A CSV to import into Buffer, Later, or their tool |
11---

Publishing and staging

Two paths, in order of preference. Whichever one runs, the post is staged, never published — the owner controls go-live and can still cancel or edit.

PathWhenWhat the owner gets
HubSpot socialMarketing Hub Professional or higherA scheduled post in the HubSpot campaign queue
Scheduling CSVNo HubSpot, or a non-Professional HubSpot planA CSV to import into Buffer, Later, or their tool

Raw lines 1 to 12
13## Before staging anything
151. **Every scheduled time must be in the future.** Calendars built weeks ago
16 go stale quietly. Compare each time against now and surface any that have
17 passed: "The Jul 8 post date has already passed. Skip it or move it?"
182. **Every asset URL must be a permanent export URL,** not an autofill
19 response thumbnail. Those expire in minutes and attach as broken images.
203. **The caption must be the approved one,** not a later edit that skipped
21 Checkpoint 3.
23---

Before staging anything

  1. Every scheduled time must be in the future. Calendars built weeks ago go stale quietly. Compare each time against now and surface any that have passed: "The Jul 8 post date has already passed. Skip it or move it?"
  2. Every asset URL must be a permanent export URL, not an autofill response thumbnail. Those expire in minutes and attach as broken images.
  3. The caption must be the approved one, not a later edit that skipped Checkpoint 3.

Raw lines 13 to 24
25## HubSpot social staging
27Requires: **HubSpot Marketing Hub Professional** (or higher).
28Base URL: `https://api.hubapi.com`
29Auth: Bearer token (OAuth 2.0 or private app token).
31---

HubSpot social staging

Requires: HubSpot Marketing Hub Professional (or higher). Base URL: https://api.hubapi.com Auth: Bearer token (OAuth 2.0 or private app token).


Raw lines 25 to 32
33## Tier check
35If you're unsure whether the user has Marketing Hub Professional:
37```
38GET /crm/v3/objects/companies?limit=1
39```
41Check the `subscriptionType` on the account. If the API returns a 403 on
42social endpoints, tell the user: "HubSpot campaign staging requires Marketing
43Hub Professional. Your current plan doesn't include it. I can export a
44scheduling CSV instead — would that work?"
46---

Tier check

If you're unsure whether the user has Marketing Hub Professional:

GET /crm/v3/objects/companies?limit=1

Check the subscriptionType on the account. If the API returns a 403 on social endpoints, tell the user: "HubSpot campaign staging requires Marketing Hub Professional. Your current plan doesn't include it. I can export a scheduling CSV instead — would that work?"


Raw lines 33 to 47
48## Create a campaign
50```
51POST /marketing/v3/campaigns
53 "name": "Summer Sale 2026 — Social",
54 "startDate": "2026-06-01",
55 "endDate": "2026-06-30",
56 "currencyCode": "<Currency from the Business context block, e.g. AUD>",
57 "utm": {
58 "source": "social",
59 "medium": "owned",
60 "campaign": "summer-sale-2026"
61 }
63```
65Response: `{ "id": "<campaign_id>", ... }` — save this for social post association.
67---

Create a campaign

POST /marketing/v3/campaigns
{
  "name": "Summer Sale 2026 — Social",
  "startDate": "2026-06-01",
  "endDate":   "2026-06-30",
  "currencyCode": "<Currency from the Business context block, e.g. AUD>",
  "utm": {
    "source": "social",
    "medium": "owned",
    "campaign": "summer-sale-2026"
  }
}

Response: { "id": "<campaign_id>", ... } — save this for social post association.


Raw lines 48 to 68
69## Create a social post
71One call per calendar row. Stage as `SCHEDULED`, never `PUBLISHED`.
73```
74POST /marketing/v3/social/posts
76 "campaignId": "<campaign_id>",
77 "channelId": "<hubspot_channel_id>",
78 "content": {
79 "body": "<approved caption text>"
80 },
81 "scheduledAt": "2026-06-03T10:00:00Z",
82 "attachments": [
83 {
84 "url": "<canva_export_png_url>"
85 }
86 ],
87 "status": "SCHEDULED"
89```
91**`channelId`** — the HubSpot Social account ID (not the platform name).
92Retrieve connected accounts:
93```
94GET /marketing/v3/social/channels
95```
96Match by `type` (`INSTAGRAM`, `FACEBOOK`, `TWITTER`, `LINKEDIN`) and use the `id` field.
98**`scheduledAt`** — must be in the future (relative to the time of the API
99call). Use noon local time for the owner's timezone unless they specify
100otherwise.
102---

Create a social post

One call per calendar row. Stage as SCHEDULED, never PUBLISHED.

POST /marketing/v3/social/posts
{
  "campaignId": "<campaign_id>",
  "channelId":  "<hubspot_channel_id>",
  "content": {
    "body": "<approved caption text>"
  },
  "scheduledAt": "2026-06-03T10:00:00Z",
  "attachments": [
    {
      "url": "<canva_export_png_url>"
    }
  ],
  "status": "SCHEDULED"
}

**channelId** — the HubSpot Social account ID (not the platform name). Retrieve connected accounts:

GET /marketing/v3/social/channels

Match by type (INSTAGRAM, FACEBOOK, TWITTER, LINKEDIN) and use the id field.

**scheduledAt** — must be in the future (relative to the time of the API call). Use noon local time for the owner's timezone unless they specify otherwise.


Raw lines 69 to 103
104## Verify the scheduled queue
106After staging all posts:
108```
109GET /marketing/v3/social/posts?status=SCHEDULED&campaignId=<campaign_id>
110```
112Surface results in a table: date, channel, first 60 chars of caption, status.
113Provide the direct HubSpot campaign URL:
114`https://app.hubspot.com/content/{portalId}/social/campaigns/{campaign_id}`
116Tell the user: "Posts are scheduled and will publish automatically. You can
117cancel or edit any post in HubSpot before the send time."
119---

Verify the scheduled queue

After staging all posts:

GET /marketing/v3/social/posts?status=SCHEDULED&campaignId=<campaign_id>

Surface results in a table: date, channel, first 60 chars of caption, status. Provide the direct HubSpot campaign URL: https://app.hubspot.com/content/{portalId}/social/campaigns/{campaign_id}

Tell the user: "Posts are scheduled and will publish automatically. You can cancel or edit any post in HubSpot before the send time."


Raw lines 104 to 120
121## CSV fallback (non-Professional users)
123When HubSpot staging is not available, export a CSV the user can import into
124Buffer, Later, or their scheduler of choice.
126Column headers:
127```
128Date,Time,Channel,Caption,ImageURL,Status
129```
131Example row:
132```
1332026-06-03,10:00 AM CDT,Instagram,"Summer Sale: 30% off candles ☀️ ...",https://canva.com/export/...,Scheduled
134```
136Tell the user: "I've prepared a scheduling CSV since your HubSpot plan doesn't
137include social staging. You can import this into Buffer or Later. [file path]"
139---

CSV fallback (non-Professional users)

When HubSpot staging is not available, export a CSV the user can import into Buffer, Later, or their scheduler of choice.

Column headers:

Date,Time,Channel,Caption,ImageURL,Status

Example row:

2026-06-03,10:00 AM CDT,Instagram,"Summer Sale: 30% off candles ☀️ ...",https://canva.com/export/...,Scheduled

Tell the user: "I've prepared a scheduling CSV since your HubSpot plan doesn't include social staging. You can import this into Buffer or Later. [file path]"


Raw lines 121 to 140
141## Field reference
143| Field | Type | Notes |
144|-------|------|-------|
145| `campaignId` | string | UUID from campaign creation |
146| `channelId` | string | From `GET /social/channels` |
147| `content.body` | string | Caption text; max 2,000 chars |
148| `scheduledAt` | ISO 8601 | Must be future time; include timezone offset |
149| `attachments[].url` | string | Public URL (Canva export URL works) |
150| `status` | enum | Always `"SCHEDULED"` — never `"PUBLISHED"` |
152---

Field reference

FieldTypeNotes
campaignIdstringUUID from campaign creation
channelIdstringFrom GET /social/channels
content.bodystringCaption text; max 2,000 chars
scheduledAtISO 8601Must be future time; include timezone offset
attachments[].urlstringPublic URL (Canva export URL works)
statusenumAlways "SCHEDULED" — never "PUBLISHED"

Raw lines 141 to 153
154## Email handoff
156Email copy is never staged by this skill, on any path. Present the approved
157subject, preheader, and body inline, grouped by send date, for the owner to
158paste into whatever they send from:
160```
161Jul 11 — "Your service plan renews Aug 1"
162Aug 5 — "Two tune-up slots left this month"
163```
165That is the whole handoff. Do not offer to send it, and do not build a Canva
166design for it.

Email handoff

Email copy is never staged by this skill, on any path. Present the approved subject, preheader, and body inline, grouped by send date, for the owner to paste into whatever they send from:

Jul 11 — "Your service plan renews Aug 1"
Aug 5  — "Two tune-up slots left this month"

That is the whole handoff. Do not offer to send it, and do not build a Canva design for it.

Raw lines 154 to 166