Tải nội dung đa phương tiện lên qua URL đã ký
curl --request POST \
--url https://app.getkavio.app/api/uploads/{token} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://app.getkavio.app/api/uploads/{token}"
payload = {}
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({})
};
fetch('https://app.getkavio.app/api/uploads/{token}', 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://app.getkavio.app/api/uploads/{token}",
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([
]),
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://app.getkavio.app/api/uploads/{token}"
payload := strings.NewReader("{}")
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://app.getkavio.app/api/uploads/{token}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.getkavio.app/api/uploads/{token}")
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 = "{}"
response = http.request(request)
puts response.read_body{
"upload_token": "<string>",
"media_id": "<string>",
"type": "<string>",
"mime_type": "<string>",
"original_filename": "<string>"
}Bài đăng
Tải nội dung đa phương tiện lên qua URL đã ký
Nhận tệp tải lên qua URL dùng một lần do MCP cấp. Endpoint này thường được gọi trong luồng request-media-upload-tool, không gọi trực tiếp.
POST
/
uploads
/
{token}
Tải nội dung đa phương tiện lên qua URL đã ký
curl --request POST \
--url https://app.getkavio.app/api/uploads/{token} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://app.getkavio.app/api/uploads/{token}"
payload = {}
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({})
};
fetch('https://app.getkavio.app/api/uploads/{token}', 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://app.getkavio.app/api/uploads/{token}",
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([
]),
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://app.getkavio.app/api/uploads/{token}"
payload := strings.NewReader("{}")
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://app.getkavio.app/api/uploads/{token}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.getkavio.app/api/uploads/{token}")
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 = "{}"
response = http.request(request)
puts response.read_body{
"upload_token": "<string>",
"media_id": "<string>",
"type": "<string>",
"mime_type": "<string>",
"original_filename": "<string>"
}Endpoint này là đích đến của URL POST đã ký do MCP
request-media-upload-tool cấp. Nhờ đó, AI agent và ứng dụng của agent có thể tải tệp cục bộ lên không gian làm việc mà không cần cấp Personal Access Token; chính URL đã ký đóng vai trò xác thực.
Thông thường, bạn không gọi trực tiếp endpoint này. Luồng xử lý như sau:
- AI agent gọi
request-media-upload-toolvà nhậnupload_urlcùngupload_token - Agent hoặc người dùng gửi tệp bằng POST tới
upload_url - AI agent gọi
attach-media-from-upload-toolvới cùngupload_tokenđể gắn Media vừa tạo vào bài đăng
POST /posts/{post}/media để tải trực tiếp lên một bài đăng mà không cần URL đã ký.
Tham số đường dẫn
string
bắt buộc
UUID dùng một lần được
request-media-upload-tool nhúng vào URL đã ký. Phải
là UUID hợp lệ; giá trị không phải UUID sẽ trả về 404.Tham số truy vấn (do URL đã ký thêm vào)
Các tham số này đượcURL::temporarySignedRoute của Laravel thêm vào URL và bảo vệ bằng chữ ký HMAC. Bạn không được tự đặt chúng.
string
bắt buộc
Không gian làm việc nhận tệp tải lên. Việc sửa giá trị này sẽ làm chữ ký mất
hiệu lực và endpoint trả về
403.integer
bắt buộc
Unix timestamp khi URL hết hạn. TTL mặc định là 15 phút kể từ lúc cấp URL
(có thể cấu hình qua
MCP_UPLOAD_URL_TTL_MINUTES).string
bắt buộc
HMAC-SHA256 của URL và tham số truy vấn, được ký bằng
APP_KEY của ứng dụng.
Middleware signed của Laravel kiểm tra chữ ký trước khi controller chạy.Yêu cầu
Gửi dưới dạngmultipart/form-data với một trường media.
file
bắt buộc
Tệp cần tải lên. Luồng MCP có giới hạn cứng 50 MB (cấu hình qua
MCP_UPLOAD_MAX_SIZE_MB) và chỉ chấp nhận các loại MIME sau:- Ảnh:
image/jpeg,image/png,image/gif,image/webp - Video:
video/mp4,video/quicktime(MOV)
Content-Type. Vì vậy, đổi tên evil.exe thành photo.png cũng không vượt qua được danh sách cho phép.Xác thực
Không cần Bearer token. URL đã ký chính là thông tin xác thực.- Middleware
signedxác minh HMAC bằngAPP_KEYvà từ chối URL hết hạn với mã403. Cache::addnguyên tử cùng ràng buộcUNIQUEtrênmedia.upload_tokenbảo đảm URL chỉ dùng một lần. Yêu cầu POST thứ hai với cùngtokensẽ nhận409.- Endpoint giới hạn 10 yêu cầu mỗi phút cho mỗi IP để giảm thiểu nguy cơ URL bị lộ và lạm dụng.
Cách hoạt động
- Tệp được lưu tại
medias/{uuid}.{ext}trên ổ lưu trữ đã cấu hình (local,s3,r2…). Đường dẫn do máy chủ tạo; tên tệp từ máy khách không được dùng trong đường dẫn. - Ảnh tĩnh PNG và WebP được chuẩn hóa thành JPEG (q100) để tương thích với mọi nền tảng. GIF được giữ nguyên, gồm cả chuyển động trên X, Bluesky và Mastodon. Video được lưu nguyên bản.
- Bản ghi
Mediamới được gắnupload_tokenđể AI agent tham chiếu quaattach-media-from-upload-tool. Media được tạo dưới dạng tài nguyên của không gian làm việc, chưa gắn với bài đăng nào. - Luồng
POST /posts/{post}/mediakhông gọi endpoint này. Endpoint chỉ xử lý URL đã ký do MCP cấp.
Phản hồi
string
Trả lại tham số đường dẫn
{token} để ứng dụng tiện sử dụng.string
UUID của bản ghi
Media vừa tạo. Truyền giá trị này (hoặc upload_token) vào
attach-media-from-upload-tool để gắn vào bài đăng.string
image hoặc video.string
Loại MIME được phát hiện. Lưu ý: đầu vào PNG/WebP được báo là
image/jpeg do
bước chuẩn hóa.string
Tên tệp do máy khách gửi (chỉ lưu trong metadata, không bao giờ dùng làm đường
dẫn lưu trữ). path).
Mã trạng thái
| Mã | Ý nghĩa |
|---|---|
201 | Đã lưu tệp tải lên và tạo Media. |
403 | Chữ ký không hợp lệ, đã hết hạn hoặc tham số truy vấn bị sửa. |
409 | Token đã được dùng (token chỉ dùng một lần). |
422 | Thiếu tệp, tệp quá lớn (> MCP_UPLOAD_MAX_SIZE_MB) hoặc loại MIME không được phép. |
429 | Vượt giới hạn tần suất (hơn 10 yêu cầu POST/phút từ IP này). |
# Assume {upload_url} came from request-media-upload-tool.
curl -X POST "{upload_url}" \
-F "media=@./photo.jpg"
{
"upload_token": "9f2c5b4e-1a3d-4e5f-8a7b-2c1d3e4f5a6b",
"media_id": "2681a1bf-131f-41b1-9866-755c1cb51f97",
"type": "image",
"mime_type": "image/jpeg",
"original_filename": "photo.jpg"
}

