API documentation
Upload images from your website, app or bot and get a public URL back in the same response. Everything is plain HTTPS and JSON.
The API works from any language that can make an HTTP request: PHP websites, JavaScript apps, Python scripts, Telegram bots, mobile backends and more.
| Base URL | https://imghost.mgamer.online/api/v1 |
|---|---|
| Format | JSON responses, standard HTTP status codes |
| Uploads | JPG, PNG, WEBP, GIF · up to 10 MB per image |
Authentication
Send your API key as a Bearer token in the Authorization header of every request.
Authorization: Bearer hostaura-YOUR_API_KEYKeys always start with hostaura-. Requests without a valid key get 401 Unauthorized. Keep keys on your server whenever you can, and never commit them to a public repository.
API keys
Create as many keys as you need in the dashboard. A separate key per project (website, mobile app, Telegram bot) keeps usage, restrictions and webhooks independent.
- Shown once. We only store a hash of your key, so copy it when it's created.
- Not editable. To change a key, terminate it and create a new one. A terminated key stops working immediately.
- Optional restrictions. Limit a key to a website domain or to an IP address (or CIDR range). Restrictions are enforced by the API on every request.
Origin (or Referer) header that browsers send, so requests that don't come from a browser are rejected. Non-browser clients can forge headers, so use an IP restriction for servers and bots, and treat domain restrictions as protection against casual misuse rather than as authentication.Image upload
POST /api/v1/images/upload
Send the file as multipart/form-data in a field named image.
| Parameter | Type | Description |
|---|---|---|
image | file | Required. JPG, PNG, WEBP, GIF, up to 10 MB. The file content is verified, not just its extension. |
curl -X POST https://imghost.mgamer.online/api/v1/images/upload \
-H "Authorization: Bearer hostaura-YOUR_API_KEY" \
-F "image=@/path/to/photo.png"<?php
$ch = curl_init('https://imghost.mgamer.online/api/v1/images/upload');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HOSTAURA_API_KEY')],
CURLOPT_POSTFIELDS => ['image' => new CURLFile('/path/to/photo.png')],
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
if ($response['success']) {
echo $response['image']['url'];
} else {
echo $response['error']['message'];
}const form = new FormData();
form.append('image', fileInput.files[0]);
const res = await fetch('https://imghost.mgamer.online/api/v1/images/upload', {
method: 'POST',
headers: { Authorization: 'Bearer hostaura-YOUR_API_KEY' },
body: form,
});
const data = await res.json();
if (data.success) console.log(data.image.url);
else console.error(data.error.message);import os, requests
with open("photo.png", "rb") as f:
res = requests.post(
"https://imghost.mgamer.online/api/v1/images/upload",
headers={"Authorization": f"Bearer {os.environ['HOSTAURA_API_KEY']}"},
files={"image": f},
timeout=30,
)
data = res.json()
print(data["image"]["url"] if data["success"] else data["error"]["message"])Response 201 Created
{
"success": true,
"image": {
"id": "img_a8F92kQx",
"url": "https://imghost.mgamer.online/i/a8F92kQx",
"direct_url": "https://imghost.mgamer.online/i/a8F92kQx.webp",
"filename": "photo.webp",
"mime_type": "image/webp",
"size": 284512,
"width": 1920,
"height": 1080,
"created_at": "2026-09-19T10:24:31Z"
}
}url opens the public image page. Use direct_url when you need the raw file, for example in an <img> tag, a Markdown image or a Telegram message.
List images
GET /api/v1/images
| Query parameter | Description |
|---|---|
page | Page number. Default 1. |
per_page | Results per page, 1–100. Default 20. |
format | Optional: jpg, png, webp or gif. |
sort | newest (default), oldest, largest, smallest. |
curl "https://imghost.mgamer.online/api/v1/images?page=1&per_page=20" \
-H "Authorization: Bearer hostaura-YOUR_API_KEY"<?php
$ch = curl_init('https://imghost.mgamer.online/api/v1/images?page=1&per_page=20');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HOSTAURA_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
foreach ($data['images'] as $image) {
echo $image['id'] . ' ' . $image['url'] . PHP_EOL;
}const res = await fetch('https://imghost.mgamer.online/api/v1/images?page=1&per_page=20', {
headers: { Authorization: 'Bearer hostaura-YOUR_API_KEY' },
});
const { images, pagination } = await res.json();
console.log(images.length, 'of', pagination.total);import os, requests
res = requests.get(
"https://imghost.mgamer.online/api/v1/images",
params={"page": 1, "per_page": 20},
headers={"Authorization": f"Bearer {os.environ['HOSTAURA_API_KEY']}"},
timeout=30,
)
for image in res.json()["images"]:
print(image["id"], image["url"]){
"success": true,
"images": [ { "id": "img_a8F92kQx", "url": "…", "direct_url": "…", "filename": "photo.webp", "mime_type": "image/webp", "size": 284512, "width": 1920, "height": 1080, "created_at": "2026-09-19T10:24:31Z" } ],
"pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
}Get image
GET /api/v1/images/{id}
Returns one image from your account. Use the id from the upload response (for example img_a8F92kQx). Unknown IDs return 404.
curl https://imghost.mgamer.online/api/v1/images/img_a8F92kQx \
-H "Authorization: Bearer hostaura-YOUR_API_KEY"<?php
$ch = curl_init('https://imghost.mgamer.online/api/v1/images/img_a8F92kQx');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HOSTAURA_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
echo $data['image']['width'] . 'x' . $data['image']['height'];const res = await fetch('https://imghost.mgamer.online/api/v1/images/img_a8F92kQx', {
headers: { Authorization: 'Bearer hostaura-YOUR_API_KEY' },
});
const { image } = await res.json();
console.log(image.url);import os, requests
res = requests.get(
"https://imghost.mgamer.online/api/v1/images/img_a8F92kQx",
headers={"Authorization": f"Bearer {os.environ['HOSTAURA_API_KEY']}"},
timeout=30,
)
print(res.json()["image"]["url"])Delete image
DELETE /api/v1/images/{id}
Permanently deletes the file. Its public links stop working right away and an image.deleted webhook is sent.
curl -X DELETE https://imghost.mgamer.online/api/v1/images/img_a8F92kQx \
-H "Authorization: Bearer hostaura-YOUR_API_KEY"<?php
$ch = curl_init('https://imghost.mgamer.online/api/v1/images/img_a8F92kQx');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HOSTAURA_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
var_dump($data['deleted']);const res = await fetch('https://imghost.mgamer.online/api/v1/images/img_a8F92kQx', {
method: 'DELETE',
headers: { Authorization: 'Bearer hostaura-YOUR_API_KEY' },
});
console.log((await res.json()).deleted);import os, requests
res = requests.delete(
"https://imghost.mgamer.online/api/v1/images/img_a8F92kQx",
headers={"Authorization": f"Bearer {os.environ['HOSTAURA_API_KEY']}"},
timeout=30,
)
print(res.json()){ "success": true, "deleted": true, "id": "img_a8F92kQx" }Webhooks
Every API key has its own webhook. When that key uploads or deletes an image, we send a signed POST to your webhook URL. The API response always comes first, so the webhook is an optional server-to-server notification and never delays your upload.
| Event | Sent when |
|---|---|
image.uploaded | An image is uploaded with the key. |
image.deleted | An image is deleted with the key, or an image that was uploaded with it is deleted elsewhere. |
{
"id": "evt_Kx81mPq02JdVhT4zLw9a",
"event": "image.uploaded",
"success": true,
"created_at": "2026-09-19T10:24:31Z",
"webhook_id": "whk_Nq5mX2pR8sT1vB7c",
"image": {
"id": "img_a8F92kQx",
"url": "https://imghost.mgamer.online/i/a8F92kQx",
"direct_url": "https://imghost.mgamer.online/i/a8F92kQx.webp",
"filename": "photo.webp",
"mime_type": "image/webp",
"size": 284512,
"width": 1920,
"height": 1080
}
}Headers
| Header | Value |
|---|---|
X-Webhook-ID | Unique event ID (evt_…). Identical across retries, so you can use it to deduplicate. |
X-Webhook-Event | The event name, such as image.uploaded. |
X-Webhook-Timestamp | Unix time (seconds) when this attempt was signed. |
X-Webhook-Signature | sha256= followed by the HMAC of the timestamp and body. See signatures. |
Delivery and retries
Respond with any 2xx status within 5 seconds to acknowledge an event. Otherwise we retry with increasing delays (about 1 minute, then 5 minutes, then 30 minutes) for up to 3 attempts in total. Every attempt, HTTP status and error is recorded in your delivery log, where you can also send a test event or retry a failed one.
Webhook URLs must be public. We don't deliver to private or local addresses, and redirects are not followed.
No webhook URL? Use the hosted endpoint
If you leave the URL empty when creating a key, we generate a hosted endpoint for it (https://imghost.mgamer.online/hooks/whk_…). Events are stored there and you can read them whenever you like:
curl "https://imghost.mgamer.online/hooks/whk_YOUR_WEBHOOK_ID?limit=20" \
-H "Authorization: Bearer hostaura-YOUR_API_KEY"Add after=evt_… to receive only newer events. The response is { "success": true, "events": [ … ] }, with the same payloads shown above.
Webhook signatures
Each key's webhook has a signing secret (whsec_…) on the Webhooks page. To verify a request, compute an HMAC-SHA256 of timestamp + "." + raw body with that secret and compare it to the signature header. Use the raw request body, and reject old timestamps (more than 5 minutes) to block replays.
<?php
$secret = getenv('HOSTAURA_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature) || abs(time() - (int)$timestamp) > 300) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
if ($event['event'] === 'image.uploaded') {
// $event['image']['url'] ...
}
http_response_code(200);import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Webhook-Timestamp') ?? '';
const signature = req.get('X-Webhook-Signature') ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.HOSTAURA_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log(event.event, event.image?.url);
res.sendStatus(200);
});import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhook")
def webhook():
body = request.get_data() # raw bytes
timestamp = request.headers.get("X-Webhook-Timestamp", "")
signature = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(
os.environ["HOSTAURA_WEBHOOK_SECRET"].encode(),
timestamp.encode() + b"." + body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature) or abs(time.time() - int(timestamp or 0)) > 300:
abort(401)
event = request.get_json()
print(event["event"], event.get("image", {}).get("url"))
return "", 200Rate limits
- 60 requests per minute per API key.
- 20 uploads per minute per API key.
- 10,000 requests per day per account.
- Repeated failed authentication from one IP address is throttled.
Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time). When you go over, you get 429 Too Many Requests with a Retry-After header in seconds.
Errors
Errors use the same shape everywhere, together with the matching HTTP status.
{
"success": false,
"error": {
"code": "invalid_api_key",
"message": "Your API key is invalid or has been terminated."
}
}| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key, invalid_api_key, key_terminated | The key is missing, wrong or terminated. |
| 403 | domain_not_allowed, ip_not_allowed | The key's domain or IP restriction doesn't allow this request. |
| 403 | account_suspended, storage_limit_exceeded | The account is suspended or out of storage. |
| 404 | image_not_found | No image with that ID on your account. |
| 405 | method_not_allowed | The endpoint doesn't accept that HTTP method. |
| 413 | file_too_large | Your image is larger than the allowed limit (10 MB). |
| 422 | no_file, unsupported_type, invalid_image | No file was sent, the type isn't supported, or the file isn't a valid image. |
| 429 | rate_limited, upload_rate_limited, daily_limit_exceeded | Too many requests. See Retry-After. |
| 500 | server_error | Something went wrong on our side. Try again. |
| 503 | maintenance | The service is briefly unavailable. |
Examples
Telegram bot (Python)
Download a photo a user sent to your bot, host it, and reply with the link. Restrict the key to your bot server's IP address.
import os, requests
BOT = os.environ["TELEGRAM_TOKEN"]
KEY = os.environ["HOSTAURA_API_KEY"]
def host_telegram_photo(file_id: str) -> str:
info = requests.get(f"https://api.telegram.org/bot{BOT}/getFile", params={"file_id": file_id}, timeout=15).json()
photo = requests.get(f"https://api.telegram.org/file/bot{BOT}/{info['result']['file_path']}", timeout=30).content
res = requests.post(
"https://imghost.mgamer.online/api/v1/images/upload",
headers={"Authorization": f"Bearer {KEY}"},
files={"image": ("photo.jpg", photo, "image/jpeg")},
timeout=30,
).json()
return res["image"]["direct_url"]Website upload form (PHP)
Forward a file from your own upload form to the API. Keep the key on the server.
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && is_uploaded_file($_FILES['photo']['tmp_name'] ?? '')) {
$ch = curl_init('https://imghost.mgamer.online/api/v1/images/upload');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HOSTAURA_API_KEY')],
CURLOPT_POSTFIELDS => ['image' => new CURLFile($_FILES['photo']['tmp_name'], $_FILES['photo']['type'], $_FILES['photo']['name'])],
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
echo $result['success'] ? '<img src="' . htmlspecialchars($result['image']['direct_url']) . '">' : htmlspecialchars($result['error']['message']);
}Browser upload (JavaScript)
Front-end code can call the API directly when the key has a domain restriction for your site.
document.querySelector('#file').addEventListener('change', async (e) => {
const form = new FormData();
form.append('image', e.target.files[0]);
const res = await fetch('https://imghost.mgamer.online/api/v1/images/upload', {
method: 'POST',
headers: { Authorization: 'Bearer hostaura-YOUR_DOMAIN_RESTRICTED_KEY' },
body: form,
});
const { success, image, error } = await res.json();
document.querySelector('#preview').src = success ? image.direct_url : '';
if (!success) alert(error.message);
});