Appearance
linkedin
38 endpoints.
GET /api/v1/linkedin/status
Status
Everything the app needs to decide what to render.
One call rather than three, because the page cannot draw anything useful until it knows all of: are there credentials, is anything connected, and what did LinkedIn actually grant.
200 — Successful Response
json
{}GET /api/v1/linkedin/accounts
List Accounts
200 — Successful Response
json
{}GET /api/v1/linkedin/auth-url
Auth Url
Where to send the browser to sign in.
The tenant and user are stashed against the state nonce because the callback arrives unauthenticated — without this the connection could not be attributed to anyone.
200 — Successful Response
json
{}POST /api/v1/linkedin/accounts/{account_id}/default
Set Default
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
account_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/accounts/{account_id}/refresh
Refresh Account
Re-read the profile and re-scan for company pages.
Useful without a reconnect whenever a page is added to (or removed from) the member's admin list on LinkedIn's side.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
account_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/linkedin/accounts/{account_id}
Disconnect
Disconnect an identity. Soft delete — published posts keep their history.
Disconnecting a MEMBER takes its company pages with it, because their only credential was that member's (see the model docstring).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
account_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/posts
List Posts
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
status | query | no | string | |
limit | query | no | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/posts
Create Post
Create a draft, schedule it, or publish it now.
action decides: draft (default), schedule (needs scheduled_at) or publish. One endpoint rather than three because the composer is one form — splitting it would mean the row is written twice and a failed publish would leave nothing behind to fix and retry.
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PATCH /api/v1/linkedin/posts/{post_id}
Update Post
Edit a draft or a scheduled post.
A PUBLISHED post is not editable here on purpose: the row is a record of what went out, and letting it drift from what LinkedIn is showing would make the library a fiction.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
post_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}DELETE /api/v1/linkedin/posts/{post_id}
Delete Post
Remove a post from the library, and optionally from LinkedIn.
remote=false (the default) leaves a published post standing on LinkedIn — deleting a row here must not silently unpublish something the world has already seen. remote=true is the explicit "take it down too".
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
post_id | path | yes | integer | |
remote | query | no | boolean |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/posts/{post_id}/publish
Publish Now
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
post_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/posts/{post_id}/duplicate
Duplicate Post
Copy a post back to a draft — the usual way to re-run something that worked. Media is copied by REFERENCE to the same local files but WITHOUT the LinkedIn URNs: an asset URN belongs to the post it was uploaded for, so the copy re-uploads rather than reusing one and getting a 422.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
post_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/upload
Upload
Stage one file for a post.
Written to disk and described back to the composer; it is NOT sent to LinkedIn here. Upload happens at publish time, against the account that turns out to be posting — LinkedIn ties an asset to its owner URN, so an asset uploaded before the author is chosen would be unusable.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
alt | query | no | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/media/{name}
Media
Serve a staged file back to the composer.
Authenticated and uuid-addressed rather than a static nginx alias, for the reason recorded on LINKEDIN_MEDIA_DIR: unpublished artwork must not be walkable, and this needs no vhost change on any box the app runs on.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
name | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/comments
List Comments
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
handled | query | no | ||
limit | query | no | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/posts/{post_id}/comments/pull
Pull Comments
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
post_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/comments/{comment_id}/reply
Reply
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
comment_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/comments/{comment_id}/handled
Mark Handled
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
comment_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"default": {},
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/engagement/sync
Sync Engagement
200 — Successful Response
json
{}GET /api/v1/linkedin/analytics
Analytics
The daily page rows, newest last so a chart can plot them directly.
totals is computed here rather than in the browser because "followers" is a LEVEL (take the latest) while impressions and clicks are FLOWS (sum them) — mixing the two up is the classic way a social dashboard reports a follower count in the tens of thousands.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
days | query | no | integer | |
account_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/analytics/sync
Sync Analytics
200 — Successful Response
json
{}GET /api/v1/linkedin/ads/context
Ads Context
Everything the wizard needs before it can draw its first screen.
One call rather than four, for the reason /status is one call: the dialog cannot render a single dropdown until it knows the connection, the ad accounts, the pages those accounts may advertise as, and the option catalogue.
It does NOT fail when the ads scopes are missing. A connection without r_ads still gets the catalogue and an unavailable sentence, so the wizard can explain itself instead of showing an empty dialog — that distinction is the whole reason AdsUnavailable is caught here rather than raised through.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
account_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/ads/pages
Ads Pages
The company pages one ad account may advertise as, plus its conversions.
Both are per-AD-ACCOUNT and neither is known until the account is picked, which is why they are not in /ads/context. The pages LinkedIn returns are intersected with the organization rows this tenant has connected, so a page the ad account can use but nobody here administers is not offered — the post would be authored by a page whose token we do not hold.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
account_urn | query | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/ads/targeting
Ads Targeting
Typeahead for one audience facet.
facet is the wizard's short key ("titles"), not the URN, so a caller cannot ask LinkedIn for an arbitrary facet by editing the query string.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
facet | query | yes | string | |
q | query | no | string | |
locale | query | no | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/ads/preview
Ads Preview
What the Review step shows: the problems, and LinkedIn's own estimate.
Validation runs through the SAME function the create path runs, so the review screen cannot disagree with the launch. The audience size is best-effort and None when LinkedIn will not price it — see LinkedInClient.audience_count.
Request body
json
{
"additionalProperties": true,
"type": "object",
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/ads/campaigns
Create Ad Campaign
Build the campaign on LinkedIn. The wizard's Launch button.
admin, not sales: everything else in this app writes a post, and this one commits a budget.
The staged media the wizard uploaded is read off disk HERE rather than being re-posted by the browser — same reason the composer stages files: an asset is tied to the owner URN it was uploaded under, and the owning page is not known until this call.
Request body
json
{
"additionalProperties": true,
"type": "object",
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/plan
Plan Campaign
The New Campaign form's submit. Writes the run, then starts the Brief.
sales, not admin: nothing here reaches LinkedIn or commits a budget — that is the publish endpoint below, which is admin for exactly that reason.
Request body
json
{
"additionalProperties": true,
"type": "object",
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/replan
Replan Campaign
"Change an answer": the edited form, planned again from the Brief.
The same campaign, not a copy — nothing exists on LinkedIn yet, so there is nothing a copy would protect. Every step the agents wrote was worked out from the old answers, so all of them are cleared and rewritten.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"default": {},
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/stages/{stage}/run
Run Campaign Stage
Write (or rewrite) one agent step — "Re-run", and co-pilot arriving on a step.
Refused while another step is being written, once the campaign is on LinkedIn, and before the step it is written from exists.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer | |
stage | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/stages/{stage}/redo-after
Redo After Stage
After an edit: clear the agent steps written from the version replaced.
The Meta builder's "Redo" on its stale notice. Co-pilot writes them again as each is reached; autopilot writes them now.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer | |
stage | path | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/campaigns/{run_id}
Read Campaign Run
Reopen an unpublished campaign — the register's "View process".
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}PUT /api/v1/linkedin/campaigns/{run_id}
Save Campaign Run
Keep what somebody changed on a step — the brief, the plan, the copy sets, the chosen set, a replaced image.
Without this an edit lived only in the open dialog, and reopening the campaign from the register brought the agent's original back. The chosen copy set is laid onto the plan on every save, because publish reads the ad's words from the plan.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"default": {},
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/check
Check Campaign Run
Everything that would stop this campaign publishing, before the press.
Runs the SAME :func:linkedin_ads.validate the publish path runs, over the SAME payload :func:linkedin_ads.payload_from_plan builds — so the review screen cannot disagree with what happens when the button is pressed. That identity is the whole point of the endpoint: the previous arrangement built the payload in the browser and validated it on the server, and the two had drifted apart in four places without anything noticing.
sales: it reads and decides nothing. Publishing is admin.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"default": {},
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/fixes/{code}
Apply Campaign Fix
Apply one quick fix, check again, and — when asked — publish as a draft.
The same concept as the Meta builder's /ad-runs/{id}/fixes/{code}: the value changes, the campaign is re-validated, and "and publish" finishes the build already under way (create_campaign reuses whatever a failed attempt created), so it never makes a second campaign. Publishing still needs admin and is refused here for anyone else, after the fix is saved.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer | |
code | path | yes | string |
Request body
json
{
"type": "object",
"additionalProperties": true,
"default": {},
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/publish
Publish Campaign Run
Build the campaign on LinkedIn from the reviewed plan.
admin: this is the press that commits a budget. Everything else in the process is sales.
The payload is what the REVIEW SCREEN holds, not what the agent proposed — the two differ whenever somebody edited a field, and publishing the agent's version instead would silently discard their edit.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
Request body
json
{
"type": "object",
"additionalProperties": true,
"title": "Body"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/linkedin/campaigns/{run_id}/go-live
Go Live Campaign Run
Publish a draft campaign live. THIS STARTS SPENDING. admin.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
run_id | path | yes | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/linkedin/ads/app-check
Ads App Check
Which LinkedIn products this site's developer app has actually been given.
2026-09-08. The wizard is blocked on a LinkedIn APPROVAL, not on anything in this codebase, and nothing tells us when that approval lands. Before this endpoint the only way to find out was to switch a scope on and try to sign in — which is also the way to break sign-in for the whole site, because an ungranted scope fails the entire authorization rather than degrading.
So this asks LinkedIn's authorization endpoint directly, without a token, a secret, a member or a completed redirect. See linkedin_client.probe_scopes.
admin: it names the client id and the products a site is missing.
200 — Successful Response
json
{}GET /api/v1/linkedin/callback
Callback
LinkedIn's redirect target. Unauthenticated by necessity.
A cancelled consent screen comes back as error=user_cancelled_login rather than as a failure, so it is reported as a message, not a 400 — the person changed their mind, nothing is broken.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
code | query | no | string | |
state | query | no | string | |
error | query | no | string | |
error_description | query | no | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}