v1 · Beta

Screenshot API

One POST request turns a public post URL into a rendered PNG. The platform is detected from the URL, and every rendering option is optional — send just the URL and you get the defaults.

The API is at v1 and currently in beta — it is ready to build on, but details may still change while we gather feedback.

Endpoint

POST
/api/v1/screenshot

Send a JSON body with Content-Type: application/json. POST is the only method this endpoint accepts.

Authentication

Pass your API key as a Bearer token. Keys are created in Dashboard → API once your account is on Pro — see Authentication.

1Authorization: Bearer sk_your_api_key

Request Body

ParameterTypeDefaultDescription
postUrl*string—URL of the post to capture. The platform is detected from the domain; anything outside X/Twitter, Bluesky, YouTube and TikTok is rejected.
responseenum"json"What comes back. "json" uploads the render and returns its URL and metadata; "image" streams the PNG bytes in the response body. One of "json", "image".
settingsobject{}Rendering options, grouped by global / twitter / bluesky / youtube / tiktok. Every field is optional and falls back to its default. Unknown fields are rejected with a 400.
1{
2 "postUrl": "https://x.com/postcaboron/status/1924847327370461555",
3 "settings": {
4 "global": {
5 "theme": "dark",
6 "platformConfig": "format:portrait",
7 "padding": 60,
8 "cardStyle": "glass",
9 "showWatermark": false
10 },
11 "twitter": {
12 "showFollowerCount": false,
13 "showViewCount": false
14 }
15 }
16}

Platform Detection

The platform comes from the postUrl domain and decides which settings group applies. A URL that matches none of these is rejected with unsupported_platform.

DomainplatformSettings group
x.com, twitter.comtwittersettings.twitter
bsky.appbskysettings.bluesky
youtube.com, youtu.beyoutubesettings.youtube
tiktok.comtiktoksettings.tiktok

Settings

Every field below is optional; anything you omit uses the default in the table. Each group is strict, so a misspelled field is a 400 instead of being silently ignored. Send only the group that matches the post you are capturing: the platform groups share field names such as showLikeCount, and sending several at once lets them collide.

settings.global

Canvas, background and typography — these apply to every platform.

