GUIDES for developers

Gan Jing World — Video Upload API Guide

This guide describes the complete workflow for programmatically uploading video content to a Gan Jing World channel account using the Platform API.


Overview

Uploading a video to Gan Jing World is an 8-step process that spans three API hosts:

HostPurposeAuthorization
gw.ganjingworld.comPlatform API — authentication, content managementRaw ACCESS_TOKEN (no Bearer prefix)
imgapi.cloudokyo.cloudImage CDN — thumbnail upload and resizingBearer <UPLOAD_TOKEN>
vodapi.cloudokyo.cloudVOD CDN — video upload (TUS protocol) and statusBearer <UPLOAD_TOKEN>

Important: The Platform API (gw.ganjingworld.com) expects the raw access token in the Authorization header. The CDN APIs (cloudokyo.cloud) expect a Bearer prefix. Mixing these up will cause 401 errors.

Workflow Summary

1. Authenticate        →  Obtain ACCESS_TOKEN + REFRESH_TOKEN
2. Get Upload Token    →  Exchange ACCESS_TOKEN for UPLOAD_TOKEN
3. Upload Thumbnail    →  Upload image, receive resized URLs
4. Create Content      →  Create video entry, receive CONTENT_ID
5. Register Upload ID  →  Associate a TUS upload ID with the content
6. Upload Video (TUS)  →  Resumable chunked upload, receive VIDEO_ID
7. Poll Status         →  Wait for transcoding to complete
8. Finalize Content    →  Notify the platform that the video is ready

Step 1: Authentication

Initial Login

Obtain your ACCESS_TOKEN and REFRESH_TOKEN through the Gan Jing World login flow. These tokens are typically extracted from the browser session after logging into your channel account.

Please log in or create a new account to get the token.

Token Refresh

Access tokens expire after approximately 10 days. Refresh tokens remain valid for 3 months. Use the refresh endpoint to obtain a new access token before it expires.

curl -X POST 'https://gw.ganjingworld.com/v1.1/auth/token/refresh' \
  -H 'Content-Type: application/json' \
  -d '{"refresh_token": "<REFRESH_TOKEN>"}'

Response:

{
  "access_token": "<NEW_ACCESS_TOKEN>",
  "refresh_token": "<NEW_REFRESH_TOKEN>",
  "expires_in": 864000
}

Note: The response may wrap tokens in a body or data field depending on the API version. Check data.access_token, body.access_token, and the top-level access_token field. The expires_in value is in seconds (864000 = 10 days). Always store and use the new refresh token from the response, as the previous one may be invalidated.

Best Practice: Implement proactive token refresh — refresh the token when it is within 1 hour of expiry rather than waiting for a 401 error. If you do receive a 401, refresh the token and retry the request once.


Step 2: Get Upload Token (VOD Token)

Exchange your access token for a short-lived upload token used with the CDN APIs.

curl -X GET 'https://gw.ganjingworld.com/v1.0c/get-vod-token' \
  -H 'Authorization: <ACCESS_TOKEN>' \
  -H 'Accept: application/json'

Note: The Authorization header uses the raw access token — do NOT include a Bearer prefix.

Response:

{
  "body": {
    "token": "<UPLOAD_TOKEN>"
  },
  "status_code": 200
}

Note: The token may appear at different paths in the response: body.token, data.token, body.upload_token, or body.vod_token. Check multiple paths for robustness.

The UPLOAD_TOKEN is used for all subsequent CDN operations (thumbnail upload, video upload, and status polling).


Step 3: Upload Thumbnail

Upload a high-quality thumbnail image. The CDN will automatically generate resized versions.

curl -X POST 'https://imgapi.cloudokyo.cloud/api/v2/image' \
  -H 'Authorization: Bearer <UPLOAD_TOKEN>' \
  -H 'resizing-list: 140,240,360,380,480,580,672,960,1280,1920' \
  -F 'file=@"thumbnail.jpg"'

