Overview
The AdultEmbed API is a free, continuously updated database of embeddable adult videos: titles, descriptions, WebP thumbnails, durations, categories, tags, performers and studios. Every video comes with a permanent player URL on adultembed.com, so you never handle or store a video file or a host link.
Base URL
https://adultembed.com/api/
Responses are JSON (UTF-8) unless a feed format says otherwise. Dates are ISO 8601 UTC. Slugs are lowercase and hyphenated, and they are what every filter takes. Only videos that are approved and currently playable are ever returned. A video that stops playing disappears from every endpoint and is announced as deleted in the changelog.
Quickstart
Fetch the ten newest videos and print an embed for each. Click Try it to run the request live from this page.
curl -s "https://adultembed.com/api/database/browse/?page_size=10" \
-H "X-Site-URL: https://your-site.com"
<?php
$ctx = stream_context_create(['http' => ['header' => "X-Site-URL: https://your-site.com\r\n"]]);
$data = json_decode(file_get_contents('https://adultembed.com/api/database/browse/?page_size=10', false, $ctx), true);
foreach ($data['results'] as $v) {
$player = 'https://adultembed.com/player/embed/' . $v['id'] . '/';
printf('<h3>%s</h3><iframe src="%s" width="640" height="360" allowfullscreen></iframe>',
htmlspecialchars($v['title']), $player);
}
const res = await fetch('https://adultembed.com/api/database/browse/?page_size=10',
{ headers: { 'X-Site-URL': 'https://your-site.com' } });
const data = await res.json();
for (const v of data.results) {
console.log(v.title, `https://adultembed.com/player/embed/${v.id}/`);
}
import requests
r = requests.get("https://adultembed.com/api/database/browse/",
params={"page_size": 10},
headers={"X-Site-URL": "https://your-site.com"}, timeout=30)
for v in r.json()["results"]:
print(v["title"], f"https://adultembed.com/player/embed/{v['id']}/")
Site & key headers
Nothing is required. Two optional headers make your integration better:
| Header | What it does |
|---|---|
| X-Site-URL | Your site, e.g. https://your-site.com. Signed embeds are bound to it and your usage is reported per site. Browsers send Origin/Referer on their own; server-side code should send this header. |
| X-API-Key | A free key (issued instantly at adultembed.com). Adds per-key statistics and is required only for /api/stats/, channels and webhook deliveries. A missing or wrong key never blocks a content request. |
Caching, limits, errors
There is no request quota. Responses are cached server-side, so polling faster than the cache refresh returns the same answer:
| Endpoint | Fresh every |
|---|---|
| /api/database/browse/ | 10 minutes per unique query string |
| /api/feeds/… | 10 minutes per unique query string |
| /api/videos/<id>/ | 2 minutes |
| /api/changelog/ | 1 minute (since is rounded to the minute) |
| /api/feeds/dump/… | regenerated nightly |
The one hard limit: POST /api/videos/<id>/get-embed/ allows 60 calls per IP per minute, plenty for a page of 24 cards, but not for bulk harvesting. Use the plain player URL when you don't need a signed one.
| Status | Meaning |
|---|---|
| 400 | Bad parameter, e.g. changelog without since, or videos/status without ids. The body says which. |
| 403 / 401 | An account endpoint (stats, channels, webhook deliveries) without a valid X-API-Key. |
| 404 | Not found or no longer playable. Treat it like a deleted changelog entry. |
| 429 | get-embed rate limit. Retry after a minute. |
Browse & filter
The main listing. Every list filter takes one slug or several comma-separated slugs. Several slugs mean any of, and different filters combine with and.
| Param | Values | Notes |
|---|---|---|
| orientation | straight · trans · gay · bi · all | Defaults to straight on browse. Use all (or mixed) for everything. A gay studio or performer returns 0 videos unless you pass orientation=gay or all. |
| categories | slugs | e.g. big-tits,blonde |
| tags | slugs | e.g. blowjob,creampie |
| performers | slugs | e.g. mannie-coco |
| studios | slugs | e.g. blacked,brazzers |
| content_type | scene · amateur | scene = matched to a studio release |
| source_type | embed · direct · hls | How the player streams it (all play in our iframe) |
| search | text | Substring match on title and description |
| sort | newest · popular · longest · views · likes | Default newest (also accepted: published, trending) |
| page, page_size | 1…, 1–100 | Default page size 20. per_page works as an alias. |
GET /api/database/browse/?orientation=trans&sort=views&page_size=50
GET /api/database/browse/?studios=blacked&tags=creampie
GET /api/database/browse/?performers=mannie-coco&content_type=scene
GET /api/database/browse/?orientation=mixed&search=massage
Response
{
"count": 410,
"next": "https://adultembed.com/api/database/browse/?page=2&studios=blacked",
"previous": null,
"results": [
{
"id": 3579910,
"title": "Tiny Beauty Can Barely Fit His Cock",
"slug": "blacked250113emirimomotatinybeautycanbarelyfithiscockxxx",
"description": "Emiri has a crazy cuckolding kink, but it's too hot for some guys to handle. …",
"thumbnail": "https://adultembed.com/media/thumbnails/webp/pz-3579910.webp",
"duration": "00:39:42",
"duration_seconds": 2382.0,
"resolution": "",
"view_count": 9, "like_count": 0, "dislike_count": 0, "rating": 0,
"content_type": "scene",
"categories": [],
"tags": [ {"id": 1110, "name": "Blowjob", "slug": "blowjob", "video_count": 48591}, … ],
"performers": [ {"id": 2682, "name": "Mannie Coco", "slug": "mannie-coco", "bio": "…"}, … ],
"studio": {"id": 344, "name": "Blacked", "slug": "blacked", "network_name": "Vixen Media Group"},
"tpdb_match_status": "matched",
"tpdb_rating": 4.5,
"tpdb_background_url": "https://cdn.theporndb.net/scene/…",
"available_hosts": ["Doodstream"],
"primary_source_type": "embed",
"published_at": "2026-07-20T00:43:44.627422Z"
}
]
}
Lists: categories, tags, performers, studios
Everything you can filter by is a live list. Fetch it, show it to your visitors (menus, tag clouds, A–Z performer pages, studio directories), and pass the chosen entry's filter straight to browse. Nothing here is static: new tags, performers and studios appear daily and the counts are recalculated every night from the videos that actually play.
| Param | Values | Default | Notes |
|---|---|---|---|
| search | text | – | Case-insensitive part of the name: search=big |
| starts_with | a–z · 0-9 | – | For A–Z index pages |
| min_videos | 0… | 1 | Hide entries with fewer playable videos; 0 lists empty ones too |
| ordering | popular · name · newest | popular | popular = most videos first; newest = most recently added |
| page, per_page | 1…, 1–500 | 1, 100 | Follow next until it is null |
Response
{
"list": "tags", "count": 122, "page": 1, "per_page": 100, "pages": 2,
"next": "https://adultembed.com/api/taxonomy/tags/?search=anal&page=2", "previous": null,
"filter_param": "tags",
"results": [
{"id": 1099, "name": "Anal", "slug": "anal", "video_count": 26070,
"filter": "tags=anal",
"videos_url": "https://adultembed.com/api/database/browse/?tags=anal"}
]
}
Performers also carry verified and image; studios carry logo and network. video_count counts every orientation, so add orientation= to the browse call for the audience you serve (browse defaults to straight).
Recipe: a menu that stays current
// 1. the 30 biggest tags, refreshed whenever you like (they are cached 10 min on our side)
const tags = await (await fetch('https://adultembed.com/api/taxonomy/tags/?per_page=30')).json();
// 2. a visitor picks one -> use its ready-made filter
const pick = tags.results[0];
const vids = await (await fetch(`https://adultembed.com/api/database/browse/?${pick.filter}&orientation=all&sort=newest`)).json();
// 3. every performer, page by page
let url = 'https://adultembed.com/api/taxonomy/performers/?per_page=500', all = [];
while (url) { const p = await (await fetch(url)).json(); all.push(...p.results); url = p.next; }
<?php
$base = 'https://adultembed.com';
$cats = json_decode(file_get_contents("$base/api/taxonomy/categories/?per_page=50"), true);
foreach ($cats['results'] as $c) {
echo '<a href="/c/' . $c['slug'] . '">' . htmlspecialchars($c['name']) . ' (' . $c['video_count'] . ')</a>';
}
// on /c/{slug}: $videos = json_decode(file_get_contents("$base/api/database/browse/?categories=$slug&orientation=all"), true);
import requests
base = 'https://adultembed.com'
studios, url = [], f'{base}/api/taxonomy/studios/?per_page=500&min_videos=5'
while url:
page = requests.get(url, timeout=30).json()
studios += page['results']
url = page['next']
videos = requests.get(f"{base}/api/database/browse/?{studios[0]['filter']}&orientation=all").json()
Bulk snapshot
One call with the biggest categories, tags and performers together (type = all · categories · tags · performers, limit up to 10000, default 2000). Handy for a one-off import; for anything that grows, use the paginated lists above.
Video detail
One video, same fields as a browse result. 404 means the video is no longer playable, so remove it.
Video status (bulk)
Which of up to 500 videos are playable right now. Run it once a day over your whole library: unpublish unavailable and republish anything that came back. This catches removals that never reach the changelog.
{"servable": [3579910], "unavailable": [1]}
Player iframe
Every video plays at a permanent URL. This is all most sites need:
<iframe src="https://adultembed.com/player/embed/3579910/"
width="640" height="360" frameborder="0" scrolling="no"
allow="autoplay; fullscreen" allowfullscreen></iframe>
By slug instead of id: https://adultembed.com/player/embed/v/<slug>/
| Param | Default | Effect |
|---|---|---|
| autoplay | 0 | 1 starts on load (browsers require muted=1 too) |
| muted | 0 | 1 starts muted |
| user_vast | — | URL-encoded VAST tag: your own pre-roll ad |
The player is responsive. For a 16:9 box that scales, wrap it: <div style="position:relative;padding-top:56.25%"> and give the iframe position:absolute;inset:0;width:100%;height:100%.
Signed embed URL
Returns a player URL signed for your domain (from X-Site-URL, otherwise Origin/Referer) and valid for 2 hours. Plays are then attributed to your site and key. Note that this is a POST.
curl -s -X POST "https://adultembed.com/api/videos/3579910/get-embed/" \
-H "X-Site-URL: https://your-site.com"
{
"player_url": "https://adultembed.com/player/embed/3579910/?sig=d6809d…&exp=1791292288&d=your-site.com&k=0",
"video_id": 3579910,
"title": "Tiny Beauty Can Barely Fit His Cock",
"embed_code": "<iframe src=\"…&autoplay=1\" width=\"100%\" height=\"100%\" frameborder=\"0\" allowfullscreen allow=\"autoplay; encrypted-media\"></iframe>"
}
oEmbed
Standard oEmbed 1.0 (type: video). Optional maxwidth (≤ 1280) and maxheight. Forum software, Ghost, Discourse, Iframely and Embedly can turn a pasted player link into the player. Our WordPress plugin registers the provider for you.
Changelog
Everything that changed since a moment, one entry per video (its latest change), oldest first. Accepts the same filters as browse (orientation, categories, studios …), so a niche site is only told about videos it wants. Unlike browse, orientation is not filtered unless you pass it.
| change_type | What to do |
|---|---|
| created | Fetch /api/videos/<id>/ and insert it |
| updated | Fetch and upsert (title, tags, thumbnail or source changed, or the video came back) |
| deleted | Unpublish it. Always sent whatever your filters are; deleting what you don't have is a no-op. |
{
"changes": [
{"video_id": 7052926, "change_type": "deleted",
"change_data": {"changes": {"title": {"old": "…", "new": "…"}}},
"timestamp": "2026-10-06T11:01:20.049309+00:00"}
],
"results": [ …same list… ],
"count": 1,
"has_more": false,
"next_since": "2026-10-06T11:21:04.102938+00:00"
}
At most 5000 changes per call. If has_more is true, call again immediately with since=next_since.
Sync recipe
- Seed once from a nightly dump (or page through feeds with limit=10000).
- Every 5–15 minutes, call the changelog with the next_since you saved last time.
- Once a day, check your whole library with /api/videos/status/ in batches of 500.
import requests, time
API = "https://adultembed.com/api"
H = {"X-Site-URL": "https://your-site.com"}
since = load_cursor() or "2026-10-01T00:00:00Z"
while True:
d = requests.get(f"{API}/changelog/", params={"since": since, "orientation": "trans"}, headers=H, timeout=60).json()
for ch in d["changes"]:
if ch["change_type"] == "deleted":
unpublish(ch["video_id"])
else:
r = requests.get(f"{API}/videos/{ch['video_id']}/", headers=H, timeout=30)
upsert(r.json()) if r.ok else unpublish(ch["video_id"])
since = d["next_since"]; save_cursor(since)
if not d["has_more"]:
time.sleep(600)
Webhooks
Push delivery (a JSON POST to your URL on video_added / video_updated) is available on request. Email [email protected] with your key and endpoint. For almost every site the changelog is simpler and can't miss an event while your server is down, so start there.
With a key, you can inspect and retry deliveries:
GET /api/webhooks/deliveries/?status=failed&limit=50 X-API-Key: your_key
POST /api/webhooks/deliveries/<delivery_id>/retry/ X-API-Key: your_key
Feeds (CSV, RSS, JSON)
Import-ready feeds for tube scripts, newest first. They take every browse filter (no orientation default) plus since=<ISO 8601> for incremental imports. Page with page or offset; limit defaults to 1000 (max 10000).
| URL | Format | Works with |
|---|---|---|
| videos.csv?layout=pornhub | Pipe-separated, no header: embed|thumb|thumbs|title|tags|categories|pornstars|duration|views|likes|dislikes | AVS, SmartCJ, TubeAce, Mechbunny, PHPVibe "Pornhub dump" presets |
| videos.csv?layout=generic | Header row; sep=, ; | or tab | KVS, AVS column mapping, spreadsheets |
| videos.xml | Media RSS 2.0 (media:player, media:thumbnail, keywords, categories) | KVS RSS import, WP RSS Aggregator, WPeMatico, WP All Import |
| videos.json | results + next_offset | Custom code |
https://adultembed.com/api/feeds/videos.csv?layout=pornhub&orientation=trans&limit=5000
https://adultembed.com/api/feeds/videos.xml?categories=amateur&since=2026-10-01T00:00:00Z
Embed codes in feeds use the plain player URL, which never expires. Plays are attributed to your site through the Referer.
Nightly dumps
The whole playable database as gzipped CSV, rebuilt every night. Stable links (they redirect to the newest file):
https://adultembed.com/api/feeds/dump/<orientation>.<layout>.csv.gz
orientation: all · straight · trans · gay · bi
layout: pornhub · generic
OpenAPI
A machine-readable OpenAPI 3.1 description lives at /api/openapi.json. Load it into Postman, Insomnia or Swagger UI, or generate a client with openapi-generator.
Your stats
Usage totals for your key. Per-site traffic is also tracked for keyless use. Ask us if you want your numbers.
Channels
A channel is a saved filter with a public, stable feed URL that keeps updating as matching videos arrive.
curl -s -X POST "https://adultembed.com/api/channels/" \
-H "X-API-Key: your_key" -H "Content-Type: application/json" \
-d '{
"name": "Long amateur",
"description": "Amateur, 20 minutes and up",
"filter_category_ids": [2060],
"filter_tag_ids": [],
"filter_performer_ids": [],
"filter_search": "",
"min_duration_seconds": 1200,
"max_duration_seconds": null,
"sort_by": "published",
"is_public": true
}'
IDs come from the lists. The response includes the channel's slug.
Same response shape and paging as browse.
View postback
Only needed if you play videos in a player of your own; our iframe counts its own plays.
POST /api/track/view/
Content-Type: application/json
{"video_id": 3579910, "country": "US", "device": "mobile", "duration_watched": 180,
"referrer": "https://your-site.com/video/123"}
Only video_id is required.
Free and open to everyone. Want per-key stats? Get a free key. Need a feed format or field we don't have yet, or found something this page gets wrong? Email 2034network@gmail.com.