ParameterTypeDefaultDescription
settings.global.platformConfigstring—Canvas preset written as "platform:layout", e.g. "twitter:single-image" or "format:portrait". Sets selectedPlatform, selectedLayout, width and height in one go, overriding any of those you send alongside it.
settings.global.themestring"light"Colour scheme of the post card itself.
settings.global.backgroundColorstring"#B4D455"Solid colour behind the card, as a hex string. Send "" for no colour. A gradient in backgroundImage paints over it.
settings.global.backgroundImagestring"linear-gradient(to top left,…CSS gradient painted behind the card, e.g. "linear-gradient(to top left, #0EA5E9, #1E3A8A)". Send "" to fall back to the solid backgroundColor.
settings.global.backgroundSizestring""CSS background-size for the gradient layer, e.g. "200% 200%". Empty leaves the browser default.
settings.global.backgroundLabelstring"Wide Matrix"Name of the background preset. Cosmetic: it is stored with the render for your dashboard history and does not change the image.
settings.global.widthnumber1200Canvas width in pixels. A selectedPlatform/selectedLayout preset (or platformConfig) overrides it; in auto mode it caps how wide the render may grow.
settings.global.paddingnumber35Space between the card and the edge of the canvas, in pixels. Applies in auto mode — fixed canvases use the preset's safe area and selectedPostSize instead.
settings.global.textSizestring"xl"Size of the post text.
settings.global.scaleFactornumber2Pixel-density multiplier for the render. 2 gives a retina-quality PNG at twice the canvas dimensions.
settings.global.backgroundImageUrlstring""Public URL of a background image. Replaces backgroundColor and backgroundImage when set. Pro.
settings.global.imageFitstring"cover"How the background image fills the canvas.
settings.global.selectedImageIdstring""ID of a PostCapture preset background image, resolved to its URL. Ignored when you send backgroundImageUrl yourself.
settings.global.customSolidColorstring"#3B82F6"Solid colour stored by the custom-background picker, as a hex string.
settings.global.customGradientColor1string"#3B82F6"First colour stop of the custom gradient, as a hex string.
settings.global.customGradientColor2string"#8B5CF6"Second colour stop of the custom gradient, as a hex string.
settings.global.customGradientDirectionstring"to right"Direction of the custom gradient, e.g. "to right" or "135deg".
settings.global.showWatermarkbooleantrueShow the PostCapture watermark. Forced on for free accounts — see the note under Plan limits.
settings.global.showShadowbooleantrueDrop shadow behind the card.
settings.global.cardStylestring"classic"Chrome drawn around the post. "frame", "sticker", "polaroid" and "tape" are Pro styles.
settings.global.postLayoutstring"standard"How the post content is arranged inside the card. "poster", "bubble", "marker", "receipt" and "magazine" are Pro layouts.
settings.global.mediaLayoutstring"row"How a post with several photos is laid out. "row" (default) lines photos up side by side at their own proportions, "auto" matches X (one or two photos in full, the first two of a larger set with a "+N" badge), "grid" shows every selected photo in a 16:9 grid, "single" shows one photo uncropped.
settings.global.mediaFitstring"crop""crop" fills each photo cell; "fit" shows the whole photo without cropping.
settings.global.mediaSelectionstring[]Which of the post's photos to include, as 0-based indices in display order, e.g. [1] or [2, 0]. Empty includes every photo.
settings.global.fontFamilystring""Font applied to the whole card. Empty keeps the platform font. Pro.
settings.global.textColorstring""Colour of the post text, as a hex string. Empty keeps the theme colour. Pro.
settings.global.linkColorstring""Colour of links and @-mentions, as a hex string. Empty keeps the theme colour. Pro.
settings.global.quoteTextColorstring""Colour of quoted-post text, as a hex string. Empty keeps the theme colour. Pro.
settings.global.selectedPlatformstring"twitter"Which family of canvas presets to size the image with. "format" gives plain aspect ratios (square, portrait, landscape) and "auto" sizes to the content.
settings.global.selectedLayoutstring"single-image"Preset within selectedPlatform, e.g. "single-image", "portrait" or "video-thumbnail". "auto" turns on auto mode.
settings.global.selectedPostSizestring"large"How large the card is drawn inside a fixed canvas. Ignored in auto mode.
settings.global.heightnumber675Canvas height in pixels. A selectedPlatform/selectedLayout preset (or platformConfig) overrides it; in auto mode it caps how tall the render may grow.
settings.global.isAutoModebooleanfalseSize the canvas to the content instead of a fixed preset, using width and height as the upper bound.

settings.twitter

ParameterTypeDefaultDescription
settings.twitter.showThreadbooleanfalseFor a reply, also render the posts above it (up to 8, oldest first) so the image shows the thread. Still counts as one screenshot.
settings.twitter.showReplybooleantrueShow the reply icon and count in the action row.
settings.twitter.showViewCountbooleantrueShow the view count.
settings.twitter.showFollowerCountbooleantrueShow the author's follower count next to their handle.
settings.twitter.showLikeCountbooleantrueShow the like count.
settings.twitter.showRepostCountbooleantrueShow the repost count.
settings.twitter.showBookmarkCountbooleantrueShow the bookmark count.
settings.twitter.showContentLabelbooleantrueShow sensitive-content and similar labels X attaches to the post.
settings.twitter.showCommunitybooleantrueShow the community a post was published in.
settings.twitter.showCommunityRolebooleanfalseShow the author's role in that community, e.g. moderator.
settings.twitter.showReplyTobooleantrueShow the "Replying to @handle" line above the post.
settings.twitter.showFullQuotebooleantrueRender the quoted post in full rather than as a compact card.
settings.twitter.showDatebooleantrueShow the post's timestamp.
settings.twitter.showMediabooleantrueInclude the post's images and video thumbnails.
settings.twitter.showLinkCardsbooleantrueRender the link preview card for URLs in the post.
settings.twitter.showQuoteMediabooleantrueInclude media attached to the quoted post.

settings.bluesky

ParameterTypeDefaultDescription
settings.bluesky.showThreadbooleanfalseFor a reply, also render the posts above it (up to 8, oldest first) so the image shows the thread. Still counts as one screenshot.
settings.bluesky.showReplybooleantrueShow the reply icon and count in the action row.
settings.bluesky.showLikeCountbooleantrueShow the like count.
settings.bluesky.showRepostCountbooleantrueShow the repost count.
settings.bluesky.showDatebooleantrueShow the post's timestamp.
settings.bluesky.showFollowerCountbooleantrueShow the author's follower count next to their handle.
settings.bluesky.showMediabooleantrueInclude the post's images.
settings.bluesky.showLinkCardsbooleantrueRender the link preview card for URLs in the post.
settings.bluesky.showQuoteMediabooleantrueInclude media attached to the quoted post.

settings.youtube

ParameterTypeDefaultDescription
settings.youtube.showPostedSincebooleantrueShow the relative upload time, e.g. "3 days ago".
settings.youtube.youtubeFormatstring"landscape"Orientation of the video card.
settings.youtube.showFollowerCountbooleantrueShow the channel's subscriber count.
settings.youtube.showLikeCountbooleantrueShow the like count.
settings.youtube.showViewCountbooleantrueShow the view count.
settings.youtube.youtubeThumbnailnumber0Which still represents the video: 0 is the uploader's thumbnail, 1-3 are the frames YouTube grabs automatically.

settings.tiktok

ParameterTypeDefaultDescription
settings.tiktok.tiktokFormatstring"landscape"Orientation of the video card.
settings.tiktok.showTikTokMusicbooleantrueShow the sound the video uses.
settings.tiktok.showTikTokDescriptionbooleantrueShow the video caption.
settings.tiktok.showFollowerCountbooleantrueShow the creator's follower count.
settings.tiktok.showLikeCountbooleantrueShow the like count.
settings.tiktok.showViewCountbooleantrueShow the view count.
settings.tiktok.showRepostCountbooleantrueShow the repost count.
settings.tiktok.showBookmarkCountbooleantrueShow the bookmark count.
settings.tiktok.tiktokThumbnailnumber0Which cover represents the video: 0 is the feed cover, higher values pick the untouched frame and any share covers. How many exist varies per video, and an out-of-range value falls back to the feed cover.

Response

JSON (default)

The render is uploaded and you get back its URL plus metadata and your remaining quota.

1{
2 "url": "https://screenshots-cdn.postcapture.com/api-renders/hK3q…/1924847327370461555-2026-07-25-11-04-12-a9f2.png",
3 "expiresAt": "2026-07-25T12:04:12.918Z",
4 "width": 2400,
5 "height": 1350,
6 "contentType": "image/png",
7 "platform": "twitter",
8 "historyId": "cm9f3k2p10001v8m2v0k4a1bd",
9 "credits": {
10 "remaining": 2989,
11 "limit": 3000,
12 "used": 11,
13 "resetsAt": "2026-08-01T00:00:00.000Z",
14 "platforms": {
15 "twitter": { "used": 11, "limit": 1000, "remaining": 989 },
16 "bsky": { "used": 0, "limit": 1000, "remaining": 1000 },
17 "youtube": { "used": 0, "limit": 1000, "remaining": 1000 },
18 "tiktok": { "used": 0, "limit": 1000, "remaining": 1000 }
19 }
20 }
21}
FieldTypeDescription
urlstringPublic link to the rendered PNG.
expiresAtstringISO timestamp one hour after the render, when the file becomes eligible for deletion.
widthnumber | nullPixel width of the PNG (canvas width × scaleFactor). Null if the dimensions could not be read.
heightnumber | nullPixel height of the PNG (canvas height × scaleFactor). Null if the dimensions could not be read.
contentTypestringAlways "image/png".
platformstringPlatform detected from postUrl: "twitter", "bsky", "youtube" or "tiktok".
historyIdstring | nullID of the entry created in your dashboard history. Null if the entry could not be written; the render still succeeded.
credits.remainingnumberScreenshots left this month after this call.
credits.limitnumberScreenshots included in your plan each month.
credits.usednumberScreenshots used this month, including this call.
credits.resetsAtstringISO timestamp when the monthly quota resets.

Image retention

The URL is valid for at least 1 hour; the image is deleted shortly after. Download the file and store it yourself if you need it for longer — expiresAt tells you when it becomes eligible for deletion. Copies of your renders stay in your dashboard history for as long as your plan keeps them.

Raw image

Send "response": "image" to get the PNG bytes in the response body instead. Nothing is uploaded, so there is no URL and no expiry to worry about — but you also get no quota metadata back.

Content-Typeimage/png
Cache-Controlpublic, max-age=60

Errors

Every error is JSON with a human-readable error and a stable code to branch on.

StatuscodeWhen
400invalid_jsonThe request body was not valid JSON.
400invalid_requestThe body failed validation — a missing postUrl, a value out of range, or an unknown settings field. The issues array names each offending path.
400unsupported_platformpostUrl is a valid URL but not an X/Twitter, Bluesky, YouTube or TikTok post.
401missing_api_keyNo Authorization: Bearer header was sent.
401invalid_api_keyThe key is unknown, disabled or expired. details carries the reason.
402quota_exceededYour monthly screenshots are used up. The body carries exceeded: true plus limit and used.
403api_access_disabledYour account is not on Pro. API access is included with Pro.
429rate_limitedMore than 10 requests in a minute. Retry after the Retry-After header (seconds) or retryAfterMs (milliseconds).
500render_failedRendering the post failed, e.g. the post is private, deleted, or the renderer timed out. No credit is used.
500storage_failedThe render succeeded but could not be stored. No credit is used — retry the request.
500internal_errorSomething unexpected failed on our side. No credit is used — retry the request.

Validation failures name every problem at once, so you can fix a payload in one pass:

1{
2 "error": "Invalid request body",
3 "code": "invalid_request",
4 "issues": [
5 { "path": "settings.global.widht", "message": "Unrecognized key(s) in object: 'widht'" },
6 { "path": "settings.global.scaleFactor", "message": "Number must be less than or equal to 4" }
7 ]
8}

Credits & Plan Limits

  • •One credit per successful call. Nothing is charged for rejected requests, and nothing is charged when rendering or storing the image fails. The quota resets on the 1st of each month.
  • •The watermark is forced on for free accounts. Sending settings.global.showWatermark: false is accepted but has no effect until you upgrade — the server sets it back on before rendering.
  • •Pro unlocks the premium looks. Premium card styles and post layouts, custom image backgrounds, and custom fonts and colours are part of the Pro plan.
  • •10 requests per minute. Shared across all of your API keys, and across v1 and the legacy endpoint — see Rate Limits.

Examples

The smallest request that works — a post URL and your key:

1curl -X POST https://postcapture.com/api/v1/screenshot \
2 -H "Authorization: Bearer $POSTCAPTURE_API_KEY" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "postUrl": "https://x.com/postcaboron/status/1924847327370461555"
6 }'