Corrections from previous documentation:

  • The endpoint is /api/v2/image (not /api/v1/image).
  • Only send the file field in the multipart form — do NOT include a name field.
  • Authorization uses Bearer <UPLOAD_TOKEN>.

Response:

{
  "body": {
    "image_id": "abc123",
    "image_url": [
      "https://image5-us-west.cloudokyo.cloud/.../140.jpg",
      "https://image5-us-west.cloudokyo.cloud/.../240.webp",
      "https://image5-us-west.cloudokyo.cloud/.../480.jpg",
      "https://image5-us-west.cloudokyo.cloud/.../480.webp",
      "https://image5-us-west.cloudokyo.cloud/.../672.webp",
      "https://image5-us-west.cloudokyo.cloud/.../960.webp",
      "https://image5-us-west.cloudokyo.cloud/.../1280.webp",
      "https://image5-us-west.cloudokyo.cloud/.../1920.webp",
      "https://image5-us-west.cloudokyo.cloud/.../origin.webp"
    ]
  },
  "file_upload": "abc123",
  "status_code": 200
}

Selecting Poster URLs

From the image_url array, select two URLs for use in the next step:

FieldSize KeyPurpose
poster_url672.webpStandard definition poster (used in listings)
poster_hd_urlorigin.webpHigh definition poster (used on video page)

Fallback order for poster_url: 672 → 480 → 580 → 360 → 240 → 140 Fallback order for poster_hd_url: origin → 1920 → 1280 → 960 → 672

Prefer .webp over .jpg when both are available at the same size.


Step 4: Create Content Entry

Create the video content entry with metadata and thumbnail URLs. This returns the CONTENT_ID that identifies this video throughout the rest of the pipeline.

curl -X POST 'https://gw.ganjingworld.com/v1.0c/add-content' \
  -H 'Authorization: <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "user_id2": "<CHANNEL_ID>",
    "type": "Video",
    "lang": "en-US",
    "category_id": "cat13",
    "title": "My Video Title",
    "description": "Video description here",
    "visibility": "public",
    "poster_url": "https://image5-us-west.cloudokyo.cloud/.../672.webp",
    "poster_hd_url": "https://image5-us-west.cloudokyo.cloud/.../origin.webp",
    "made_for_kids": false,
    "user_made_for_kids": false
  }'

Corrections from previous documentation:

  • Do NOT include "mode": "draft". Omitting this field allows the video to auto-publish after transcoding completes. Including "mode": "draft" will leave the video in an unpublished state.
  • The made_for_kids and user_made_for_kids boolean fields are required.
  • Authorization uses the raw access token (no Bearer prefix).

Request body fields:

FieldTypeRequiredDescription
user_id2stringYesYour GJW channel ID
typestringYesAlways "Video"
langstringYesLanguage code (e.g., "en-US")
category_idstringYesContent category (e.g., "cat13")
titlestringYesVideo title
descriptionstringYesVideo description
visibilitystringYes"public" or "private"
poster_urlstringYesStandard poster URL (from Step 3)
poster_hd_urlstringYesHD poster URL (from Step 3)
made_for_kidsbooleanYesWhether content is made for children
user_made_for_kidsbooleanYesWhether the creator marked it as for children

Response:

{
  "content_id": "<CONTENT_ID>",
  "owner_id": "<CHANNEL_ID>",
  "type": "Video",
  "title": "My Video Title",
  "visibility": "public",
  "created_at": "2025-01-15T10:30:00Z"
}

Note: The content_id may also appear at data.content_id, data.id, or id depending on the response structure.


Step 5: Register Upload ID

Before starting the TUS upload, register a unique upload identifier with the platform. This associates the upcoming video file with the content entry.

curl -X POST 'https://gw.ganjingworld.com/v1.1/content/upload-id' \
  -H 'Authorization: <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "content_id": "<CONTENT_ID>",
    "type": "Studio_TUS",
    "upload_id": "<UPLOAD_ID>"
  }'

