LinkedIn account-scoped endpoints + DSC/existing-post creatives - #21
Merged
Conversation
Fixes the LinkedIn write endpoints to use account-scoped paths and wires up the Direct Sponsored Content (DSC) Posts API so creatives can back a real post. Ads may either reference a hand-published post via `existing_post_urn` (no posting permission needed) or let the server mint a DSC dark post. Adds Campaign Group runSchedule, offsiteDeliveryEnabled + politicalIntent, locale, and B2B targeting passthrough, with focused tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the LinkedIn integration to align with newer LinkedIn Marketing API requirements by switching write endpoints to account-scoped paths and introducing Direct Sponsored Content (DSC) post backing for creatives (either reusing an existing post via existing_post_urn or minting a dark post). It also adds Campaign Group runSchedule, plus campaign creation defaults for offsiteDeliveryEnabled and politicalIntent, with focused tests to pin these behaviors.
Changes:
- Move LinkedIn write calls to account-scoped endpoints (
/adAccounts/{id}/...) and add a read helper (list_campaigns). - Add DSC post creation + creative post references, including
existing_post_urnpassthrough in the domain model and agent prompts. - Add mapping + orchestration for Campaign Group
runSchedule, plus client defaults/validation foroffsiteDeliveryEnabledandpoliticalIntent, and new integration tests.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/yieldagent/integrations/linkedin/client.py |
Switch writes to account-scoped paths; add DSC create_post, creative intendedStatus, and campaign defaults/validation. |
src/yieldagent/integrations/linkedin/server.py |
Publish orchestration now creates/reuses backing posts and sets Campaign Group runSchedule; loads dotenv in main(). |
src/yieldagent/integrations/linkedin/mapping.py |
Add campaign_run_schedule and new post/creative mapping helpers (post_article_content, post_commentary, creative_content_reference). |
src/yieldagent/integrations/linkedin/config.py |
Add optional organization_urn and env parsing for org URN/ID; bump default API version. |
src/yieldagent/domain/brief.py |
Introduce CreativeAsset.existing_post_urn field for reusing published posts. |
src/yieldagent/agents/linkedin_setup/cli.py |
Load dotenv before CLI execution. |
src/yieldagent/agents/campaign_setup/prompts.py |
Instruct planner to preserve/copy LinkedIn post URNs into existing_post_urn. |
tests/integrations/test_linkedin_client.py |
New tests pin correct endpoint paths, payload constraints, and required headers. |
tests/integrations/test_linkedin_mapping.py |
New tests for runSchedule and post/creative mapping helpers. |
tests/integrations/test_linkedin_publish.py |
New orchestration tests for “reuse existing post” vs “mint dark post” branching. |
docs/linkedin-integration.md |
Update documented default LinkedIn API version. |
briefs/example_linkedin_brief.md |
Update example budget/KPI values and document how to specify an existing post URN. |
tests/integrations/__init__.py |
Package marker for integration tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+38
to
+43
| # Accept a bare org id or a full URN. | ||
| org = os.environ.get("LINKEDIN_ORGANIZATION_URN") or os.environ.get( | ||
| "LINKEDIN_ORGANIZATION_ID" | ||
| ) | ||
| if org and not org.startswith("urn:li:organization:"): | ||
| org = f"urn:li:organization:{org.strip()}" |
| `adContext.dscAdAccount`. `dscAdType` must NOT be sent — it is read-only and | ||
| a 422 ("ReadOnly field present in a create request") results otherwise. | ||
| """ | ||
| payload: dict[str, Any] = { |
| article=post_article_content(ad.creative), | ||
| dsc_ad_account_urn=config.account_urn, | ||
| ) | ||
| post_urn = post.get("id") |
Comment on lines
+6
to
8
| Optional env: LINKEDIN_API_VERSION (default 202605), | ||
| LINKEDIN_ALLOWED_AD_ACCOUNTS (comma-separated allowlist), | ||
| YIELDAGENT_ALLOW_LIVE (set to 1 to bypass the allowlist) |
Comment on lines
44
to
48
| | Variable | Default | Meaning | | ||
| |---|---|---| | ||
| | `LINKEDIN_API_VERSION` | `202405` | Versioned API header (`LinkedIn-Version`). | | ||
| | `LINKEDIN_API_VERSION` | `202605` | Versioned API header (`LinkedIn-Version`). | | ||
| | `LINKEDIN_ALLOWED_AD_ACCOUNTS` | unset | Comma-separated allowlist of ad account ids. The configured account must be in the list, or `YIELDAGENT_ALLOW_LIVE=1` must be set. | | ||
| | `YIELDAGENT_ALLOW_LIVE` | unset | Set to `1` to bypass the allowlist. Disables the only safety net LinkedIn offers through this integration — use sparingly. | |
Comment on lines
+3
to
+8
| These pin the endpoint paths the client uses for writes. LinkedIn deprecated | ||
| the global write endpoints (`/adCampaignGroups`, `/adCampaigns`, `/creatives`) | ||
| in favour of account-scoped ones (`/adAccounts/{id}/...`). The old endpoints | ||
| return 400 with an explicit migration message — see | ||
| `docs/claude_docs/linkedin_campaign_manager_api_debug_prompt.md`. | ||
|
|
Comment on lines
+49
to
+55
| @pytest.fixture | ||
| def recorded_client(): | ||
| recorder = _Recorder() | ||
| transport = httpx.MockTransport(recorder.handler) | ||
| http = httpx.AsyncClient(transport=transport, timeout=5.0) | ||
| client = LinkedInClient(_make_config(), http=http) | ||
| yield client, recorder |
th0rz05
added a commit
that referenced
this pull request
Jun 9, 2026
) Fixes the LinkedIn write endpoints to use account-scoped paths and wires up the Direct Sponsored Content (DSC) Posts API so creatives can back a real post. Ads may either reference a hand-published post via `existing_post_urn` (no posting permission needed) or let the server mint a DSC dark post. Adds Campaign Group runSchedule, offsiteDeliveryEnabled + politicalIntent, locale, and B2B targeting passthrough, with focused tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
existing_post_urn(no posting permission needed) or mint a DSC dark post.runSchedule,offsiteDeliveryEnabled+politicalIntent, locale, and B2B targeting passthrough.Second of 3 small PRs. Builds on #20 (dotenv loader).
Test plan
PYTHONPATH=src pytest -q(34 passed)ruff checkon changed files (only pre-existing main warnings remain)