yikart/AiToEarn · error · LinkedInPlatformException
15070
15070
Error message
{{platform}} platform API request failed What it means
LinkedInService's axios interceptor converts every failed LinkedIn REST API request into a LinkedInPlatformException with ResponseCode.ChannelPlatformApiFailed (15070). Both network-level Axios errors (no response) and non-2xx responses from api.linkedin.com are wrapped; the LinkedIn error body is parsed for platform code/message. Context captured includes endpoint, method, HTTP status, error category, retryability, and the raw LinkedIn payload.
Source
Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin.service.ts:37
import { LinkedInOAuthGrantType, LinkedInPostLifecycleState } from './linkedin.interface'
import { LinkedInDistribution, LinkedInVisibility } from './linkedin.schema'
@Injectable()
export class LinkedInService {
private readonly http: AxiosInstance
private readonly apiBaseUrl = 'https://api.linkedin.com/v2'
private readonly restBaseUrl = 'https://api.linkedin.com/rest'
constructor(private readonly cfg: LinkedinConfig) {
this.http = this.createHttpClient()
}
private createHttpClient(): AxiosInstance {
const http = axios.create()
http.interceptors.response.use(
response => response,
(error: AxiosError<LinkedInErrorBody>) => {
throw LinkedInPlatformException.fromAxiosError(error)
},
)
return http
}
private restHeaders(accessToken: string): Record<string, string> {
return {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Linkedin-Version': this.cfg.restVersion,
'X-Restli-Protocol-Version': '2.0.0',
}
}
generateAuthUrl(scopes: string[], state: string): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.cfg.clientId,View on GitHub (pinned to d3aa8bea5b)
Solutions
- Inspect the thrown LinkedInPlatformException's cause (platformMessage, httpStatus, platformCode) — LinkedIn includes a detailed message and serviceErrorCode in the body.
- If httpStatus is 401, refresh the LinkedIn access token (or re-run the OAuth flow) and retry.
- If 403, verify the app has requested the required products/scopes (e.g. 'Share on LinkedIn') and that the member/organization has post permission.
- If 429 or retryable network/5xx, back off and retry respecting Retry-After.
- Confirm the LinkedIn-Version header matches a supported REST API version and the request body conforms to current UGC-post schema.
Example fix
// before: token expired -> 401 wrapped as 15070
const res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(accessToken) })
// after: refresh on 401 and retry once
try {
const res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(accessToken) })
} catch (e) {
if (e instanceof LinkedInPlatformException && e.cause?.httpStatus === 401) {
const fresh = await this.refreshAccessToken(channel)
const res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(fresh) })
} else throw e
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: token validity and required product/scope confirmation if (!accessToken || tokenExpiresAt <= new Date()) await refreshLinkedInToken(channel) // member must have granted 'w_member_social' (or org admin for 'w_organization_social') before posting
Type guard
function isLinkedInPlatformException(e: unknown): e is LinkedInPlatformException {
return e instanceof LinkedInPlatformException
} Try / catch
try {
const result = await linkedinService.createPost(accessToken, postBody)
} catch (e) {
if (e instanceof LinkedInPlatformException) {
switch (e.cause?.httpStatus) {
case 401: /* refresh token, retry once */ break
case 403: /* check scopes/products and org admin rights */ break
case 429: /* honor Retry-After header before retrying */ break
default:
if (e.retryable) { /* backoff retry */ } else { /* surface cause.platformMessage */ }
}
} else throw e
} Prevention
- Re-authenticate LinkedIn channels before the ~60-day token lifetime expires.
- Request the 'Share on LinkedIn' product and w_member_social / w_organization_social scopes during app setup.
- Confirm organization admin rights before posting as a company page.
- Respect LinkedIn throttling limits and Retry-After headers in batch workflows.
- Keep the LinkedIn-Version header on a currently supported REST API version and test after version bumps.
When it happens
Trigger: Any call through this.http — OAuth code exchange, fetching member profile (restHeaders-authenticated /rest endpoints), creating UGC posts, uploading images — that returns a non-2xx status (401 invalid token, 403 missing w_member_social or w_organization_social scope, 429 throttle, 400 bad payload) or fails before a response (timeout, DNS, connection refused).
Common situations: LinkedIn access token expired (60-day lifetime); app missing the required marketing/community product access; posting as an organization without admin rights; LinkedIn throttling (429) during batch posting; REST API version header mismatch after API version updates; corporate proxy blocking api.linkedin.com.
Related errors
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/1b78e16c56e15995.
Report an issue: GitHub.