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
/api/v1/screenshotSend 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 API access is enabled for your account — see Authentication.
1Authorization: Bearer sk_your_api_key
Request Body
| Parameter | Type | Default | Description |
|---|---|---|---|
| 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. |
| response | enum | "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". |
| settings | object | {} | 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": false10 },11 "twitter": {12 "showFollowerCount": false,13 "showViewCount": false14 }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.
| Domain | platform | Settings group |
|---|---|---|
| x.com, twitter.com | settings.twitter | |
| bsky.app | bsky | settings.bluesky |
| youtube.com, youtu.be | youtube | settings.youtube |
| tiktok.com | tiktok | settings.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.
| Parameter | Type | Default | Description |
|---|---|---|---|
| settings.global.platformConfig | string | — | 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.theme | string | "light" | Colour scheme of the post card itself. |
| settings.global.backgroundColor | string | "#B4D455" | Solid colour behind the card, as a hex string. Send "" for no colour. A gradient in backgroundImage paints over it. |
| settings.global.backgroundImage | string | "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.backgroundSize | string | "" | CSS background-size for the gradient layer, e.g. "200% 200%". Empty leaves the browser default. |
| settings.global.backgroundLabel | string | "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.width | number | 1200 | Canvas width in pixels. A selectedPlatform/selectedLayout preset (or platformConfig) overrides it; in auto mode it caps how wide the render may grow. Range 100-5000. Whole numbers only. |
| settings.global.padding | number | 35 | Space 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. Range 0-500. |
| settings.global.textSize | enum | "xl" | Size of the post text. One of "sm", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl". |
| settings.global.scaleFactor | number | 2 | Pixel-density multiplier for the render. 2 gives a retina-quality PNG at twice the canvas dimensions. Range 1-4. |
| settings.global.backgroundImageUrl | string | "" | Public URL of a background image. Replaces backgroundColor and backgroundImage when set. Pro. |
| settings.global.imageFit | enum | "cover" | How the background image fills the canvas. One of "cover", "contain", "fill". |
| settings.global.selectedImageId | string | "" | ID of a PostCapture preset background image, resolved to its URL. Ignored when you send backgroundImageUrl yourself. |
| settings.global.customSolidColor | string | "#3B82F6" | Solid colour stored by the custom-background picker, as a hex string. |
| settings.global.customGradientColor1 | string | "#3B82F6" | First colour stop of the custom gradient, as a hex string. |
| settings.global.customGradientColor2 | string | "#8B5CF6" | Second colour stop of the custom gradient, as a hex string. |
| settings.global.customGradientDirection | string | "to right" | Direction of the custom gradient, e.g. "to right" or "135deg". |
| settings.global.showWatermark | boolean | true | Show the PostCapture watermark. Forced on for free accounts — see the note under Plan limits. |
| settings.global.showShadow | boolean | true | Drop shadow behind the card. |
| settings.global.cardStyle | enum | "classic" | Chrome drawn around the post. "frame", "sticker", "polaroid" and "tape" are Pro styles. One of "classic", "glass", "window", "browser", "frame", "sticker", "polaroid", "tape", "minimal". |
| settings.global.postLayout | enum | "standard" | How the post content is arranged inside the card. "poster", "bubble", "marker", "receipt" and "magazine" are Pro layouts. One of "standard", "poster", "bubble", "marker", "quote", "statement", "chat", "split", "receipt", "magazine". |
| settings.global.fontFamily | string | "" | Font applied to the whole card. Empty keeps the platform font. Pro. |
| settings.global.textColor | string | "" | Colour of the post text, as a hex string. Empty keeps the theme colour. Pro. |
| settings.global.linkColor | string | "" | Colour of links and @-mentions, as a hex string. Empty keeps the theme colour. Pro. |
| settings.global.quoteTextColor | string | "" | Colour of quoted-post text, as a hex string. Empty keeps the theme colour. Pro. |
| settings.global.selectedPlatform | enum | "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. One of "auto", "format", "twitter", "facebook", "instagram", "linkedin", "youtube", "pinterest", "reddit", "snapchat", "tiktok". |
| settings.global.selectedLayout | string | "single-image" | Preset within selectedPlatform, e.g. "single-image", "portrait" or "video-thumbnail". "auto" turns on auto mode. |
| settings.global.selectedPostSize | enum | "large" | How large the card is drawn inside a fixed canvas. Ignored in auto mode. One of "small", "medium", "large". |
| settings.global.height | number | 675 | Canvas height in pixels. A selectedPlatform/selectedLayout preset (or platformConfig) overrides it; in auto mode it caps how tall the render may grow. Range 100-5000. Whole numbers only. |
| settings.global.isAutoMode | boolean | false | Size the canvas to the content instead of a fixed preset, using width and height as the upper bound. |
settings.twitter
| Parameter | Type | Default | Description |
|---|---|---|---|
| settings.twitter.showThread | boolean | false | Render the whole thread instead of the single post. |
| settings.twitter.showReply | boolean | true | Show the reply icon and count in the action row. |
| settings.twitter.showViewCount | boolean | true | Show the view count. |
| settings.twitter.showFollowerCount | boolean | true | Show the author's follower count next to their handle. |
| settings.twitter.showLikeCount | boolean | true | Show the like count. |
| settings.twitter.showRepostCount | boolean | true | Show the repost count. |
| settings.twitter.showBookmarkCount | boolean | true | Show the bookmark count. |
| settings.twitter.showContentLabel | boolean | true | Show sensitive-content and similar labels X attaches to the post. |
| settings.twitter.showCommunity | boolean | true | Show the community a post was published in. |
| settings.twitter.showCommunityRole | boolean | false | Show the author's role in that community, e.g. moderator. |
| settings.twitter.showReplyTo | boolean | true | Show the "Replying to @handle" line above the post. |
| settings.twitter.showFullQuote | boolean | true | Render the quoted post in full rather than as a compact card. |
| settings.twitter.showDate | boolean | true | Show the post's timestamp. |
| settings.twitter.showMedia | boolean | true | Include the post's images and video thumbnails. |
| settings.twitter.showLinkCards | boolean | true | Render the link preview card for URLs in the post. |
| settings.twitter.showQuoteMedia | boolean | true | Include media attached to the quoted post. |
settings.bluesky
| Parameter | Type | Default | Description |
|---|---|---|---|
| settings.bluesky.showReply | boolean | true | Show the reply icon and count in the action row. |
| settings.bluesky.showLikeCount | boolean | true | Show the like count. |
| settings.bluesky.showRepostCount | boolean | true | Show the repost count. |
| settings.bluesky.showDate | boolean | true | Show the post's timestamp. |
| settings.bluesky.showFollowerCount | boolean | true | Show the author's follower count next to their handle. |
| settings.bluesky.showMedia | boolean | true | Include the post's images. |
| settings.bluesky.showLinkCards | boolean | true | Render the link preview card for URLs in the post. |
| settings.bluesky.showQuoteMedia | boolean | true | Include media attached to the quoted post. |
settings.youtube
| Parameter | Type | Default | Description |
|---|---|---|---|
| settings.youtube.showPostedSince | boolean | true | Show the relative upload time, e.g. "3 days ago". |
| settings.youtube.youtubeFormat | enum | "landscape" | Orientation of the video card. One of "landscape", "portrait". |
| settings.youtube.showFollowerCount | boolean | true | Show the channel's subscriber count. |
| settings.youtube.showLikeCount | boolean | true | Show the like count. |
| settings.youtube.showViewCount | boolean | true | Show the view count. |
| settings.youtube.youtubeThumbnail | number | 0 | Which still represents the video: 0 is the uploader's thumbnail, 1-3 are the frames YouTube grabs automatically. Range 0-3. Whole numbers only. |
settings.tiktok
| Parameter | Type | Default | Description |
|---|---|---|---|
| settings.tiktok.tiktokFormat | enum | "landscape" | Orientation of the video card. One of "landscape", "portrait". |
| settings.tiktok.showTikTokMusic | boolean | true | Show the sound the video uses. |
| settings.tiktok.showTikTokDescription | boolean | true | Show the video caption. |
| settings.tiktok.showFollowerCount | boolean | true | Show the creator's follower count. |
| settings.tiktok.showLikeCount | boolean | true | Show the like count. |
| settings.tiktok.showViewCount | boolean | true | Show the view count. |
| settings.tiktok.showRepostCount | boolean | true | Show the repost count. |
| settings.tiktok.showBookmarkCount | boolean | true | Show the bookmark count. |
| settings.tiktok.tiktokThumbnail | number | 0 | Which 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. Range 0-10. Whole numbers only. |
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}
| Field | Type | Description |
|---|---|---|
| url | string | Public link to the rendered PNG. |
| expiresAt | string | ISO timestamp one hour after the render, when the file becomes eligible for deletion. |
| width | number | null | Pixel width of the PNG (canvas width × scaleFactor). Null if the dimensions could not be read. |
| height | number | null | Pixel height of the PNG (canvas height × scaleFactor). Null if the dimensions could not be read. |
| contentType | string | Always "image/png". |
| platform | string | Platform detected from postUrl: "twitter", "bsky", "youtube" or "tiktok". |
| historyId | string | null | ID of the entry created in your dashboard history. Null if the entry could not be written; the render still succeeded. |
| credits.remaining | number | Screenshots left this month after this call. |
| credits.limit | number | Screenshots included in your plan each month. |
| credits.used | number | Screenshots used this month, including this call. |
| credits.resetsAt | string | ISO timestamp when the monthly quota resets. |
| credits.platforms | object | undefined | Per-platform usage this month, keyed by platform ("twitter", "bsky", "youtube", "tiktok"), each with used, limit and remaining. Present only on plans that cap how much of the monthly quota a single platform may use — the API plan allows 1000 per platform out of its 3000. Omitted entirely on plans with no such cap. |
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-Type | image/png |
| Cache-Control | public, max-age=60 |
Errors
Every error is JSON with a human-readable error and a stable code to branch on.
| Status | code | When |
|---|---|---|
| 400 | invalid_json | The request body was not valid JSON. |
| 400 | invalid_request | The body failed validation — a missing postUrl, a value out of range, or an unknown settings field. The issues array names each offending path. |
| 400 | unsupported_platform | postUrl is a valid URL but not an X/Twitter, Bluesky, YouTube or TikTok post. |
| 401 | missing_api_key | No Authorization: Bearer header was sent. |
| 401 | invalid_api_key | The key is unknown, disabled or expired. details carries the reason. |
| 402 | quota_exceeded | Your monthly screenshots are used up. The body carries exceeded: true plus limit and used. |
| 402 | platform_quota_exceeded | This platform has used its share of the monthly quota, though screenshots remain overall. The body carries the platform, its limit and used, plus a credits block whose platforms map shows where you still have room. |
| 403 | api_access_disabled | API access is not active for your account. It is included with the API plan. |
| 429 | rate_limited | More than 10 requests in a minute. Retry after the Retry-After header (seconds) or retryAfterMs (milliseconds). |
| 500 | unknown_user | The key verified but no account could be resolved for it. Contact support if you see this. |
| 500 | render_failed | Rendering the post failed, e.g. the post is private, deleted, or the renderer timed out. No credit is used. |
| 500 | storage_failed | The render succeeded but could not be stored. 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: falseis 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": false13 },14 "twitter": {15 "showFollowerCount": false,16 "showViewCount": false17 }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/screenshotwith query params becomesPOST /api/v1/screenshotwith a JSON body — no more URL-length ceiling on your settings. - •The
apiKeyquery param becomes anAuthorization: Bearerheader, so keys stop showing up in URLs and logs. - •Flat settings become grouped:
&showLikeCount=falsebecomessettings.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".