GoonAPI v1

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, GET only
Auth
One API key per website

Quickstart

  1. 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.

  2. Check it works

    curl "{base}/v1/me" \
      -H "Authorization: Bearer $GOONAPI_KEY"
  3. List videos and render cards

    Every video has a title, duration in seconds and a thumbnail image you can drop straight into an <img>, with thumbnail_proxy as 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>`);
  4. 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:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetSeconds until the allowance refills.
Retry-AfterOn 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." } }
StatusCodeWhat to do
400invalid_parameterFix the parameter named in the message.
401missing_api_key, invalid_api_keySend a valid key.
403key_revokedThe key was turned off. Contact us.
404video_not_found, not_foundThe video is gone upstream, or the path is wrong.
429rate_limitedWait Retry-After seconds, then retry.
502streams_unavailable, upstream_unavailableThe source is having trouble. Retry shortly or offer another video.
500internal_errorOur 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:

  • mp4 plays in a plain <video> element everywhere and supports seeking.
  • hls is 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:

FieldServed byUse it for
thumbnailThe source's CDNThumbnails. Hotlinking works for every source today and is the fastest option.
thumbnail_proxyGoonAPI edgeFallback when a thumbnail fails to load.
preview_proxyGoonAPI edgeHover previews. Several source CDNs refuse hotlinked previews, so use this one.
previewThe source's CDNOnly 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.

SourceViewsRatingTagsDateChannelResolutionPreview clip
pornhubmostmostmostmostsomenosome
redtubeyesyesnonoyesnoyes
xvideosyesnononoyesyesyes
eporneryesyesyesyesnonoyes
youpornyesyesnonoyesnoyes
tube8yesyesnonoyesnoyes
sex.comyesyesnonononoyes
pmvhavenyesyesnonomostsomesome

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.

Objects