Photo API
Read photos from the public portfolio, then queue an Instagram publish. Base URL:
https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod
Finding photos needs no API key. The two Instagram routes need an admin token. This page does not say how to get that token.
Browser calls only succeed from https://vbolshakov.photo, https://www.vbolshakov.photo, https://admin.vbolshakov.photo, and http://localhost ports 4171–4178. curl and other server-side clients can call from anywhere.
List galleries
GET /pages/visible hides the draft gallery. GET /pages includes it. Optional limit (default 100).
Each item includes photo_count and a default_photo cover. GET /pages/{page} is one gallery and omits those two fields. An unknown page is 404 { "message": "Resource not found" }.
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/pages/visible[
{
"page": "germany",
"title": "Germany",
"sort": 100,
"photo_count": 42,
"default_photo": {
"photo_id": "abc123",
"s3_key": "germany/photo1.jpg",
"urls": {
"thumbnail": "https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_800,f_webp,q_85,t_r/germany/photo1.jpg"
}
}
}
]All photos in a gallery
GET /photos/{page} returns that gallery, ordered by sort. An unknown page is 200 and [].
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/photos/germanyThe body is an array of photo objects. See One photo for the fields a publisher needs.
All photos
GET /photos returns photos across every gallery. Optional limit (default 300). That cap is a scan limit, not a sorted feed and not pagination. There is no cursor. Order is not guaranteed. For gallery order, use GET /photos/{page}.
curl "https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/photos?limit=50"Most recent 20 photos
GET /photos/recent returns photos whose created_date falls in the last 30 days, newest first. It ignores limit and days. Take the first 20 items yourself. If fewer than 20 were added in that window, the list is shorter. There is no “latest 20 of all time” route. GET /photos?limit=20 is not that.
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/photos/recent[
{
"page": "germany",
"created_date": "2026-09-20T08:12:00Z",
"photo": {
"page": "germany",
"photo_id": "abc123",
"slug": "dawn-on-the-elbe"
}
}
]Tags
GET /tags returns { tags, total }, sorted by count descending. Each tag has tag, count, url, and up to 3 recent_photos. Those photos have only photo_id, page, and s3_key — no urls map. Build a thumbnail from the key:
https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_800,f_webp,q_85,t_r/{s3_key}
GET /tags/popular is the same shape, limited to tags with a count greater than 5.
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/tags{
"tags": [
{
"tag": "dawn",
"count": 18,
"url": "https://vbolshakov.photo/tags/dawn",
"recent_photos": [
{
"photo_id": "abc123",
"page": "germany",
"s3_key": "germany/photo1.jpg"
}
]
}
],
"total": 1
}Search by one tag
GET /tags/{tag} returns photos for that tag. The tag is lowercased. One tag only: there is no multi-tag query and no text search. Optional rating keeps photos whose star_rating is at least that integer. Photos are full objects, sorted by gallery then sort. An empty tag is 400.
curl "https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/tags/dawn?rating=4"{
"tag": "dawn",
"count": 1,
"photos": [
{
"page": "germany",
"photo_id": "abc123",
"slug": "dawn-on-the-elbe",
"info": {
"title": "Dawn on the Elbe",
"ig_description": "First light on the river.",
"star_rating": 5
},
"urls": {
"seo": "https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_630,f_jpg,q_95,t_r/germany/photo1.jpg"
}
}
]
}One photo
GET /pages/{page}/{slug} returns one photo. A 32-character md5 id works in place of the slug. If the slug is stale, the body includes redirect_to, a site path such as /photo/germany/dawn-on-the-elbe. A missing photo is 404 { "message": "Resource not found" }.
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/pages/germany/dawn-on-the-elbe{
"page": "germany",
"photo_id": "abc123",
"slug": "dawn-on-the-elbe",
"sort": 100,
"tags": "dawn, river",
"width": 6048,
"height": 4024,
"info": {
"title": "Dawn on the Elbe",
"description": "First light on the river.",
"tags": "dawn, river",
"star_rating": 5,
"ig_description": "First light on the river.",
"ig_publish": {
"status": "published",
"post_id": "17895695668004550",
"permalink": "https://www.instagram.com/p/ABC123/"
}
},
"urls": {
"thumbnail": "https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_800,f_webp,q_85,t_r/germany/photo1.jpg",
"seo": "https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_630,f_jpg,q_95,t_r/germany/photo1.jpg",
"src": "https://d2xkwrs8ekvgk2.cloudfront.net/w_2000,h_1500,f_webp,q_90,t_r/germany/photo1.jpg",
"full": "https://d2xkwrs8ekvgk2.cloudfront.net/w_2500,h_1875,f_avif,q_95,t_r/germany/photo1.jpg"
}
}Before publishing, read these fields:
info.ig_descriptionis the caption. If it is missing or blank, do not publish.info.ig_publishis the Instagram state. Apost_idmeans it was already posted.statusmay bequeued,publishing,published, orfailed.urls.seois the JPEG Instagram accepts. Otherurlsvalues may be WebP or AVIF.infocan also includephoto_stocks.s3_keyis the path used to build any other size. See Image sizes.
Image sizes
Every photo urls value is a CDN address. The segment before the s3_key is the size. t_r fits the image inside the box. t_c crops it to the box.
base— 1920×1080, WebP, quality 85, resizetech— 1200×800, quality 85, resize, source formatsrc— 2000×1500, WebP, quality 90, resizeseo— 1200×630, JPEG, quality 95, resizemobile— 800×600, WebP, quality 90, resizemobile_thumbnail— 400×300, WebP, quality 85, cropthumbnail— 1200×800, WebP, quality 85, resizepopupandfull— 2500×1875, AVIF, quality 95, resizeretina— 2400×1350, AVIF, quality 90, resize
Tag-index thumbnails are not in urls. Build one from s3_key:
https://d2xkwrs8ekvgk2.cloudfront.net/w_1200,h_800,f_webp,q_85,t_r/{s3_key}
Custom size
Request any size by putting the same parameters in front of the photo’s s3_key. The CDN creates it on the first request and then serves that file.
https://d2xkwrs8ekvgk2.cloudfront.net/w_1080,h_1350,f_jpg,q_90,t_c/germany/photo1.jpgParameters are comma-separated:
w_width in pixels, up to 4000h_height in pixels, up to 4000. Leave it out to set the width only and keep the aspect ratio.q_quality, from 1 to 100f_format:jpg,webp,png, oravift_fit:rresize inside the box,ccrop to the box,wpadbg_pad color as hex, used witht_w
Width only, WebP, quality 85:
https://d2xkwrs8ekvgk2.cloudfront.net/w_1600,f_webp,q_85,t_r/germany/photo1.jpgInstagram needs a JPEG. Use urls.seo, or a custom f_jpg URL. AVIF and WebP are not accepted as the published image.
Publish to Instagram
POST /instagram/publish queues a photo. It requires an admin token. The body is page and photo_id.
Success is 202, not a permalink. The post is queued. Poll the status route for the outcome.
curl -X POST https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/instagram/publish \
-H "Content-Type: application/json" \
-d '{"page":"germany","photo_id":"abc123"}'{
"success": true,
"queued": true,
"page": "germany",
"photo_id": "abc123"
}Failures:
- 400
MISSING_PARAMS—pageorphoto_idis missing. - 400
MISSING_DESCRIPTION— noig_description. - 400
ALREADY_PUBLISHED— includesexisting_post_id. - 400
NO_IMAGE_URL— no image to send. - 404
NOT_FOUND— no such photo. - 409
PUBLISH_IN_FLIGHT— a publish for this photo is already running. - 500
CONFIG_ERROR— Instagram is not configured. - 502
TRIGGER_FAILED— the publish could not be queued.
Publish status
GET /instagram/status/{page}/{photo_id} requires an admin token. Poll until published is true, or until instagram.status is failed. A missing photo is 404 { "success": false, "error": "NOT_FOUND", "message": "Photo not found" }.
curl https://4kapqxsnde.execute-api.eu-central-1.amazonaws.com/prod/instagram/status/germany/abc123{
"success": true,
"page": "germany",
"photo_id": "abc123",
"published": false,
"instagram": {
"status": "queued"
}
}