Skip to main content
Developer Documentation

Multi-Platform Publishing API

Complete guides, code examples, and integration references for publishing videos, photo carousels, scheduled posts, and status tracking across TikTok, Instagram, Threads, and Facebook.

All public content endpoints require authentication via X-API-Key. Use this key only from a trusted server-side environment.

// Server-to-server authentication
X-API-Key: process.env.WAHDX_API_KEY

// Example key format
X-API-Key: sk_d8864c20XXXXXXxxxXXXxxXx

Get Your API Key

  1. Login to your dashboard
  2. Click the profile icon in the header
  3. Generate or view your API key
  4. Store it in your backend or serverless environment variables

Security: Never expose the API key in browser code, mobile apps, client bundles, or public repositories. Route requests through your backend or server-side proxy and rotate the key immediately if compromised.

Post to TikTok, Instagram, Threads, or Facebook in seconds from your backend integration layer. Use one platform and one account per request.

Send mediaItems as the recommended request format. For backward compatibility, the backend also accepts mediaUrls, media_urls, and direct string URLs inside the array. External media URLs are automatically proxied to a verified domain when needed.

// Server-side example only (Node.js, serverless function, or backend proxy)
const API_BASE_URL = 'https://api-tiktok.wahdx.co'; // Store in env on your server if preferred
const API_KEY = process.env.WAHDX_API_KEY;

await fetch(API_BASE_URL + '/api/content/post', {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    platform: 'instagram', // tiktok | instagram | threads | facebook
    accountId: 'YOUR_ACCOUNT_ID',
    content: 'Check out this video! #viral #fyp',
    mediaItems: [
      { url: 'https://your-domain.com/video.mp4' }
    ],
    instagramSettings: {
      share_to_feed: true
    }
  })
})

Every endpoint below requires authentication via X-API-Key. The public publish endpoint accepts exactly one platform and one accountId per request.

POST/api/content/postCreate post
GET/api/content/status/:accountId/:publishIdCheck status
POST/api/content/status/batchCheck multiple statuses
GET/api/content/creator-info/:accountIdGet permissions
GET/api/content/accountsList linked content accounts

This endpoint returns all linked content accounts across TikTok, Instagram, Threads, and Facebook. The server will return the full linked account list even for large users with more than 1000 accounts. Each item includes a platform field. Use that field together with accountId when calling /api/content/postor /api/content/status/:accountId/:publishId. Only /api/content/creator-info/:accountId remains TikTok-only.

For TikTok status lookups, append ?mediaType=video or ?mediaType=photo to let the API return a single public_post_url. Legacy requests without this query still work and return public_post_url_candidates when the post is already public.

POST /api/content/status/batch supports two request modes:

{ "checks": [{ "accountId": "ACCOUNT_ID", "publishId": "PUBLISH_ID", "mediaType": "video" }] }
{ "postId": "SCHEDULED_POST_ID" }

The unified publish endpoint supports single-video posts and photo posts. The backend detects the media type from file extensions and validates limits per platform.

Video

Single video post (3s - 10min)

Photo Carousel

Up to 35 photos on TikTok and up to 10 photos on Instagram

Important: You cannot mix photos and videos in the same post.

Accepted Video Formats

SpecificationRequirement
FormatTikTok: MP4, MOV, WebM. Instagram, Threads, Facebook: MP4, MOV, WebM, M4V, AVI
CodecH.264 (recommended)
Aspect Ratio9:16 (vertical)
Resolution1080x1920 (Full HD)
Duration3 seconds - 10 minutes
File SizeUp to 500MB
Frame Rate24-60 FPS

Video Cover (Thumbnail)

Customize which frame appears as the thumbnail:

{
  "tiktokSettings": {
    "video_cover_timestamp_ms": 3000
  }
}
  • • Value in milliseconds
  • • Default: 1000
  • • Must be within video duration
Content TypeTitle FieldDescription Field
Videocontent up to 4,000 characters-
Photo CarouselphotoTitle up to 90 characterscontent up to 4,000 characters

Tip: Use the photoTitle field for the main headline and content field for the longer description when posting photo carousels.

Let TikTok automatically add recommended music to photo carousels:

{
  "tiktokSettings": {
    "auto_add_music": true // Default: true for photos
  }
}

Note: This feature is only available for photo carousels.

Disclose AI-generated content to comply with TikTok policies:

{
  "tiktokSettings": {
    "video_made_with_ai": true
  }
}

When to use: Set to true if your content was created or significantly modified using AI tools.