This step was missing from previous documentation. Without it, the TUS upload will succeed but the video may not be correctly linked to the content entry.

UPLOAD_ID should be a random 32-character hexadecimal string (e.g., generated via crypto.randomBytes(16).toString('hex')).


Step 6: Upload Video via TUS Protocol

Video files are uploaded using the TUS resumable upload protocol, not as a simple multipart form upload.

Correction from previous documentation: The video upload endpoint is a TUS endpoint (/api/v1/tus/upload), NOT a standard form upload endpoint (/api/v1/video). The previous documentation showed a multipart form upload which is not the current upload method.

TUS Upload Flow

1. Create upload (POST):

POST https://vodapi.cloudokyo.cloud/api/v1/tus/upload
Authorization: Bearer <UPLOAD_TOKEN>
Tus-Resumable: 1.0.0
Upload-Length: <FILE_SIZE_IN_BYTES>
Upload-Metadata: <base64-encoded metadata pairs>
Content-Type: application/offset+octet-stream

TUS Metadata fields (each value is base64-encoded):

KeyValueDescription
languageen-USContent language
content_id<CONTENT_ID>From Step 4
content_typeVideoAlways Video
channel_id<CHANNEL_ID>Your user_id2
analyze_flagtrueEnable content analysis
filetypevideo/mp4MIME type of the video
auto_caption_flagtrueEnable automatic captioning
caption_prioritylowCaption processing priority
filenamevideo.mp4Original filename
profile_namefullbitrateEncoding profile
preview_start00:00:00Preview clip start time
preview_end00:00:20Preview clip end time
prioritylowProcessing priority

2. Upload chunks (PATCH):

Send the file in chunks (recommended: 10 MB per chunk) to the URL returned in the Location header from the POST response.

PATCH <UPLOAD_URL>
Authorization: Bearer <UPLOAD_TOKEN>
Tus-Resumable: 1.0.0
Upload-Offset: <BYTE_OFFSET>
Content-Type: application/offset+octet-stream
Content-Length: <CHUNK_SIZE>

<binary chunk data>

3. Handle errors:

StatusMeaningAction
204Chunk acceptedContinue with next chunk
409Offset mismatchSend HEAD request to get current offset, then resume from there
423Upload lockedWait and retry (the server is processing a previous chunk)
5xxServer errorRetry with exponential backoff

Recommended retry delays: 0s, 1s, 3s, 5s, 10s, 20s

4. Retrieve Video ID (GET):

After the TUS upload completes (all bytes accepted), GET the upload URL to retrieve the VIDEO_ID:

curl 'https://vodapi.cloudokyo.cloud/api/v1/tus/upload/<UPLOAD_RESOURCE_ID>' \
  -H 'Authorization: Bearer <UPLOAD_TOKEN>' \
  -H 'Accept: application/json'

Response:

{
  "body": {
    "video_id": "<VIDEO_ID>",
    "filename": "video.mp4"
  },
  "status_code": 200
}

Using tus-js-client

If you are building with JavaScript/TypeScript, the tus-js-client library (v4.x) handles the TUS protocol automatically:

import * as tus from "tus-js-client";

const upload = new tus.Upload(fileBuffer, {
  endpoint: "https://vodapi.cloudokyo.cloud/api/v1/tus/upload",
  chunkSize: 10 * 1024 * 1024, // 10 MB
  retryDelays: [0, 1000, 3000, 5000, 10000, 20000],
  metadata: {
    content_id: CONTENT_ID,
    content_type: "Video",
    channel_id: CHANNEL_ID,
    filetype: "video/mp4",
    filename: "video.mp4",
    language: "en-US",
    analyze_flag: "true",
    auto_caption_flag: "true",
    caption_priority: "low",
    profile_name: "fullbitrate",
    preview_start: "00:00:00",
    preview_end: "00:00:20",
    priority: "low"
  },
  headers: {
    Authorization: `Bearer ${UPLOAD_TOKEN}`
  },
  onProgress(bytesUploaded, bytesTotal) {
    const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
    console.log(`Upload progress: ${percentage}%`);
  },
  onSuccess() {
    console.log("Upload complete! URL:", upload.url);
    // GET upload.url to retrieve the video_id
  },
  onError(error) {
    console.error("Upload failed:", error);
  }
});

