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"
      }
    }
  }
]

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/germany

The 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_description is the caption. If it is missing or blank, do not publish.
  • info.ig_publish is the Instagram state. A post_id means it was already posted. status may be queued, publishing, published, or failed.
  • urls.seo is the JPEG Instagram accepts. Other urls values may be WebP or AVIF.
  • info can also include photo_stocks.
  • s3_key is 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, resize
  • tech — 1200×800, quality 85, resize, source format
  • src — 2000×1500, WebP, quality 90, resize
  • seo — 1200×630, JPEG, quality 95, resize
  • mobile — 800×600, WebP, quality 90, resize
  • mobile_thumbnail — 400×300, WebP, quality 85, crop
  • thumbnail — 1200×800, WebP, quality 85, resize
  • popup and full — 2500×1875, AVIF, quality 95, resize
  • retina — 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.jpg

Parameters are comma-separated:

  • w_ width in pixels, up to 4000
  • h_ height in pixels, up to 4000. Leave it out to set the width only and keep the aspect ratio.
  • q_ quality, from 1 to 100
  • f_ format: jpg, webp, png, or avif
  • t_ fit: r resize inside the box, c crop to the box, w pad
  • bg_ pad color as hex, used with t_w

Width only, WebP, quality 85:

https://d2xkwrs8ekvgk2.cloudfront.net/w_1600,f_webp,q_85,t_r/germany/photo1.jpg

Instagram 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 — page or photo_id is missing.
  • 400 MISSING_DESCRIPTION — no ig_description.
  • 400 ALREADY_PUBLISHED — includes existing_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"
  }
}