Send content to Creator's TikTok Inbox as a draft instead of publishing immediately. Works for both video and photo posts.

Usage

Add draft: true to your tiktokSettings:

// Video Draft
{
  "accountId": "YOUR_ACCOUNT_ID",
  "content": "My video caption",
  "mediaItems": [
    { "url": "https://your-domain.com/video.mp4" }
  ],
  "tiktokSettings": {
    "draft": true
  }
}

// Photo Draft
{
  "accountId": "YOUR_ACCOUNT_ID",
  "content": "Photo description",
  "mediaItems": [
    { "url": "https://your-domain.com/photo1.jpg" },
    { "url": "https://your-domain.com/photo2.jpg" }
  ],
  "tiktokSettings": {
    "media_type": "photo",
    "draft": true,
    "privacy_level": "PUBLIC_TO_EVERYONE"
  }
}

How It Works

TypeBehaviorSettings
Video DraftSent to TikTok inboxTitle, privacy, etc. configured in TikTok app
Photo DraftSent to TikTok inboxAll settings sent via API

Video Draft Note: When sending a video to inbox, only the video file is uploaded. Title, privacy level, and other settings must be configured directly in the TikTok app before publishing.

Use case: Ideal for workflows requiring manual review before publishing, or for unaudited apps that cannot use Direct Post.

PUBLIC_TO_EVERYONE

Visible to everyone

MUTUAL_FOLLOW_FRIENDS

Visible to mutual followers

FOLLOWER_OF_CREATOR

Visible to followers only

SELF_ONLY

Private (only you)

These TikTok settings are optional. If omitted, the backend applies defaults such as privacy_level: SELF_ONLY, comments/duet/stitch remaining enabled unless explicitly set to false, and video_cover_timestamp_ms: 1000.

privacy_level- Defaults to SELF_ONLY when not provided
allow_comment- Comments stay enabled unless you set it to false
allow_duet- Duets stay enabled unless you set it to false (video only)
allow_stitch- Stitch stays enabled unless you set it to false (video only)
video_cover_timestamp_ms- Cover frame timestamp in milliseconds, default 1000

Invalid privacy_level

The privacy level must maintain or restrict the user's TikTok account settings. Check /api/content/creator-info/:accountId for allowed values on active TikTok accounts.

Cannot mix media types

A post must be either single video OR photo carousel. Mixing is not supported.

Video rejected

  • • Check duration (3s - 10min)
  • • Verify format (MP4 recommended)
  • • Ensure file size < 500MB

Title Truncated

Photo carousel titles are limited to 90 characters. Descriptions can be up to 4,000 characters.

⚡ Maximum Subscription Only

This feature is exclusively available for users with Maximum subscription plan. Other plans will receive a 403 error.

Connect TikTok accounts via QR code scan — no browser redirect needed.
The user scans the QR code with their TikTok app and confirms the authorization. This method is ideal if you want to integrate our platform into your own custom application, allowing your users to link their TikTok accounts seamlessly from within your app.

1️⃣

Request QR

POST /qr-login/request

2️⃣

Display QR

Show base64 image to user

3️⃣

Poll Status

POST /qr-login/check every 10s

4️⃣

Account Linked

Status: confirmed

1. Request QR Code

POST /api/content/qr-login/request

Request Body (optional)

{
  "unique_id": "my_session_12345"  // Optional, min 8 chars. Auto-generated if not provided.
}

Example Request

// Step 1: Request QR Code from your server-side integration layer
const API_BASE_URL = 'https://api-tiktok.wahdx.co'; // Store in env on your server if preferred
const API_KEY = process.env.WAHDX_API_KEY;

await fetch(API_BASE_URL + '/api/content/qr-login/request', {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    unique_id: 'my_session_12345' // Optional, min 8 chars
  })
})

Success Response (200)

{
  "success": true,
  "data": {
    "qrCodeImage": "data:image/png;base64,iVBORw0KGgo...",
    "token": "VJ5JCKGJGRSWNMFWHQH4W5NKY943Q97D",
    "unique_id": "my_session_12345",
    "expiresIn": 300
  }
}

Subscription Error (403)

{
  "error": "QR Code login is only available for Maximum subscription",
  "subscription": "free"
}

2. Check QR Status

POST /api/content/qr-login/check

Request Body

{
  "token": "VJ5JCKGJGRSWNMFWHQH4W5NKY943Q97D"  // Required: token from Step 1
}

Example Request

// Step 2: Poll QR Status from your server-side integration layer
const API_BASE_URL = 'https://api-tiktok.wahdx.co'; // Store in env on your server if preferred
const API_KEY = process.env.WAHDX_API_KEY;

