GoonAPI
Add a full video catalog to any tube site. One API gives you search, browse feeds, video details, playable streams and embeddable thumbnails, merged from many sources and kept working for you.
- Base URL
- Format
- JSON over HTTPS,
GETonly - Auth
- One API key per website
Quickstart
-
Get your key
Keys are issued per website by the GoonAPI team. A key looks like
goon_live_…. Keep it on your server; treat it like a password. -
Check it works
curl "{base}/v1/me" \ -H "Authorization: Bearer $GOONAPI_KEY" -
List videos and render cards
Every video has a
title,durationin seconds and athumbnailimage you can drop straight into an<img>, withthumbnail_proxyas a fallback.const res = await fetch("{base}/v1/videos?q=amateur&limit=24", { headers: { Authorization: `Bearer ${process.env.GOONAPI_KEY}` }, }); const { data: videos, next_page } = await res.json(); const cards = videos.map((v) => ` <a href="/watch/${v.id}"> <img src="${v.thumbnail}" data-fallback="${v.thumbnail_proxy}" alt="" loading="lazy"> <span>${v.title}</span> </a>`); -
Play one
When a visitor opens a video, ask for its streams and hand the best one to your player. See Playing video.
const res = await fetch(`{base}/v1/videos/${id}/streams`, { headers: { Authorization: `Bearer ${process.env.GOONAPI_KEY}` }, }); const { data: streams } = await res.json(); const mp4 = streams.find((s) => s.format === "mp4") ?? streams[0];
Authentication
Send your key with every request to /v1, in the Authorization header:
Authorization: Bearer goon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
An X-API-Key: <key> header and an api_key query parameter are accepted too. Prefer the header: query strings end up in logs.
Call the API from your server. A key placed in browser JavaScript is visible to every visitor. Fetch from your backend and send your pages the results. Image and stream URLs in responses need no key, so those are safe to put in pages.
Rate limits
Each key has a requests-per-minute limit (120 unless agreed otherwise; /v1/me shows yours). Every response carries the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the allowance refills. |
Retry-After | On a 429: seconds to wait before retrying. |
Listings change slowly. Caching them for a few minutes on your side keeps you well under the limit and makes your pages faster. Thumbnail and stream URLs do not count as API requests.
Errors
Errors use ordinary HTTP status codes and always have the same body:
{ "error": { "code": "invalid_api_key", "message": "This API key is not recognised." } }
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_parameter | Fix the parameter named in the message. |
| 401 | missing_api_key, invalid_api_key | Send a valid key. |
| 403 | key_revoked | The key was turned off. Contact us. |
| 404 | video_not_found, not_found | The video is gone upstream, or the path is wrong. |
| 429 | rate_limited | Wait Retry-After seconds, then retry. |
| 502 | streams_unavailable, upstream_unavailable | The source is having trouble. Retry shortly or offer another video. |
| 500 | internal_error | Our fault. Retry, and tell us if it persists. |
Pagination
/v1/videos is paged with page, starting at 1. Each response includes next_page: request that page next, and stop when it is null.
A page is a fresh merge across all sources, so pages vary in size (typically 300 to 500 videos) and a video can occasionally reappear on a later page. Use limit to trim a page, and de-duplicate by id if you build an infinite scroll.
Playing video
/v1/videos/{id}/streams returns the quality variants of a video, best first. Each has a format:
mp4plays in a plain<video>element everywhere and supports seeking.hlsis an adaptive playlist. Safari plays it natively; other browsers need hls.js.
const video = document.querySelector("video");
const pick = streams.find((s) => s.format === "mp4") ?? streams[0];
if (pick.format === "mp4" || video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = pick.url;
} else {
const hls = new Hls();
hls.loadSource(pick.url);
hls.attachMedia(video);
}
Stream URLs expire. They are signed by the source and last only minutes (expires_in says how long). Request streams when the visitor presses play, never store them, and request them again if playback fails with an expired link.
Stream URLs allow cross-origin requests and byte ranges, so they work from any domain. For seek-bar previews, the detail endpoint's storyboard is a WebVTT thumbnail track when the source provides one.
Thumbnails and previews
Every video carries two versions of each image:
| Field | Served by | Use it for |
|---|---|---|
thumbnail | The source's CDN | Thumbnails. Hotlinking works for every source today and is the fastest option. |
thumbnail_proxy | GoonAPI edge | Fallback when a thumbnail fails to load. |
preview_proxy | GoonAPI edge | Hover previews. Several source CDNs refuse hotlinked previews, so use this one. |
preview | The source's CDN | Only if you check it loads; some sources reject it. |
Recommended: show thumbnail, and switch to thumbnail_proxy if it errors.
document.addEventListener("error", (event) => {
const img = event.target;
if (img.tagName === "IMG" && img.dataset.fallback && img.src !== img.dataset.fallback) {
img.src = img.dataset.fallback;
}
}, true);
preview is a short muted clip for hover effects, present for sources that offer one. With include=frames, listings also return frames (still images to cycle on hover), but only a few sources supply them, so build hover effects on preview_proxy.
Edge URLs need no API key, allow cross-origin use and are cached at the edge, so they can go directly into <img> and <video> tags.
API reference
All endpoints are GET and return JSON. Successful responses wrap their result in data.
Loading the reference…
What each source provides
Sources differ in what their listings expose. Fields a source doesn't provide are null (or an empty list for tags); handle that in your templates. Title, thumbnail and duration are always present.
| Source | Views | Rating | Tags | Date | Channel | Resolution | Preview clip |
|---|---|---|---|---|---|---|---|
| pornhub | most | most | most | most | some | no | some |
| redtube | yes | yes | no | no | yes | no | yes |
| xvideos | yes | no | no | no | yes | yes | yes |
| eporner | yes | yes | yes | yes | no | no | yes |
| youporn | yes | yes | no | no | yes | no | yes |
| tube8 | yes | yes | no | no | yes | no | yes |
| sex.com | yes | yes | no | no | no | no | yes |
| pmvhaven | yes | yes | no | no | most | some | some |
The detail endpoint adds tags for pornhub, redtube and eporner, and a description where the uploader wrote one. Because most listings carry no date, don't rely on published_at for ordering; use sort.