← back to Marketing Command Center
submissions: LinkedIn dedicated-app + Community Management API request package (new-app spec + email-verify + post-approval finish)
eb16f0015e90b53bfa663e4c58be767d416051dd · 2026-07-21 16:33:09 -0700 · Steve
Files touched
A docs/submissions/linkedin-app-review.md
Diff
commit eb16f0015e90b53bfa663e4c58be767d416051dd
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 21 16:33:09 2026 -0700
submissions: LinkedIn dedicated-app + Community Management API request package (new-app spec + email-verify + post-approval finish)
---
docs/submissions/linkedin-app-review.md | 231 ++++++++++++++++++++++++++++++++
1 file changed, 231 insertions(+)
diff --git a/docs/submissions/linkedin-app-review.md b/docs/submissions/linkedin-app-review.md
new file mode 100644
index 0000000..010acd8
--- /dev/null
+++ b/docs/submissions/linkedin-app-review.md
@@ -0,0 +1,231 @@
+# LinkedIn Community Management API — access package (Designer Wallcoverings)
+
+**Purpose:** get the Marketing Command Center (MCC) able to post organic content to the
+**Designer Wallcoverings LinkedIn Company Page** (`w_organization_social`).
+
+**Honest framing up front — this is a NEW-APP + LinkedIn-REVIEW cycle, not a same-session
+finish.** Steps 1–3 (create the dedicated app + request the product + verify the business email)
+are driveable now. Then LinkedIn reviews the access request and emails a decision, typically
+**1–3 business days**. Only after that email arrives can Steps in §4 (enable scope → mint token →
+deploy → verify a real post) run. Budget two work sessions separated by the review wait.
+
+---
+
+## 0. Why a NEW app (the root blocker, tooltip-confirmed 2026-07-21)
+
+Posting to the DW **Company Page** requires the `w_organization_social` scope, which is only
+granted by the **Community Management API** product. LinkedIn enforces a hard policy:
+
+> Community Management API **requires it be the ONLY product on the application** (legal/security).
+> *"A new developer application can be created to request this product."*
+
+The existing app **"For Claude - 5-6-26"** (client `86d1oaphl1brc3`, app id `235220450`,
+company-verified to the DW Page May-6) already has **"Share on LinkedIn"** provisioned. So its
+Community Management API **"Request access" button is permanently greyed** — you cannot add CMA to
+an app that already carries another product.
+
+**Resolution = create a brand-new dedicated LinkedIn developer app that has ONLY the Community
+Management API product**, tied to the Designer Wallcoverings Page. That is what this package sets up.
+
+> Note: "Share on LinkedIn" (`w_member_social`) posts to a **personal profile**, not the brand
+> Page — it is NOT what we want and must NOT be added to the new app (adding it re-triggers the
+> exact greyed-out blocker above).
+
+---
+
+## 1. Create the new app
+
+**Click-path:** https://www.linkedin.com/developers/apps/new
+(sign in as a Designer Wallcoverings Page **admin** — the same identity that admins the DW Company
+Page, so the app can be verified against it.)
+
+Fill exactly:
+
+| Field | Value |
+|---|---|
+| **App name** | `DW Marketing – Community` |
+| **LinkedIn Page** | `Designer Wallcoverings` (type it, select the real DW Company Page — this associates the app with the Page and drives company verification) |
+| **App logo** | Upload the DW square logo (the same mark used on the DW Page avatar — a PNG ≥ 100×100). |
+| **Privacy policy URL** | `https://designerwallcoverings.com/policies/privacy-policy` |
+| **Legal agreement** | ✅ check "I have read and agree to these terms" — **STEVE must click this** (legal acceptance, gated). |
+
+Click **Create app**.
+
+After creation, on the app's **Settings** tab, confirm the DW Page shows a **"Verify"** state and
+complete company verification if prompted (LinkedIn sends a verification link to a DW Page admin —
+one click confirms the app legitimately represents the DW Page). The new app gets its **own**
+client id + client secret (they will differ from the old `86d1oaphl1brc3`) — note both; the secret
+routes to `secrets` as the app's `LINKEDIN_CLIENT_ID` / `LINKEDIN_CLIENT_SECRET` for the new app.
+
+---
+
+## 2. Add ONLY the Community Management API (+ business-email verification)
+
+On the new app → **Products** tab:
+
+1. Find **Community Management API** → click **Request access**.
+2. **Do NOT add "Share on LinkedIn" or any other product.** CMA must be the *sole* product or the
+ request will be blocked exactly as it was on the old app.
+3. LinkedIn requires a **verified business email** before it will accept the request. In the
+ verification sub-flow:
+ - Enter **`info@designerwallcoverings.com`**.
+ - LinkedIn emails a **6-digit code** to that inbox.
+ - **George reads the code** (Gmail — the info@ catch-all forwards into the George Gmail;
+ search subject like *"LinkedIn"* / *"verification code"*). A code `977385` was seen earlier
+ this session but is **almost certainly expired** — request a fresh one and read the newest.
+ - Enter the fresh 6-digit code to verify the business email.
+4. Submit the access request. The button goes **greyed** = the request is **lodged and pending
+ LinkedIn review**. LinkedIn **emails a decision** (approve/deny), typically **1–3 business days**.
+
+If a **justification / use-case free-text form** appears during the request, paste §3 verbatim.
+
+---
+
+## 3. Access-request justification (paste-ready)
+
+> **Designer Wallcoverings** is a luxury wallcovering and interior-design retailer. We use the
+> LinkedIn Community Management API **solely to publish our own first-party organic content**
+> — new product launches, design collections, and editorial imagery — **to our own Designer
+> Wallcoverings LinkedIn Company Page**, for the interior-design and architecture trade audience
+> that follows us there.
+>
+> The integration posts **only** to the single Company Page we own and administer. We do **not**
+> manage, moderate, or post to any third-party Pages, we do **not** read or aggregate other
+> organizations' content, and we do **not** offer this as a service to other businesses. This is a
+> single-tenant, owned-Page, organic-posting use case: our marketing tool authenticates as a DW
+> Page admin and publishes DW's own posts on DW's own Page. No third-party page management.
+
+---
+
+## 4. Post-approval finish (only after LinkedIn's approval email lands — ≈ 5–10 min)
+
+All of these are on the **new** app unless noted. Use the **new app's** client id/secret.
+
+### 4a. Enable the scope + redirect (Auth tab)
+- App → **Auth** tab → **OAuth 2.0 scopes** → enable **`w_organization_social`**.
+ (Community Management approval unlocks it in the scope list; before approval it isn't selectable.)
+- App → **Auth** tab → **Authorized redirect URLs** → add exactly:
+ ```
+ https://marketing.designerwallcoverings.com/api/channels/oauth/linkedin/callback
+ ```
+ (This is the MCC's live LinkedIn callback — it must match the `redirect_uri` used at mint time
+ byte-for-byte or the exchange 400s with `redirect_uri_mismatch`.)
+
+### 4b. Mint the 60-day token (Steve clicks Allow as DW Page admin)
+Build the authorize URL with the **NEW app's** client id:
+```
+https://www.linkedin.com/oauth/v2/authorization?response_type=code
+ &client_id=<NEW_APP_CLIENT_ID>
+ &redirect_uri=https%3A%2F%2Fmarketing.designerwallcoverings.com%2Fapi%2Fchannels%2Foauth%2Flinkedin%2Fcallback
+ &scope=w_organization_social
+ &state=dwmcc
+```
+Steve opens it **signed in as a DW Company Page admin**, clicks **Allow**. LinkedIn redirects to
+the MCC callback with `?code=...`. Exchange the code for the access token (60-day):
+```sh
+curl -s -X POST https://www.linkedin.com/oauth/v2/accessToken \
+ -d grant_type=authorization_code \
+ -d code='<CODE_FROM_CALLBACK>' \
+ -d redirect_uri='https://marketing.designerwallcoverings.com/api/channels/oauth/linkedin/callback' \
+ -d client_id='<NEW_APP_CLIENT_ID>' \
+ -d client_secret='<NEW_APP_CLIENT_SECRET>'
+# → { "access_token": "...", "expires_in": 5184000, ... } (5184000s = 60 days)
+```
+That `access_token` is `LINKEDIN_ACCESS_TOKEN`.
+
+### 4c. Find `LINKEDIN_ORG_URN`
+The MCC adapter (`modules/channels/index.js` → `postLinkedIn`) posts as
+`env('LINKEDIN_ORG_URN')`, which must be **`urn:li:organization:<DW page numeric id>`**.
+Query the organizations this token can act as (send the version + protocol headers the adapter
+uses):
+```sh
+curl -s 'https://api.linkedin.com/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED' \
+ -H "Authorization: Bearer <LINKEDIN_ACCESS_TOKEN>" \
+ -H 'LinkedIn-Version: 202401' \
+ -H 'X-Restli-Protocol-Version: 2.0.0'
+# → elements[].organization = "urn:li:organization:<NUMERIC_ID>" ← that's LINKEDIN_ORG_URN
+```
+(Alternatively, the DW Company Page's numeric id is visible in the Page admin URL; wrap it as
+`urn:li:organization:<id>`.) Verify the returned org is Designer Wallcoverings before saving.
+
+### 4d. Route secrets + deploy
+Route both values via the `secrets` skill so they fan out to the master `.env`, the MCC project
+`.env`, and the Kamatera MCC `.env`:
+- `LINKEDIN_ACCESS_TOKEN=<60-day token>`
+- `LINKEDIN_ORG_URN=urn:li:organization:<DW numeric id>`
+
+Then on Kamatera restart with fresh env:
+```sh
+pm2 restart marketing-command-center --update-env
+```
+Confirm the channel is now connected (does NOT expose the token):
+```sh
+curl -u admin:DW2024! https://marketing.designerwallcoverings.com/api/channels/status
+# linkedin → connected: true, accounts: ["urn:li:organization:<id>"]
+```
+
+### 4e. Prove it — re-approve a LinkedIn Engine suggestion
+The MCC Engine already carries LinkedIn suggestions (trade-voice captions). Approve one so the
+scheduler fires `channels.postLinkedIn` (image `initializeUpload` → PUT bytes → `POST /rest/posts`).
+Then **verify the post appears on the Designer Wallcoverings LinkedIn Company Page**.
+Outbound publish is Steve-gated (fire from the MCC UI or via `!`).
+
+---
+
+## 5. Checklist
+
+- [ ] **(Steve)** Create app `DW Marketing – Community` at `/developers/apps/new`, LinkedIn Page =
+ Designer Wallcoverings, logo, privacy URL, **check the legal agreement**.
+- [ ] Complete DW Page company verification on the new app (Settings tab).
+- [ ] Capture the NEW app's client id + client secret → route to `secrets`.
+- [ ] Products tab → **Request access to Community Management API ONLY** (no Share on LinkedIn).
+- [ ] Business-email verify: enter `info@designerwallcoverings.com` → **George reads the fresh
+ 6-digit code** → enter it.
+- [ ] Submit; button greys = request lodged, pending LinkedIn review (1–3 days).
+- [ ] **⏳ WAIT for LinkedIn's decision email** (George/canary can watch info@ for it).
+- [ ] On approval: Auth tab → enable `w_organization_social` + add MCC redirect URI.
+- [ ] **(Steve)** Open authorize URL, click **Allow** as DW Page admin.
+- [ ] Exchange code → 60-day `LINKEDIN_ACCESS_TOKEN`.
+- [ ] Find `LINKEDIN_ORG_URN` via `/rest/organizationAcls` (or Page numeric id); confirm = DW.
+- [ ] Route both to `secrets` → Kamatera `.env` → `pm2 restart marketing-command-center --update-env`.
+- [ ] `channels/status` shows linkedin connected.
+- [ ] Approve a LinkedIn Engine suggestion → verify the post on the DW Company Page.
+
+---
+
+## 6. Who does what — agent (openclaw) vs Steve vs George
+
+| Step | Driver |
+|---|---|
+| Fill the Create-app form fields (name, Page, logo, privacy URL) | **Agent via openclaw real-Chrome** (Steve signed in) |
+| **Check the legal-agreement box + click Create app** | **STEVE** (legal acceptance — gated) |
+| Company-verify the app to the DW Page | Agent can click through; the verification link goes to a DW Page admin (Steve) |
+| Products tab → Request access to Community Management API only | **Agent via openclaw** |
+| Enter `info@designerwallcoverings.com` for business-email verify | **Agent** |
+| **Read the 6-digit verification code** | **George** (auto-reads the info@ Gmail; no Steve action) |
+| Submit the access request | **Agent** |
+| **Wait for LinkedIn's decision email** | passive — **George/canary** surfaces it |
+| Enable `w_organization_social` + add redirect (Auth tab) | **Agent via openclaw** |
+| **Click "Allow" on the OAuth consent** (as DW Page admin) | **STEVE** (the OAuth Allow — gated) |
+| Exchange code → token; find ORG_URN; route to secrets; deploy | **Agent** |
+| Approve an Engine LinkedIn suggestion → verify live post | **Agent stages; publish is Steve-gated** (fire from UI) |
+
+**Net:** the only two things that strictly need Steve in real time are (1) the **legal-agreement
+checkbox** on app creation and (2) the **OAuth "Allow" click**. Everything else — form-fill,
+product request, email-code reading (George), token mint, ORG_URN lookup, deploy — the agent can
+drive.
+
+---
+
+## 7. State of the MCC adapter (already built — no build work needed post-approval)
+
+`modules/channels/index.js`:
+- `postLinkedIn(content)` reads `LINKEDIN_ORG_URN` (falls back to `LINKEDIN_AUTHOR_URN`) as the
+ author, does the full image flow: `POST /rest/images?action=initializeUpload` → `PUT` the image
+ bytes → `POST /rest/posts` with `commentary` (text+link) + the image URN.
+- Headers pinned: `LinkedIn-Version: 202401`, `X-Restli-Protocol-Version: 2.0.0`.
+- `channels/status` reports `connected` iff `LINKEDIN_ACCESS_TOKEN` **and** an org/author URN are
+ present — so the token + `LINKEDIN_ORG_URN` are the only two runtime values still missing.
+- The Engine already generates trade-voice LinkedIn suggestions.
+
+So once §4 lands the two env values, LinkedIn is a live 7th channel with **zero** additional code.
← 4e9c425 submissions: TikTok production App Review package (fields +
·
back to Marketing Command Center
·
submissions: Facebook pages_manage_posts App Review package 2becbe5 →