curl --request POST \
--url https://api.fanfeed.ai/v1/users/{user_id}/media \
--header 'Content-Type: application/json' \
--header 'X-PARTNER-API-KEY: <api-key>' \
--data '
{
"media": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2026-06-14T02:31:07Z",
"latitude": "<unknown>",
"longitude": "<unknown>",
"media_type": "photo",
"filename": "IMG_4821.HEIC",
"thumbnail_url": "<string>",
"duration_seconds": "<unknown>",
"device_make": "Apple",
"device_model": "iPhone 15 Pro",
"device_lens_model": "<string>"
}
]
}
'import requests
url = "https://api.fanfeed.ai/v1/users/{user_id}/media"
payload = { "media": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2026-06-14T02:31:07Z",
"latitude": "<unknown>",
"longitude": "<unknown>",
"media_type": "photo",
"filename": "IMG_4821.HEIC",
"thumbnail_url": "<string>",
"duration_seconds": "<unknown>",
"device_make": "Apple",
"device_model": "iPhone 15 Pro",
"device_lens_model": "<string>"
}
] }
headers = {
"X-PARTNER-API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-PARTNER-API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
media: [
{
id: 'B84E8479-475C-4727-A4A4-B77AA9980897/L0/001',
taken_at: '2026-06-14T02:31:07Z',
latitude: '<unknown>',
longitude: '<unknown>',
media_type: 'photo',
filename: 'IMG_4821.HEIC',
thumbnail_url: '<string>',
duration_seconds: '<unknown>',
device_make: 'Apple',
device_model: 'iPhone 15 Pro',
device_lens_model: '<string>'
}
]
})
};
fetch('https://api.fanfeed.ai/v1/users/{user_id}/media', 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://api.fanfeed.ai/v1/users/{user_id}/media",
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([
'media' => [
[
'id' => 'B84E8479-475C-4727-A4A4-B77AA9980897/L0/001',
'taken_at' => '2026-06-14T02:31:07Z',
'latitude' => '<unknown>',
'longitude' => '<unknown>',
'media_type' => 'photo',
'filename' => 'IMG_4821.HEIC',
'thumbnail_url' => '<string>',
'duration_seconds' => '<unknown>',
'device_make' => 'Apple',
'device_model' => 'iPhone 15 Pro',
'device_lens_model' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-PARTNER-API-KEY: <api-key>"
],
]);
$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://api.fanfeed.ai/v1/users/{user_id}/media"
payload := strings.NewReader("{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-PARTNER-API-KEY", "<api-key>")
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://api.fanfeed.ai/v1/users/{user_id}/media")
.header("X-PARTNER-API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fanfeed.ai/v1/users/{user_id}/media")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-PARTNER-API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"accepted": 96,
"matched": 8,
"rejected": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/002",
"reason": "missing_coordinates"
}
],
"matches": [
{
"media_id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"matched": true,
"event_id": 18012359,
"venue_id": 4412
}
],
"events": [
{
"id": 18012359,
"name": "Zach Bryan",
"starts_at_local": "2026-06-13T19:30:00",
"venue": {
"id": 4412,
"name": "Madison Square Garden",
"city": "New York",
"region": "NY",
"country": "US",
"latitude": "<unknown>",
"longitude": "<unknown>"
},
"performers": [
{
"id": 34421,
"name": "Zach Bryan",
"category": "Country",
"image_url": "<string>",
"is_headliner": true
}
],
"media_count": 12,
"media": [
{
"media_id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2023-11-07T05:31:56Z",
"media_type": "photo",
"thumbnail_url": "<string>"
}
]
}
],
"last_sync_at": "2026-08-25T18:44:12Z"
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}Ingest a batch of camera-roll metadata
Send photo metadata only, never image bytes. FanFeed matches each item against venue and event data using its coordinates and capture time, and returns the matches inline in the response for that batch.
There is no webhook and nothing to poll: each batch’s response is the result for that batch. Drive your progress UI from batch completions.
Send 50–200 items per request. Items without coordinates or a capture time cannot
be matched and are returned in rejected; filter them out client-side to save the
round trip.
Re-sending a batch is safe. Photos that matched upsert on your asset id and never produce a second entry; photos that matched nothing are not retained, so re-sending one costs a re-evaluation rather than a duplicate. Either way the result is the same, so overlapping your scan window slightly is the right call.
FanFeed records the sync high-water mark per user; last_sync_at comes back on every
response and is also available on the stats endpoint.
curl --request POST \
--url https://api.fanfeed.ai/v1/users/{user_id}/media \
--header 'Content-Type: application/json' \
--header 'X-PARTNER-API-KEY: <api-key>' \
--data '
{
"media": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2026-06-14T02:31:07Z",
"latitude": "<unknown>",
"longitude": "<unknown>",
"media_type": "photo",
"filename": "IMG_4821.HEIC",
"thumbnail_url": "<string>",
"duration_seconds": "<unknown>",
"device_make": "Apple",
"device_model": "iPhone 15 Pro",
"device_lens_model": "<string>"
}
]
}
'import requests
url = "https://api.fanfeed.ai/v1/users/{user_id}/media"
payload = { "media": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2026-06-14T02:31:07Z",
"latitude": "<unknown>",
"longitude": "<unknown>",
"media_type": "photo",
"filename": "IMG_4821.HEIC",
"thumbnail_url": "<string>",
"duration_seconds": "<unknown>",
"device_make": "Apple",
"device_model": "iPhone 15 Pro",
"device_lens_model": "<string>"
}
] }
headers = {
"X-PARTNER-API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-PARTNER-API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
media: [
{
id: 'B84E8479-475C-4727-A4A4-B77AA9980897/L0/001',
taken_at: '2026-06-14T02:31:07Z',
latitude: '<unknown>',
longitude: '<unknown>',
media_type: 'photo',
filename: 'IMG_4821.HEIC',
thumbnail_url: '<string>',
duration_seconds: '<unknown>',
device_make: 'Apple',
device_model: 'iPhone 15 Pro',
device_lens_model: '<string>'
}
]
})
};
fetch('https://api.fanfeed.ai/v1/users/{user_id}/media', 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://api.fanfeed.ai/v1/users/{user_id}/media",
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([
'media' => [
[
'id' => 'B84E8479-475C-4727-A4A4-B77AA9980897/L0/001',
'taken_at' => '2026-06-14T02:31:07Z',
'latitude' => '<unknown>',
'longitude' => '<unknown>',
'media_type' => 'photo',
'filename' => 'IMG_4821.HEIC',
'thumbnail_url' => '<string>',
'duration_seconds' => '<unknown>',
'device_make' => 'Apple',
'device_model' => 'iPhone 15 Pro',
'device_lens_model' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-PARTNER-API-KEY: <api-key>"
],
]);
$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://api.fanfeed.ai/v1/users/{user_id}/media"
payload := strings.NewReader("{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-PARTNER-API-KEY", "<api-key>")
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://api.fanfeed.ai/v1/users/{user_id}/media")
.header("X-PARTNER-API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fanfeed.ai/v1/users/{user_id}/media")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-PARTNER-API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"media\": [\n {\n \"id\": \"B84E8479-475C-4727-A4A4-B77AA9980897/L0/001\",\n \"taken_at\": \"2026-06-14T02:31:07Z\",\n \"latitude\": \"<unknown>\",\n \"longitude\": \"<unknown>\",\n \"media_type\": \"photo\",\n \"filename\": \"IMG_4821.HEIC\",\n \"thumbnail_url\": \"<string>\",\n \"duration_seconds\": \"<unknown>\",\n \"device_make\": \"Apple\",\n \"device_model\": \"iPhone 15 Pro\",\n \"device_lens_model\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"accepted": 96,
"matched": 8,
"rejected": [
{
"id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/002",
"reason": "missing_coordinates"
}
],
"matches": [
{
"media_id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"matched": true,
"event_id": 18012359,
"venue_id": 4412
}
],
"events": [
{
"id": 18012359,
"name": "Zach Bryan",
"starts_at_local": "2026-06-13T19:30:00",
"venue": {
"id": 4412,
"name": "Madison Square Garden",
"city": "New York",
"region": "NY",
"country": "US",
"latitude": "<unknown>",
"longitude": "<unknown>"
},
"performers": [
{
"id": 34421,
"name": "Zach Bryan",
"category": "Country",
"image_url": "<string>",
"is_headliner": true
}
],
"media_count": 12,
"media": [
{
"media_id": "B84E8479-475C-4727-A4A4-B77AA9980897/L0/001",
"taken_at": "2023-11-07T05:31:56Z",
"media_type": "photo",
"thumbnail_url": "<string>"
}
]
}
],
"last_sync_at": "2026-08-25T18:44:12Z"
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}{
"error": {
"code": "invalid_batch_size",
"message": "Send at most 200 media items per request.",
"details": {}
}
}Authorizations
Your FanFeed partner API key, issued to your organization.
FanFeed trusts your authentication: the key identifies you, and the user_id in the
path identifies which of your users the request is for. FanFeed performs no end-user
authentication of its own.
Treat the key as a server-side secret. See the guide for what that implies for the camera-roll ingest path, which is the one call that naturally originates on-device.
Path Parameters
The FanFeed user id returned by POST /users.
"8f14e45f-ceea-467a-9a1e-2b4d9c3f0a11"
Body
Aim for 50–200 items per request. 200 is a hard cap; anything larger returns 413.
Smaller batches are valid and expected: the last batch of a scan is normally short, and a user with only a handful of geotagged photos may never fill one.
1 - 200 elementsShow child attributes
Show child attributes
Response
Batch processed. Matches are included.
Items in this batch accepted for matching: everything you sent that was not rejected.
96
A subset of accepted: how many of those landed on an event (matched: true
with an event_id). The remainder are perfectly good ingested photos that were
not taken at a live event.
8
Always present. Empty when every item in the batch was accepted. Rejections are per-item and never fail the request.
Show child attributes
Show child attributes
One entry per accepted item, in no particular order. Resolve by media_id.
Show child attributes
Show child attributes
The distinct events matched in this batch, fully expanded, so you can render newly-found events as the sync progresses without a second call.
Show child attributes
Show child attributes
Server-side timestamp of the last time FanFeed received media or a sync-complete
for this user, advanced by this batch. It never moves backwards, so the value on
any response is the latest mark even when concurrent batches commit out of order.
"2026-08-25T18:44:12Z"