await fetch(API_BASE_URL + '/api/content/qr-login/check', {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    token: 'TOKEN_FROM_STEP_1'
  })
})

Response by Status

new — QR code not yet scanned

{ "success": true, "data": { "status": "new", "unique_id": "my_session_12345" } }

scanned — User has scanned, awaiting confirmation

{ "success": true, "data": { "status": "scanned", "unique_id": "my_session_12345" } }

confirmed — Account successfully linked

{
  "success": true,
  "data": {
    "status": "confirmed",
    "unique_id": "my_session_12345",
    "account": {
      "accountId": "8abd72f0-4ede-441c-9c17-XXXXXXXXX",
      "display_name": "Creator Name",
      "username": "creator_username"
    }
  }
}

expired — QR code has expired, request a new one

{ "success": true, "data": { "status": "expired", "unique_id": "my_session_12345" } }

utilised — Auth code has already been used

{ "success": true, "data": { "status": "utilised", "unique_id": "...", "message": "QR code has already been used." } }

3. Check List Account

GET /api/content/accounts

Response List Account

{
  "success": true,
  "subscription": "maximum",
  "data": [
    {
      "accountId": "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX",
      "platform": "tiktok",
      "display_name": "Account 1",
      "username": "account_1",
      "avatar_url": "https://p16-sign-va.tiktokcdn.com/tos-...",
      "bio_description": "My awesome bio",
      "status": "active",
      "followers": 1500,
      "following": 300,
      "likes": 12500,
      "videos": 42,
      "is_verified": false
    },
    {
      "accountId": "8abd72f0-xxxx-xxxx-xxxx-XXXXXXXXXXX",
      "platform": "instagram",
      "display_name": "Brand IG",
      "username": "brand_ig",
      "avatar_url": "https://instagram.com/avatar.jpg",
      "bio_description": "Lifestyle brand",
      "status": "active",
      "followers": 9800,
      "following": 210,
      "likes": null,
      "videos": 325,
      "is_verified": null
    },
    {
      "accountId": "c9152d30-xxxx-xxxx-xxxx-XXXXXXXXXXX",
      "platform": "threads",
      "display_name": "Brand Threads",
      "username": "brand_threads",
      "avatar_url": "https://threads.net/avatar.jpg",
      "bio_description": "Realtime updates",
      "status": "active",
      "followers": 4300,
      "following": 180,
      "likes": null,
      "videos": null,
      "is_verified": null
    },
    {
      "accountId": "23369e89-xxxx-xxxx-xxxx-XXXXXXXXXXX",
      "platform": "facebook",
      "display_name": "Brand Page",
      "username": "123456789012345",
      "avatar_url": "https://facebook.com/page-avatar.jpg",
      "bio_description": "Official page",
      "status": "expired",
      "followers": 24000,
      "following": null,
      "likes": 22100,
      "videos": null,
      "is_verified": null
    }
  ]
}

Complete Example

// Complete QR Login Flow (server-side example)
async function loginWithQR() {
  const API_BASE_URL = 'https://api-tiktok.wahdx.co'; // Store in env on your server if preferred
  const API_KEY = process.env.WAHDX_API_KEY;

  // Step 1: Request QR Code
  const qr = await fetch(API_BASE_URL + '/api/content/qr-login/request', {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ unique_id: 'session_' + Date.now() })
  }).then(r => r.json());

  // Display QR: <img src={qr.data.qrCodeImage} />
  console.log('Scan this QR code with TikTok app');

  // Step 2: Poll status every 10 seconds
  const poll = setInterval(async () => {
    const check = await fetch(API_BASE_URL + '/api/content/qr-login/check', {
      method: 'POST',
      headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: qr.data.token })
    }).then(r => r.json());

    if (check.data.status === 'confirmed') {
      clearInterval(poll);
      console.log('Account linked!', check.data.account);
    } else if (['expired', 'utilised'].includes(check.data.status)) {
      clearInterval(poll);
      console.log('QR expired, request a new one');
    }
  }, 10000);
}

POST /api/content/post (TikTok direct post)

{
  "success": true,
  "accountId": "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX",
  "platform": "tiktok",
  "status": "PUBLISH_INITIATED",
  "data": {
    "publish_id": "v_pub_file~v2.1234567890"
  },
  "mediaType": "video",
  "mediaUrls": [
    "https://media.wahdx.co/uploads/abc123.mp4"
  ],
  "fileIds": ["uploads/abc123.mp4"]
}