upload.start();

Step 7: Poll Processing Status

After the video is uploaded, the server transcodes it into HLS streaming format. Poll the status endpoint until processing completes.

curl 'https://vodapi.cloudokyo.cloud/api/v1/status/<VIDEO_ID>' \
  -H 'Authorization: Bearer <UPLOAD_TOKEN>' \
  -H 'Accept: application/json'

In-progress response:

{
  "body": {
    "video_id": "<VIDEO_ID>",
    "filename": "video.mp4",
    "progress": 45,
    "status": "in_progress"
  },
  "status_code": 200
}

Completed response:

{
  "body": {
    "video_id": "<VIDEO_ID>",
    "url": "https://video1-us-east.cloudokyo.cloud/.../master.m3u8",
    "duration_sec": "125.500000",
    "width": 1920,
    "height": 1080,
    "loudness": "-14.2 LUFS",
    "thumb": {
      "base_url": "...",
      "sizes": "..."
    }
  },
  "status_code": 200
}

Completion Detection

Important: Do NOT rely on status strings to detect completion. The video is ready when body.url is truthy — i.e., it contains an .m3u8 HLS manifest URL. The status field values are not guaranteed to be consistent.

Recommended polling strategy:

  • Maximum 120 attempts (10 minutes timeout)
  • Check body.url for truthiness on each response
  • If status is "error" or "failed", stop polling and report the failure

Published Video URL

After successful upload and processing, the video is accessible at:

https://www.ganjingworld.com/video/<CONTENT_ID>

Summary of Corrections

The following items were incorrect or missing in the previous developer documentation:

ItemPrevious (Incorrect)Corrected
Thumbnail endpoint/api/v1/image/api/v2/image
Thumbnail form fieldsfile + name: "thumbnail"file only (no name field)
Create content bodyIncluded "mode": "draft"Omit mode field (allows auto-publish)
Create content bodyMissing made_for_kids fieldsmade_for_kids and user_made_for_kids are required
Video upload methodMultipart form to /api/v1/videoTUS resumable upload to /api/v1/tus/upload
Upload ID registrationNot documentedStep 5 is required before TUS upload
Completion detectionCheck status stringCheck body.url for truthy .m3u8 URL
Content finalizationNot documentedStep 8: POST /v1.0c/update-content
Response structuredata fieldResponses wrap in body field (not data)
Platform API authNot clearly specifiedRaw token (no Bearer prefix)
CDN API authNot clearly specifiedBearer <UPLOAD_TOKEN>
Token refresh endpoint/v1.0c/auth/refresh with {"token": ...}/v1.1/auth/token/refresh with {"refresh_token": ...}

Error Handling Best Practices

  1. Token Expiry: Proactively refresh tokens within 1 hour of expiry. Implement 401 retry as a safety net.
  2. Upload Failures: The TUS protocol supports resume — on network failures, send a HEAD request to get the current offset and resume from there.
  3. 409 Conflict: During TUS upload, a 409 means the server's offset doesn't match yours. Send a HEAD request to re-sync.
  4. 423 Locked: The server is processing a previous chunk. Wait and retry.
  5. Processing Timeout: If polling exceeds 10 minutes, the transcoding may have failed silently. Check the status field for error indicators.
  6. Non-ASCII Filenames: Sanitize filenames to printable ASCII before including them in multipart headers or TUS metadata to avoid server-side parsing errors.
Copyright © 2026