This guide describes the complete workflow for programmatically uploading video content to a Gan Jing World channel account using the Platform API.
Uploading a video to Gan Jing World is an 8-step process that spans three API hosts:
| Host | Purpose | Authorization |
|---|---|---|
gw.ganjingworld.com | Platform API — authentication, content management | Raw ACCESS_TOKEN (no Bearer prefix) |
imgapi.cloudokyo.cloud | Image CDN — thumbnail upload and resizing | Bearer <UPLOAD_TOKEN> |
vodapi.cloudokyo.cloud | VOD CDN — video upload (TUS protocol) and status | Bearer <UPLOAD_TOKEN> |
Important: The Platform API (
gw.ganjingworld.com) expects the raw access token in theAuthorizationheader. The CDN APIs (cloudokyo.cloud) expect aBearerprefix. Mixing these up will cause 401 errors.
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
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.
Sign InAccess 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
bodyordatafield depending on the API version. Checkdata.access_token,body.access_token, and the top-levelaccess_tokenfield. Theexpires_invalue 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.
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
Authorizationheader uses the raw access token — do NOT include aBearerprefix.
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, orbody.vod_token. Check multiple paths for robustness.
The UPLOAD_TOKEN is used for all subsequent CDN operations (thumbnail upload, video upload, and status polling).
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
filefield in the multipart form — do NOT include anamefield.- 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
}
From the image_url array, select two URLs for use in the next step:
| Field | Size Key | Purpose |
|---|---|---|
poster_url | 672.webp | Standard definition poster (used in listings) |
poster_hd_url | origin.webp | High 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.
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_kidsanduser_made_for_kidsboolean fields are required.- Authorization uses the raw access token (no
Bearerprefix).
Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
user_id2 | string | Yes | Your GJW channel ID |
type | string | Yes | Always "Video" |
lang | string | Yes | Language code (e.g., "en-US") |
category_id | string | Yes | Content category (e.g., "cat13") |
title | string | Yes | Video title |
description | string | Yes | Video description |
visibility | string | Yes | "public" or "private" |
poster_url | string | Yes | Standard poster URL (from Step 3) |
poster_hd_url | string | Yes | HD poster URL (from Step 3) |
made_for_kids | boolean | Yes | Whether content is made for children |
user_made_for_kids | boolean | Yes | Whether 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_idmay also appear atdata.content_id,data.id, oriddepending on the response structure.
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')).
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.
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):
| Key | Value | Description |
|---|---|---|
language | en-US | Content language |
content_id | <CONTENT_ID> | From Step 4 |
content_type | Video | Always Video |
channel_id | <CHANNEL_ID> | Your user_id2 |
analyze_flag | true | Enable content analysis |
filetype | video/mp4 | MIME type of the video |
auto_caption_flag | true | Enable automatic captioning |
caption_priority | low | Caption processing priority |
filename | video.mp4 | Original filename |
profile_name | fullbitrate | Encoding profile |
preview_start | 00:00:00 | Preview clip start time |
preview_end | 00:00:20 | Preview clip end time |
priority | low | Processing 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:
| Status | Meaning | Action |
|---|---|---|
| 204 | Chunk accepted | Continue with next chunk |
| 409 | Offset mismatch | Send HEAD request to get current offset, then resume from there |
| 423 | Upload locked | Wait and retry (the server is processing a previous chunk) |
| 5xx | Server error | Retry 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
}
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();
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
}
Important: Do NOT rely on
statusstrings to detect completion. The video is ready whenbody.urlis truthy — i.e., it contains an.m3u8HLS manifest URL. Thestatusfield values are not guaranteed to be consistent.
Recommended polling strategy:
body.url for truthiness on each responsestatus is "error" or "failed", stop polling and report the failureAfter successful upload and processing, the video is accessible at:
https://www.ganjingworld.com/video/<CONTENT_ID>
The following items were incorrect or missing in the previous developer documentation:
| Item | Previous (Incorrect) | Corrected |
|---|---|---|
| Thumbnail endpoint | /api/v1/image | /api/v2/image |
| Thumbnail form fields | file + name: "thumbnail" | file only (no name field) |
| Create content body | Included "mode": "draft" | Omit mode field (allows auto-publish) |
| Create content body | Missing made_for_kids fields | made_for_kids and user_made_for_kids are required |
| Video upload method | Multipart form to /api/v1/video | TUS resumable upload to /api/v1/tus/upload |
| Upload ID registration | Not documented | Step 5 is required before TUS upload |
| Completion detection | Check status string | Check body.url for truthy .m3u8 URL |
| Content finalization | Not documented | Step 8: POST /v1.0c/update-content |
| Response structure | data field | Responses wrap in body field (not data) |
| Platform API auth | Not clearly specified | Raw token (no Bearer prefix) |
| CDN API auth | Not clearly specified | Bearer <UPLOAD_TOKEN> |
| Token refresh endpoint | /v1.0c/auth/refresh with {"token": ...} | /v1.1/auth/token/refresh with {"refresh_token": ...} |
status field for error indicators.