Upload Content
curl --request POST \
--url https://api.shortkit.dev/v1/content/upload \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contentType": "<string>",
"title": "<string>",
"description": "<string>",
"tags": [
"<string>"
],
"section": "<string>",
"author": "<string>",
"customMetadata": {},
"protected": true,
"origin": "<string>",
"publishAt": "<string>",
"expiresAt": "<string>",
"images": [
{}
],
"autoScrollInterval": 123,
"callbackUrl": "<string>"
}
'import requests
url = "https://api.shortkit.dev/v1/content/upload"
payload = {
"contentType": "<string>",
"title": "<string>",
"description": "<string>",
"tags": ["<string>"],
"section": "<string>",
"author": "<string>",
"customMetadata": {},
"protected": True,
"origin": "<string>",
"publishAt": "<string>",
"expiresAt": "<string>",
"images": [{}],
"autoScrollInterval": 123,
"callbackUrl": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
contentType: '<string>',
title: '<string>',
description: '<string>',
tags: ['<string>'],
section: '<string>',
author: '<string>',
customMetadata: {},
protected: true,
origin: '<string>',
publishAt: '<string>',
expiresAt: '<string>',
images: [{}],
autoScrollInterval: 123,
callbackUrl: '<string>'
})
};
fetch('https://api.shortkit.dev/v1/content/upload', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.shortkit.dev/v1/content/upload",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'contentType' => '<string>',
'title' => '<string>',
'description' => '<string>',
'tags' => [
'<string>'
],
'section' => '<string>',
'author' => '<string>',
'customMetadata' => [
],
'protected' => true,
'origin' => '<string>',
'publishAt' => '<string>',
'expiresAt' => '<string>',
'images' => [
[
]
],
'autoScrollInterval' => 123,
'callbackUrl' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.shortkit.dev/v1/content/upload"
payload := strings.NewReader("{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.shortkit.dev/v1/content/upload")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.shortkit.dev/v1/content/upload")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyContent
Upload Content
Upload a video or carousel directly. Returns signed URLs for the client to upload media files to.
POST
/
v1
/
content
/
upload
Upload Content
curl --request POST \
--url https://api.shortkit.dev/v1/content/upload \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contentType": "<string>",
"title": "<string>",
"description": "<string>",
"tags": [
"<string>"
],
"section": "<string>",
"author": "<string>",
"customMetadata": {},
"protected": true,
"origin": "<string>",
"publishAt": "<string>",
"expiresAt": "<string>",
"images": [
{}
],
"autoScrollInterval": 123,
"callbackUrl": "<string>"
}
'import requests
url = "https://api.shortkit.dev/v1/content/upload"
payload = {
"contentType": "<string>",
"title": "<string>",
"description": "<string>",
"tags": ["<string>"],
"section": "<string>",
"author": "<string>",
"customMetadata": {},
"protected": True,
"origin": "<string>",
"publishAt": "<string>",
"expiresAt": "<string>",
"images": [{}],
"autoScrollInterval": 123,
"callbackUrl": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
contentType: '<string>',
title: '<string>',
description: '<string>',
tags: ['<string>'],
section: '<string>',
author: '<string>',
customMetadata: {},
protected: true,
origin: '<string>',
publishAt: '<string>',
expiresAt: '<string>',
images: [{}],
autoScrollInterval: 123,
callbackUrl: '<string>'
})
};
fetch('https://api.shortkit.dev/v1/content/upload', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.shortkit.dev/v1/content/upload",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'contentType' => '<string>',
'title' => '<string>',
'description' => '<string>',
'tags' => [
'<string>'
],
'section' => '<string>',
'author' => '<string>',
'customMetadata' => [
],
'protected' => true,
'origin' => '<string>',
'publishAt' => '<string>',
'expiresAt' => '<string>',
'images' => [
[
]
],
'autoScrollInterval' => 123,
'callbackUrl' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.shortkit.dev/v1/content/upload"
payload := strings.NewReader("{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.shortkit.dev/v1/content/upload")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.shortkit.dev/v1/content/upload")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"contentType\": \"<string>\",\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"tags\": [\n \"<string>\"\n ],\n \"section\": \"<string>\",\n \"author\": \"<string>\",\n \"customMetadata\": {},\n \"protected\": true,\n \"origin\": \"<string>\",\n \"publishAt\": \"<string>\",\n \"expiresAt\": \"<string>\",\n \"images\": [\n {}\n ],\n \"autoScrollInterval\": 123,\n \"callbackUrl\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyThis is the primary way to get content into ShortKit. Send metadata and get back signed upload URLs — your client uploads files directly to cloud storage, no proxying through your server.
This endpoint accepts both publishable keys and secret keys, making it safe to call directly from mobile apps and browsers.
Once the upload completes, ShortKit automatically begins processing. The content’s
Images can be uploaded in parallel. Max 10 MB per image. Once all images are uploaded, the carousel is automatically validated and marked as
Repeat for each video you want to add (up to 10). Videos appear in the carousel in the order they are added. Use
2.
Building client-side uploads? A guide for integrating direct uploads into your app is coming soon.
Automatic captions and language detection. ShortKit generates captions for every video automatically — no configuration needed. Once processing completes, the detected language is stored as
customMetadata.language (BCP 47 code, e.g. "en", "es"). You can use this field to filter the feed by language.Request body
string
required
"video", "carousel", or "video_carousel".string
required
Display title. Max 200 characters.
string
Optional description. Max 2000 characters.
string[]
Tags for categorization and filtering.
string
Editorial section (e.g.,
"Sports", "News"). Max 200 characters.string
Content author name. Max 200 characters.
object
Arbitrary key-value pairs passed through to the feed.
boolean
When
true, playback is gated behind a short-lived signed streaming URL and the item’s streamingUrl is withheld until one is issued. When omitted, defaults to your organization’s default. See Protected Content.string
default:"other"
"ios_upload" or "other". Set to "ios_upload" for videos recorded on iOS devices to enable color-accurate thumbnail extraction. iOS HEVC recordings frequently omit color metadata, which can cause default first-frame thumbnails to render with incorrect colors. Marking the origin enables an alternate extraction path that infers the correct color space.Only applies to contentType: "video". No effect for image or video carousels. If you also render this content in a custom feed via the SDK, pair with the origin field on FeedInput.video.string
ISO 8601 timestamp. Content won’t appear in the feed before this time.
string
ISO 8601 timestamp. Content is removed from the feed after this time.
object[]
Carousel only. Defines the image slots for the carousel. Each object can include:
position(integer) — Display order (0-indexed). Defaults to array index.altText(string) — Accessibility text for the image.
number
Carousel only. Seconds between automatic image transitions.
string
A webhook URL that ShortKit will POST to as the content moves through the processing pipeline. Applies to
"video" and "carousel" content types. See Callback events for payload shape and delivery behavior.Uploading a video
Send aPOST with contentType: "video" and your metadata. The response includes a signed uploadUrl — your client PUTs the video file directly to that URL.
- 1. Create upload
- 2. Upload the file
curl -X POST https://api.shortkit.dev/v1/content/upload \
-H "X-API-Key: pk_live_your_publishable_key" \
-H "Content-Type: application/json" \
-d '{
"contentType": "video",
"title": "Morning Headlines",
"section": "News",
"tags": ["news", "morning"]
}'
{
"data": {
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"contentType": "video",
"uploadUrl": "https://uploads.shortkit.dev/direct/abc123...?token=eyJhbG...",
"uploadStatus": "pending",
"title": "Morning Headlines",
"section": "News",
"tags": ["news", "morning"],
"createdAt": "2026-02-05T12:00:00Z"
},
"meta": {
"request_id": "req_upload001"
}
}
# PUT the video file directly to the signed URL
curl -X PUT "https://uploads.shortkit.dev/direct/abc123...?token=eyJhbG..." \
-H "Content-Type: video/mp4" \
--data-binary @video.mp4
uploadStatus transitions from pending → processing → ready.Uploading a carousel
For carousels, specify the image slots you need in theimages array. The response returns a signed upload URL for each slot.
- 1. Create upload
- 2. Upload each image
curl -X POST https://api.shortkit.dev/v1/content/upload \
-H "X-API-Key: pk_live_your_publishable_key" \
-H "Content-Type: application/json" \
-d '{
"contentType": "carousel",
"title": "Top 5 Weekend Destinations",
"section": "Travel",
"autoScrollInterval": 5.0,
"images": [
{"position": 0, "altText": "Beach sunset"},
{"position": 1, "altText": "Mountain vista"},
{"position": 2, "altText": "City skyline"}
]
}'
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"contentType": "carousel",
"uploadStatus": "pending",
"title": "Top 5 Weekend Destinations",
"imageSlots": [
{
"position": 0,
"uploadUrl": "https://storage.googleapis.com/shortkit-images/.../0?signature=...",
"maxSizeBytes": 10485760
},
{
"position": 1,
"uploadUrl": "https://storage.googleapis.com/shortkit-images/.../1?signature=...",
"maxSizeBytes": 10485760
},
{
"position": 2,
"uploadUrl": "https://storage.googleapis.com/shortkit-images/.../2?signature=...",
"maxSizeBytes": 10485760
}
]
},
"meta": {
"request_id": "req_upload002"
}
}
# Upload each image to its signed URL
curl -X PUT "https://storage.googleapis.com/shortkit-images/.../0?signature=..." \
-H "Content-Type: image/jpeg" \
--data-binary @beach.jpg
curl -X PUT "https://storage.googleapis.com/shortkit-images/.../1?signature=..." \
-H "Content-Type: image/jpeg" \
--data-binary @mountain.jpg
curl -X PUT "https://storage.googleapis.com/shortkit-images/.../2?signature=..." \
-H "Content-Type: image/jpeg" \
--data-binary @city.jpg
ready.Creating a video carousel
Video carousels are containers that reference existing video content items. Create the carousel first, then add videos to it.- 1. Create the carousel
- 2. Add videos
curl -X POST https://api.shortkit.dev/v1/content/upload \
-H "Authorization: Bearer sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"contentType": "video_carousel",
"title": "Top Plays of the Week",
"section": "Sports",
"author": "SportsCast",
"tags": ["sports", "highlights"]
}'
{
"data": {
"id": "b4c5d6e7-f8a9-0123-bcde-f45678901234",
"contentType": "video_carousel",
"uploadStatus": "ready",
"title": "Top Plays of the Week",
"section": "Sports",
"createdAt": "2026-02-05T12:00:00Z"
},
"meta": {
"request_id": "req_upload003"
}
}
# Add existing video content items to the carousel
curl -X POST https://api.shortkit.dev/v1/content/b4c5d6e7.../videos \
-H "Authorization: Bearer sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"videoContentId": "d290f1ee-6c54-4b01-90e6-d701748f0851"
}'
PUT /v1/content/{id}/videos/order to reorder.Video carousels reference existing video content — the videos must already be uploaded and in
ready status. Empty carousels are excluded from the feed.Uploading in bulk
There is no batch upload endpoint — each content item requires its ownPOST /v1/content/upload call. To upload many items, make requests in parallel:
import asyncio
import httpx
items = [
{"contentType": "video", "title": "Video 1", "tags": ["batch"]},
{"contentType": "video", "title": "Video 2", "tags": ["batch"]},
{"contentType": "video", "title": "Video 3", "tags": ["batch"]},
]
async def upload_one(client, item):
resp = await client.post(
"https://api.shortkit.dev/v1/content/upload",
headers={"Authorization": "Bearer sk_live_your_secret_key"},
json=item,
)
return resp.json()["data"]
async def main():
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*[upload_one(client, i) for i in items])
for r in results:
print(f"{r['title']}: {r['uploadUrl']}")
asyncio.run(main())
For bulk server-side ingestion from URLs (cloud storage, CMS, etc.), use Import instead — it accepts up to 100 items in a single request.
Callback events
When you provide acallbackUrl, ShortKit sends a POST request to that URL at each stage of processing. Your endpoint must respond with a 2xx status code to acknowledge receipt.
Video uploads
ShortKit delivers up to three callbacks per video upload: 1.content.imported with status: "processing"
Sent when your file has been received and processing has begun. If a playbackId is present, store it alongside your content record — the video is already streamable at this point, though transcoding is not yet complete. Viewers of longer videos may experience buffering near the end until encoding finishes.
playbackId may be null in this callback if processing is still in its earliest stage. The ready callback always includes a non-null playbackId.{
"event": "content.imported",
"contentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"externalId": null,
"status": "processing",
"playbackId": "rJ4OE02Z0Z4IY01Eo4Vgup5OilG8xZB9",
"title": "Product Launch Teaser",
"source": "upload",
"timestamp": "2026-03-10T14:30:00.000Z"
}
content.imported with status: "ready"
Sent when transcoding completes and the video is fully available at all quality levels. The payload shape is identical to the processing callback, with status set to "ready". playbackId is always present.
3. content.import_failed with status: "error"
Sent if processing fails. The payload includes an error field with a human-readable description of the failure. No playbackId is included.
{
"event": "content.import_failed",
"contentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"externalId": null,
"status": "error",
"error": "A human-readable description of the failure.",
"title": "Product Launch Teaser",
"source": "upload",
"timestamp": "2026-03-10T14:35:00.000Z"
}
Carousel uploads
A single callback fires once all images have been validated. Thestatus is "ready" if at least one image processed successfully, or "errored" if all images failed. Only successfully processed images are included in the images array.
{
"event": "content.imported",
"contentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"externalId": null,
"contentType": "carousel",
"status": "ready",
"title": "Top 5 Weekend Destinations",
"images": [
{ "position": 0, "url": "https://..." },
{ "position": 1, "url": "https://..." },
{ "position": 2, "url": "https://..." }
],
"timestamp": "2026-03-10T14:30:00.000Z"
}
externalId reflects the value you provided on upload, or null if not set.Retry behavior
Each callback is attempted up to 3 times. If your endpoint returns a non-2xx status or is unreachable, ShortKit retries after 1 second, then again after 5 seconds. If all three attempts fail, the callback is abandoned. Callbacks are fire-and-forget — a failed callback does not affect processing. If your endpoint was down and you missed a callback, you can always retrieve the current status by pollingGET /v1/content/{id}.