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

  1. Inspect the thrown LinkedInPlatformException's cause (platformMessage, httpStatus, platformCode) — LinkedIn includes a detailed message and serviceErrorCode in the body.
  2. If httpStatus is 401, refresh the LinkedIn access token (or re-run the OAuth flow) and retry.
  3. If 403, verify the app has requested the required products/scopes (e.g. 'Share on LinkedIn') and that the member/organization has post permission.
  4. If 429 or retryable network/5xx, back off and retry respecting Retry-After.
  5. 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

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.