The same call with a portrait canvas, a dark glass card and a couple of counters hidden:

1curl -X POST https://postcapture.com/api/v1/screenshot \
2 -H "Authorization: Bearer $POSTCAPTURE_API_KEY" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "postUrl": "https://x.com/postcaboron/status/1924847327370461555",
6 "settings": {
7 "global": {
8 "theme": "dark",
9 "platformConfig": "format:portrait",
10 "padding": 60,
11 "cardStyle": "glass",
12 "showWatermark": false
13 },
14 "twitter": {
15 "showFollowerCount": false,
16 "showViewCount": false
17 }
18 }
19 }'

More patterns — batching, retries and every platform — are in Code Examples.

Moving from the legacy endpoint

The legacy GET /api/screenshot endpoint keeps working, but it is no longer documented. What changes when you move:

  • •GET /api/screenshot with query params becomes POST /api/v1/screenshot with a JSON body — no more URL-length ceiling on your settings.
  • •The apiKey query param becomes an Authorization: Bearer header, so keys stop showing up in URLs and logs.
  • •Flat settings become grouped: &showLikeCount=false becomes settings.twitter.showLikeCount: false.
  • •Typos are errors now. The legacy endpoint forwarded unknown params and quietly ignored them; v1 rejects them with invalid_request.
  • •You get JSON by default instead of PNG bytes. Keep the old behaviour with "response": "image".