POST /api/content/post (Instagram, Threads, Facebook)

{
  "success": true,
  "accountId": "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX",
  "platform": "instagram",
  "status": "PUBLISH_COMPLETE",
  "data": {
    "id": "17893746543210000",
    "publish_id": "17893746543210000",
    "public_post_url": "https://www.instagram.com/p/ABC123XYZ/"
  },
  "mediaType": "video",
  "mediaUrls": [
    "https://media.wahdx.co/uploads/abc123.mp4"
  ],
  "fileIds": ["uploads/abc123.mp4"],
  "public_post_url": "https://www.instagram.com/p/ABC123XYZ/"
}

GET /api/content/status/:accountId/:publishId?mediaType=video

Use the mediaType query for TikTok so the backend can return a ready-to-open public_post_url.

{
  "success": true,
  "accountId": "tiktok-account-id",
  "platform": "tiktok",
  "data": {
    "publish_id": "v_pub_file~v2.1234567890",
    "platform": "tiktok",
    "status": "PUBLISH_COMPLETE",
    "uploaded_bytes": "5821041",
    "public_post_url": "https://www.tiktok.com/@mybrand/video/7489123456789012345",
    "data": {
      "publicaly_available_post_id": ["7489123456789012345"]
    }
  }
}

GET /api/content/status/:accountId/:publishId (legacy TikTok request)

If you omit mediaType, TikTok status checks still work. The response includes candidate URLs for backward compatibility instead of a single public_post_url.

{
  "success": true,
  "accountId": "tiktok-account-id",
  "platform": "tiktok",
  "data": {
    "publish_id": "v_pub_file~v2.1234567890",
    "platform": "tiktok",
    "status": "PUBLISH_COMPLETE",
    "public_post_url": null,
    "public_post_url_candidates": {
      "video": "https://www.tiktok.com/@mybrand/video/7489123456789012345",
      "photo": "https://www.tiktok.com/@mybrand/photo/7489123456789012345"
    },
    "data": {
      "publicaly_available_post_id": ["7489123456789012345"]
    }
  }
}

POST /api/content/status/batch

{
  "success": true,
  "data": {
    "results": [
      {
        "accountId": "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX",
        "platform": "instagram",
        "publishId": "17893746543210000",
        "success": true,
        "publish_id": "17893746543210000",
        "status": "PUBLISH_COMPLETE",
        "public_post_url": "https://www.instagram.com/p/ABC123XYZ/"
      },
      {
        "accountId": "missing-account-id",
        "publishId": "missing-publish-id",
        "success": false,
        "error": "Account not found"
      }
    ],
    "byAccountId": {
      "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX": [
        {
          "accountId": "aada5586-xxxx-xxxx-xxxx-XXXXXXXXXXX",
          "platform": "instagram",
          "publishId": "17893746543210000",
          "success": true,
          "publish_id": "17893746543210000",
          "status": "PUBLISH_COMPLETE",
          "public_post_url": "https://www.instagram.com/p/ABC123XYZ/"
        }
      ]
    }
  }
}
{
  "success": true,
  "data": {
    "results": [
      {
        "accountId": "tiktok-account-id",
        "platform": "tiktok",
        "publishId": "v_pub_file~v2.1234567890",
        "success": true,
        "publish_id": "v_pub_file~v2.1234567890",
        "status": "PUBLISH_COMPLETE",
        "public_post_url": "https://www.tiktok.com/@mybrand/video/7489123456789012345"
      },
      {
        "accountId": "legacy-tiktok-account-id",
        "platform": "tiktok",
        "publishId": "v_pub_file~v2.99887766",
        "success": true,
        "publish_id": "v_pub_file~v2.99887766",
        "status": "PUBLISH_COMPLETE",
        "public_post_url": null,
        "public_post_url_candidates": {
          "video": "https://www.tiktok.com/@mybrand/video/7489123456789012345",
          "photo": "https://www.tiktok.com/@mybrand/photo/7489123456789012345"
        }
      }
    ]
  }
}

For batch status checks, you can send either checks or postId. For TikTok inside checks, add mediaType per item if you want a single public_post_url; otherwise legacy requests still return public_post_url_candidates.

// Alternative batch mode: resolve publish IDs from a stored post
const API_BASE_URL = 'https://api-tiktok.wahdx.co'; // Store in env on your server if preferred
const API_KEY = process.env.WAHDX_API_KEY;

await fetch(API_BASE_URL + '/api/content/status/batch', {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    postId: 'SCHEDULED_POST_ID'
  })
})