{"openapi":"3.1.0","info":{"title":"ScreenshotBuddy API","version":"1.0.0","description":"Render a web page as an image or a PDF, and read where the account stands. A successful render is the image or the PDF itself, so treat it as binary and write it to a file; every error is JSON in one envelope, carrying a machine readable `code` to branch on, a human `message`, a `request_id` to quote at support, and, for a validation failure, an `errors` object. Every render costs one credit, and a failed render is refunded.\n\nA render can also be asked for and collected later. `POST /renders` takes a set of captures, queues them and answers straight away; the results are collected by polling `GET /renders/{id}` or by one signed webhook when the whole batch is done. Every item runs the same billed lifecycle as a synchronous call, so nothing about permissions, credits, caching or rate limits changes by asking asynchronously."},"servers":[{"url":"https://api.screenshotbuddy.io/v1","description":"The base URL of the API."}],"security":[{"bearerToken":[]}],"components":{"securitySchemes":{"bearerToken":{"type":"http","scheme":"bearer","description":"An API token from your dashboard, sent as `Authorization: Bearer <token>`."}},"schemas":{"Error":{"type":"object","description":"The one shape every error answer takes.","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"The machine readable reason the request was refused. Branch on this rather than on the message, which is written for a person and may be reworded at any time.","enum":["unauthenticated","invalid_signature","signed_url_expired","email_unverified","missing_ability","no_active_plan","credit_limit_reached","validation_failed","invalid_url","blocked_host","target_unreachable","rate_limited","render_failed","server_error","upstream_error","upstream_quota_exceeded","upstream_rate_limited","render_timeout","not_found","method_not_allowed","api_moved"]},"message":{"type":"string","description":"The reason in words, for a person to read. Not a stable contract."},"errors":{"type":"object","description":"Present only on a validation shaped failure. Keyed by the query parameter at fault, with the messages for that parameter.","additionalProperties":{"type":"array","items":{"type":"string"}}},"request_id":{"type":"string","format":"uuid","description":"Identifies this one answer, and is in the server log for the request that produced it. Quote it in a support request."}}},"RenderBatch":{"type":"object","description":"A batch and every item in it.","required":["id","status","created_at","finished_at","prunes_at","webhook","items"],"properties":{"id":{"type":"string","description":"The id of the batch. It is what `GET /renders/{id}` is called with, and it travels on every webhook delivery as `X-Webhook-Id`."},"status":{"type":"string","enum":["pending","complete"],"description":"`complete` once every item is terminal, `pending` until then. It is derived from the items rather than from a stamp, so a poll can never report a finished batch as still running."},"created_at":{"type":"string","format":"date-time","description":"When the batch was submitted."},"finished_at":{"type":["string","null"],"format":"date-time","description":"When the last item finished, or null while any of them is still running."},"prunes_at":{"type":"string","format":"date-time","description":"When this record is deleted, 7 days after it finishes. A batch that is still running is told the earliest it could go, which moves out as the batch takes longer. After it, the id answers `404`."},"webhook":{"type":["object","null"],"description":"The delivery this batch asked for, or null when it asked for none. A delivery carries `X-Webhook-Id`, `X-Webhook-Timestamp` and `X-Webhook-Signature`, the last being the lowercase hex HMAC-SHA256 of `\"snap-webhook-v1\\n\" + timestamp + \"\\n\" + the raw body`, keyed with the signing secret of the token that submitted the batch. See [Batch and async renders](https://api.screenshotbuddy.io/documentation/batch-renders).","properties":{"url":{"type":"string","format":"uri","description":"Where the finished batch is posted."},"delivered_at":{"type":["string","null"],"format":"date-time","description":"When a delivery was acknowledged with a 2xx, or null while none has been. It stays null when every attempt failed; the batch is still here to be polled."}}},"items":{"type":"array","description":"The renders, in the order they were submitted.","items":{"type":"object","required":["id","url","status","error","etag","target_status","expires_at","signed_url","credit_cost"],"properties":{"id":{"type":"string","description":"The id of this item, stable for the life of the batch."},"url":{"type":"string","format":"uri","description":"The page this item renders."},"status":{"type":"string","enum":["queued","rendering","done","failed"],"description":"Where this item has got to. `done` and `failed` are terminal; anything else means it is still on its way. The set may grow, so treat a status you do not recognise as not finished yet rather than as an error."},"error":{"type":["object","null"],"description":"Why this item failed, or null. It carries the same `code` and `message` an error envelope carries, from the same published set, so one branch handles a synchronous refusal and an asynchronous one.","required":["code","message"],"properties":{"code":{"type":"string","description":"The machine readable reason. Branch on this rather than on the message.","enum":["unauthenticated","invalid_signature","signed_url_expired","email_unverified","missing_ability","no_active_plan","credit_limit_reached","validation_failed","invalid_url","blocked_host","target_unreachable","rate_limited","render_failed","server_error","upstream_error","upstream_quota_exceeded","upstream_rate_limited","render_timeout","not_found","method_not_allowed","api_moved"]},"message":{"type":"string","description":"The reason in words. Not a stable contract."}}},"etag":{"type":["string","null"],"description":"The entity tag of the rendering, once there is one. It is the same value the rendered answer carries, so it can go straight into an `If-None-Match` on a later request."},"target_status":{"type":["integer","null"],"description":"The HTTP status the captured page itself answered, the same value the `X-Target-Status` header carries on a synchronous answer. A page that answered `404` or `403` still renders and still costs a credit, so this is what tells a finished item apart from a finished item that captured a login wall. Null means it is not known rather than `200`: an item that has not finished, a PDF item, or one answered out of a cache entry stored before this was recorded."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the cached rendering lapses. After it, the same request renders the page again and costs a credit again."},"signed_url":{"type":["string","null"],"format":"uri","description":"A link that fetches this rendering, present only when the item is `done`, the cached copy is still there, and the polling token has a signing secret to mint one with. Its own expiry never outlives the cached copy, so opening it is always a hit rather than a fresh render nobody asked for. Null is not a failure: repeat the identical `GET /snap` request instead, which is a hit too."},"credit_cost":{"type":["integer","null"],"description":"What this item cost: `1` for a render, `0` for a cache hit and for a failure, which is refunded. Null until the item is terminal, because an item that has not run yet has not decided which of the two it will be."}}}}}}}},"paths":{"/snap":{"get":{"operationId":"snap","summary":"Render a web page as an image or a PDF.","description":"Renders the page at `url` and returns the file. Sending `pdf=true` switches the endpoint to PDF mode, which accepts a different set of parameters: the `x-modes` list on each parameter says which modes accept it, and a parameter sent to a mode that does not accept it is rejected with a 422 rather than ignored.\n\nAn identical repeat of a request is answered with the rendering already made for it, for no credit, marked `X-Cache: HIT`. Every rendered answer carries an `ETag`; sending it back on the next request as `If-None-Match` is answered `304` with no body whenever the bytes still match it. A `304` the cache could answer costs neither a credit nor the download; one that had to render the page to find out costs the render, and the download is what it saves. Send `cache=0` to render the page again instead, and `cacheTtl` to set how long a rendering is kept.\n\n`cache=0` and `If-None-Match` together are therefore a change monitor: the page is loaded every time, a `200` means it looks different and carries the new capture, and a `304` means it does not.\n\nThe account is held to two per-minute limits rather than one. A request that has to render, a cache miss and a `cache=0` request alike, is counted against the render limit of its plan. An answer served out of the cache is counted against a separate flat limit of 300 per minute, so a burst of repeats cannot exhaust the renders. A `304` answered from the cache is counted against neither, a `304` that had to render is counted against the render limit like the `200` it replaces, and a request refused for its validation or its permissions before the cache is consulted is counted against neither.","security":[{"bearerToken":[]}],"parameters":[{"name":"url","in":"query","required":true,"description":"The URL of the page to render. It must start with `http://` or `https://`, be at most `2048` characters long, and point at a publicly reachable host. Private, loopback and internal addresses are refused.","schema":{"type":"string"},"x-modes":["screenshot","pdf"]},{"name":"fullPage","in":"query","required":false,"description":"Whether to capture the entire scrollable page instead of just the viewport. Defaults to `false`. Screenshots only; sending it with `pdf` is rejected, because a PDF always prints the whole document.","schema":{"type":"boolean","default":false},"x-modes":["screenshot"]},{"name":"pdf","in":"query","required":false,"description":"Set this to `true` to render a PDF instead of an image. It changes which other parameters are accepted, so see [creating PDFs](https://api.screenshotbuddy.io/documentation/creating-pdfs) for the options that belong to that mode. Defaults to `false`.","schema":{"type":"boolean","default":false},"x-modes":["screenshot","pdf"]},{"name":"landscape","in":"query","required":false,"description":"Whether to use landscape orientation. Defaults to `false` (portrait). PDFs only; sending it without `pdf` is rejected. A screenshot is shaped by `width` and `height` instead.","schema":{"type":"boolean","default":false},"x-modes":["pdf"]},{"name":"format","in":"query","required":false,"description":"The image format to return. Possible values are `png`, `jpeg` and `webp`. Defaults to `png`. Screenshots only; sending it with `pdf` is rejected. Use `paperFormat` to set the paper size of a PDF.","schema":{"type":"string","enum":["png","jpeg","webp"],"default":"png"},"x-modes":["screenshot"]},{"name":"quality","in":"query","required":false,"description":"The quality of the image, between `1` and `100`. It applies to lossy formats only, so `format` has to be `jpeg` or `webp`; sending it with `png` is rejected. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":100},"x-modes":["screenshot"]},{"name":"paperFormat","in":"query","required":false,"description":"The paper format. Possible values are `letter`, `legal`, `tabloid`, `ledger`, and `a0` through `a6`. Defaults to `a4`. PDFs only; sending it without `pdf` is rejected. Use `format` to set the image format of a screenshot.","schema":{"type":"string","enum":["letter","legal","tabloid","ledger","a0","a1","a2","a3","a4","a5","a6"],"default":"a4"},"x-modes":["pdf"]},{"name":"width","in":"query","required":false,"description":"The viewport width of a screenshot, or the paper width of a PDF, between `1` and `10000`. Must be used together with `height`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","schema":{"type":"number","minimum":1,"maximum":10000},"x-modes":["screenshot","pdf"],"x-requires-together":["height"]},{"name":"height","in":"query","required":false,"description":"The viewport height of a screenshot, or the paper height of a PDF, between `1` and `10000`. Must be used together with `width`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","schema":{"type":"number","minimum":1,"maximum":10000},"x-modes":["screenshot","pdf"],"x-requires-together":["width"]},{"name":"marginTop","in":"query","required":false,"description":"Top margin, between `0` and `1000`, in the unit set by `marginUnit`. All four margins (`marginTop`, `marginRight`, `marginBottom`, `marginLeft`) have to be set together. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginRight","marginBottom","marginLeft"]},{"name":"marginRight","in":"query","required":false,"description":"Right margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginBottom","marginLeft"]},{"name":"marginBottom","in":"query","required":false,"description":"Bottom margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginLeft"]},{"name":"marginLeft","in":"query","required":false,"description":"Left margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginBottom"]},{"name":"marginUnit","in":"query","required":false,"description":"The unit for the margins and for a custom paper size. Possible values are `mm`, `cm`, `in` and `px`. Defaults to `mm`. PDFs only; sending it without `pdf` is rejected, because a screenshot's `width` and `height` are viewport pixels.","schema":{"type":"string","enum":["mm","cm","in","px"],"default":"mm"},"x-modes":["pdf"]},{"name":"scale","in":"query","required":false,"description":"The scale of the rendering, between `1` and `3` for screenshots and between `0.1` and `2` for PDFs. Screenshots take whole numbers only, because the browser renders at whole device scale factors; PDFs take fractions. Defaults to `1`. A value outside the range of the mode you are in is rejected.","schema":{"anyOf":[{"type":"integer","minimum":1,"maximum":3,"x-modes":["screenshot"]},{"type":"number","minimum":0.1,"maximum":2,"x-modes":["pdf"]}],"default":1},"x-modes":["screenshot","pdf"]},{"name":"delay","in":"query","required":false,"description":"How long to wait before capturing, in milliseconds, between `0` and `10000`. Useful for pages that animate on load. Screenshots only; sending it with `pdf` is rejected, because the PDF renderer has no way to wait a fixed amount of time before it prints.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"]},{"name":"selector","in":"query","required":false,"description":"A CSS selector. Only the first element that matches it is captured, instead of the page. At most `512` characters, and it may not contain single quotes, backslashes or control characters, so write attribute selectors with double quotes: `a[href=\"/pricing\"]`. It chooses what to capture, so it cannot be combined with a clip region or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string"},"x-modes":["screenshot"]},{"name":"clipX","in":"query","required":false,"description":"The distance from the left edge of the page to the region to capture, in pixels, between `0` and `10000`. All four clip parameters (`clipX`, `clipY`, `clipWidth`, `clipHeight`) have to be set together. A clip region chooses what to capture, so it cannot be combined with `selector` or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipY","clipWidth","clipHeight"]},{"name":"clipY","in":"query","required":false,"description":"The distance from the top edge of the page to the region to capture, in pixels, between `0` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipWidth","clipHeight"]},{"name":"clipWidth","in":"query","required":false,"description":"The width of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipHeight"]},{"name":"clipHeight","in":"query","required":false,"description":"The height of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipWidth"]},{"name":"omitBackground","in":"query","required":false,"description":"Whether to render the page background transparent. Defaults to `false`. The format has to be able to hold transparency, so `png` or `webp`; sending it with `jpeg` is rejected rather than answered with a black background. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"boolean","default":false},"x-modes":["screenshot"]},{"name":"waitForSelector","in":"query","required":false,"description":"A CSS selector to wait for before capturing. The render continues once an element matching it exists. At most `512` characters, and under the same character restriction as `selector`: no single quotes, backslashes or control characters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string"},"x-modes":["screenshot"]},{"name":"waitUntil","in":"query","required":false,"description":"The load event to wait for before capturing. Possible values are `load`, `domcontentloaded`, `networkidle0` (no network connections for half a second) and `networkidle2` (at most two). Defaults to `networkidle2`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string","enum":["load","domcontentloaded","networkidle0","networkidle2"],"default":"networkidle2"},"x-modes":["screenshot"]},{"name":"cache","in":"query","required":false,"description":"Whether an identical repeat of this request may be answered with the rendering we already made, and whether this rendering is kept for the next one. Defaults to `true`. A cached answer costs no credit, says so with `X-Cache: HIT`, and is counted against the account's cached-answer limit rather than against its renders; send `cache=0` to render the page again, which costs a credit and a render slot as any render does. Entries belong to your own account. See [caching](https://api.screenshotbuddy.io/documentation/caching) for the whole picture.","schema":{"type":"boolean","default":true},"x-modes":["screenshot","pdf"]},{"name":"cacheTtl","in":"query","required":false,"description":"How long the rendering is kept, in seconds, between `60` and `2592000` (thirty days). Defaults to `86400`. Sending it with `cache=0` is rejected, because there is no lifetime to set on a rendering that is not being kept.","schema":{"type":"integer","minimum":60,"maximum":2592000,"default":86400},"x-modes":["screenshot","pdf"]}],"responses":{"200":{"description":"The rendered file. The body is the image or the PDF itself rather than JSON, so write it to a file instead of parsing it. The Content-Type names the format that was rendered.","x-retryable":false,"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"X-Credits-Limit":{"description":"Credits the current period was granted. Sent on every answer to a request that authenticated.","schema":{"type":"integer"}},"X-Credits-Remaining":{"description":"Credits left to spend, counted after this request was accounted for, so a render that succeeded is already subtracted and a render that failed is already refunded. Floored at zero.","schema":{"type":"integer"}},"X-Credits-Reset":{"description":"When the credits are granted again, as a Unix timestamp in seconds.","schema":{"type":"integer"}},"X-Cache":{"description":"Whether this answer came out of the cache. `HIT` means nothing was rendered and no credit was spent; `MISS` means the page was rendered for this request and the result was kept for the next one. Absent when the request sent `cache=0`, which declines the cache rather than missing it.","schema":{"type":"string","enum":["HIT","MISS"]}},"ETag":{"description":"The quoted digest of the body. Send it back as `If-None-Match` on the same request and bytes that match it are answered `304` with no body: free when the cache could answer, and costing the render but not the download when the page had to be loaded to find out.","schema":{"type":"string"}},"X-Target-Status":{"description":"The HTTP status the captured page itself answered while it was being loaded, which is not the status of this response: a rendered `404` page is a successful `200` answer carrying `X-Target-Status: 404`. Check it before storing a capture, because a login wall and a 404 page both render perfectly. Present on screenshot answers only, and absent whenever it is not known: a PDF render is never told one, and neither is an answer served out of a cache entry stored before this header existed. Absent means not known, not `200`.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"The size of the per-minute budget this answer was counted against: the account's own render limit when the page was rendered for it, the flat cached-answer limit of 300 when the answer came out of the cache. The X-Cache header on the same answer says which of the two it was. On a 429 it is the limit that refused the request. On a 304 the pair is there only when the page had to be rendered to establish the answer; a 304 the cache could give was counted against neither budget and carries neither header.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"What is left of that same budget after this request.","schema":{"type":"integer"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"The rendering is byte for byte the copy already held by the caller, named in the `If-None-Match` of the request, so no body is sent. What it costs depends on how that was established. An answer the cache could give is free: no credit, and counted against neither per-minute budget. One that had to render the page first, because the request sent `cache=0` or because the entry had lapsed, costs exactly what that render costs, a credit and a slot of the render budget, and what the caller saves is the download. `X-Cache: HIT` marks the free one; anything else was rendered, and carries the X-RateLimit headers to say so.","x-retryable":false,"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"X-Cache":{"description":"Whether this answer came out of the cache. `HIT` means nothing was rendered and no credit was spent; `MISS` means the page was rendered for this request and the result was kept for the next one. Absent when the request sent `cache=0`, which declines the cache rather than missing it.","schema":{"type":"string","enum":["HIT","MISS"]}},"ETag":{"description":"The quoted digest of the body. Send it back as `If-None-Match` on the same request and bytes that match it are answered `304` with no body: free when the cache could answer, and costing the render but not the download when the page had to be loaded to find out.","schema":{"type":"string"}},"X-Target-Status":{"description":"The HTTP status the captured page itself answered while it was being loaded, which is not the status of this response: a rendered `404` page is a successful `200` answer carrying `X-Target-Status: 404`. Check it before storing a capture, because a login wall and a 404 page both render perfectly. Present on screenshot answers only, and absent whenever it is not known: a PDF render is never told one, and neither is an answer served out of a cache entry stored before this header existed. Absent means not known, not `200`.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"The size of the per-minute budget this answer was counted against: the account's own render limit when the page was rendered for it, the flat cached-answer limit of 300 when the answer came out of the cache. The X-Cache header on the same answer says which of the two it was. On a 429 it is the limit that refused the request. On a 304 the pair is there only when the page had to be rendered to establish the answer; a 304 the cache could give was counted against neither budget and carries neither header.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"What is left of that same budget after this request.","schema":{"type":"integer"}}}},"401":{"description":"The request carried no bearer token, or a token that is unknown or revoked.","x-retryable":false,"x-error-codes":["unauthenticated"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"Unauthenticated.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"402":{"description":"The account has no active plan, or it has used every credit in the current period. The code says which of the two it is.","x-retryable":false,"x-error-codes":["no_active_plan","credit_limit_reached"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"X-Credits-Limit":{"description":"Credits the current period was granted. Sent on every answer to a request that authenticated.","schema":{"type":"integer"}},"X-Credits-Remaining":{"description":"Credits left to spend, counted after this request was accounted for, so a render that succeeded is already subtracted and a render that failed is already refunded. Floored at zero.","schema":{"type":"integer"}},"X-Credits-Reset":{"description":"When the credits are granted again, as a Unix timestamp in seconds.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credit_limit_reached","message":"You have reached the credit limit of your plan.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"403":{"description":"The email address of the account has not been verified yet, or the token is not allowed to ask for the mode the request is in. The code says which of the two it is: a token carries a permission per mode, and one narrowed to screenshots cannot print a PDF.","x-retryable":false,"x-error-codes":["email_unverified","missing_ability"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"email_unverified","message":"You must verify your email address.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"422":{"description":"A query parameter did not validate, or the target URL could not be reached or loaded (\"The target URL could not be reached or loaded. Check that it is publicly available and try again.\"). The errors object names the parameter at fault. No credit is charged.","x-retryable":false,"x-error-codes":["validation_failed","invalid_url","blocked_host","target_unreachable"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_url","message":"The url query parameter is required.","errors":{"url":["The url query parameter is required."]},"request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"429":{"description":"The account went over one of its two per-minute limits: the renders its plan allows, or the separate and far larger number of answers it may be served out of the cache. The X-RateLimit headers on this answer describe the limit that refused it. Wait for the number of seconds in the Retry-After header before sending the request again.","x-retryable":true,"x-error-codes":["rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"X-RateLimit-Limit":{"description":"The size of the per-minute budget this answer was counted against: the account's own render limit when the page was rendered for it, the flat cached-answer limit of 300 when the answer came out of the cache. The X-Cache header on the same answer says which of the two it was. On a 429 it is the limit that refused the request. On a 304 the pair is there only when the page had to be rendered to establish the answer; a 304 the cache could give was counted against neither budget and carries neither header.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"What is left of that same budget after this request.","schema":{"type":"integer"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"Too Many Attempts.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"500":{"description":"The render failed for a reason we did not recognise. The credit is refunded.","x-retryable":false,"x-error-codes":["render_failed","server_error"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"render_failed","message":"Server error while taking screenshot.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"502":{"description":"The rendering service failed to process the request. The credit is refunded and the request is worth sending again.","x-retryable":true,"x-error-codes":["upstream_error","upstream_quota_exceeded"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"upstream_error","message":"The rendering service failed to process this request. Please try again shortly.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"503":{"description":"The rendering service was momentarily busy and stayed busy across our own retries. The credit is refunded. Unlike the 502 this is not a failure of anything: wait the few seconds in the Retry-After header and the same request goes through.","x-retryable":true,"x-error-codes":["upstream_rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"upstream_rate_limited","message":"The rendering service is momentarily busy. Please try again in a few seconds.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"504":{"description":"The render did not finish within its time budget. The credit is refunded and the request is worth sending again.","x-retryable":true,"x-error-codes":["render_timeout"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"render_timeout","message":"The render did not complete in time. Please try again.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}}}}},"/snap/signed":{"get":{"operationId":"snapSigned","summary":"Render a web page from a URL that carries its own proof of authorisation.","description":"The same render as `/snap`, authenticated by an HMAC in the query string instead of by a bearer token, so the URL can be put straight in an `<img src>` or behind a CDN without the token going with it. Every parameter of `/snap` applies unchanged, and so does everything after authentication: the token's permissions, the credit it costs, the cache and both of the account's rate limits. Thumbnails embedded from signed URLs are normally cache hits, so a page of them is paced by the cached-answer limit rather than by the account's renders.\n\nSign the canonical form of the query with the signing secret of the token, which is shown once under Settings, API tokens: drop `signature`, sort the remaining parameters bytewise by name, percent encode both halves of every pair, join them with `&`, prefix the result with `snap-signed-v1` and a newline, and take the lowercase hex HMAC-SHA256 of that with the secret as the key. Every parameter is covered, so anything appended to the URL afterwards invalidates it. The [signed URLs](https://api.screenshotbuddy.io/documentation/signed-urls) page has the algorithm spelled out with a worked example and reference implementations.\n\nThis operation takes no bearer token: sending one changes nothing, because the signature is what authenticates.","security":[],"parameters":[{"name":"url","in":"query","required":true,"description":"The URL of the page to render. It must start with `http://` or `https://`, be at most `2048` characters long, and point at a publicly reachable host. Private, loopback and internal addresses are refused.","schema":{"type":"string"},"x-modes":["screenshot","pdf"]},{"name":"fullPage","in":"query","required":false,"description":"Whether to capture the entire scrollable page instead of just the viewport. Defaults to `false`. Screenshots only; sending it with `pdf` is rejected, because a PDF always prints the whole document.","schema":{"type":"boolean","default":false},"x-modes":["screenshot"]},{"name":"pdf","in":"query","required":false,"description":"Set this to `true` to render a PDF instead of an image. It changes which other parameters are accepted, so see [creating PDFs](https://api.screenshotbuddy.io/documentation/creating-pdfs) for the options that belong to that mode. Defaults to `false`.","schema":{"type":"boolean","default":false},"x-modes":["screenshot","pdf"]},{"name":"landscape","in":"query","required":false,"description":"Whether to use landscape orientation. Defaults to `false` (portrait). PDFs only; sending it without `pdf` is rejected. A screenshot is shaped by `width` and `height` instead.","schema":{"type":"boolean","default":false},"x-modes":["pdf"]},{"name":"format","in":"query","required":false,"description":"The image format to return. Possible values are `png`, `jpeg` and `webp`. Defaults to `png`. Screenshots only; sending it with `pdf` is rejected. Use `paperFormat` to set the paper size of a PDF.","schema":{"type":"string","enum":["png","jpeg","webp"],"default":"png"},"x-modes":["screenshot"]},{"name":"quality","in":"query","required":false,"description":"The quality of the image, between `1` and `100`. It applies to lossy formats only, so `format` has to be `jpeg` or `webp`; sending it with `png` is rejected. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":100},"x-modes":["screenshot"]},{"name":"paperFormat","in":"query","required":false,"description":"The paper format. Possible values are `letter`, `legal`, `tabloid`, `ledger`, and `a0` through `a6`. Defaults to `a4`. PDFs only; sending it without `pdf` is rejected. Use `format` to set the image format of a screenshot.","schema":{"type":"string","enum":["letter","legal","tabloid","ledger","a0","a1","a2","a3","a4","a5","a6"],"default":"a4"},"x-modes":["pdf"]},{"name":"width","in":"query","required":false,"description":"The viewport width of a screenshot, or the paper width of a PDF, between `1` and `10000`. Must be used together with `height`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","schema":{"type":"number","minimum":1,"maximum":10000},"x-modes":["screenshot","pdf"],"x-requires-together":["height"]},{"name":"height","in":"query","required":false,"description":"The viewport height of a screenshot, or the paper height of a PDF, between `1` and `10000`. Must be used together with `width`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","schema":{"type":"number","minimum":1,"maximum":10000},"x-modes":["screenshot","pdf"],"x-requires-together":["width"]},{"name":"marginTop","in":"query","required":false,"description":"Top margin, between `0` and `1000`, in the unit set by `marginUnit`. All four margins (`marginTop`, `marginRight`, `marginBottom`, `marginLeft`) have to be set together. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginRight","marginBottom","marginLeft"]},{"name":"marginRight","in":"query","required":false,"description":"Right margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginBottom","marginLeft"]},{"name":"marginBottom","in":"query","required":false,"description":"Bottom margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginLeft"]},{"name":"marginLeft","in":"query","required":false,"description":"Left margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","schema":{"type":"number","minimum":0,"maximum":1000},"x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginBottom"]},{"name":"marginUnit","in":"query","required":false,"description":"The unit for the margins and for a custom paper size. Possible values are `mm`, `cm`, `in` and `px`. Defaults to `mm`. PDFs only; sending it without `pdf` is rejected, because a screenshot's `width` and `height` are viewport pixels.","schema":{"type":"string","enum":["mm","cm","in","px"],"default":"mm"},"x-modes":["pdf"]},{"name":"scale","in":"query","required":false,"description":"The scale of the rendering, between `1` and `3` for screenshots and between `0.1` and `2` for PDFs. Screenshots take whole numbers only, because the browser renders at whole device scale factors; PDFs take fractions. Defaults to `1`. A value outside the range of the mode you are in is rejected.","schema":{"anyOf":[{"type":"integer","minimum":1,"maximum":3,"x-modes":["screenshot"]},{"type":"number","minimum":0.1,"maximum":2,"x-modes":["pdf"]}],"default":1},"x-modes":["screenshot","pdf"]},{"name":"delay","in":"query","required":false,"description":"How long to wait before capturing, in milliseconds, between `0` and `10000`. Useful for pages that animate on load. Screenshots only; sending it with `pdf` is rejected, because the PDF renderer has no way to wait a fixed amount of time before it prints.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"]},{"name":"selector","in":"query","required":false,"description":"A CSS selector. Only the first element that matches it is captured, instead of the page. At most `512` characters, and it may not contain single quotes, backslashes or control characters, so write attribute selectors with double quotes: `a[href=\"/pricing\"]`. It chooses what to capture, so it cannot be combined with a clip region or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string"},"x-modes":["screenshot"]},{"name":"clipX","in":"query","required":false,"description":"The distance from the left edge of the page to the region to capture, in pixels, between `0` and `10000`. All four clip parameters (`clipX`, `clipY`, `clipWidth`, `clipHeight`) have to be set together. A clip region chooses what to capture, so it cannot be combined with `selector` or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipY","clipWidth","clipHeight"]},{"name":"clipY","in":"query","required":false,"description":"The distance from the top edge of the page to the region to capture, in pixels, between `0` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":0,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipWidth","clipHeight"]},{"name":"clipWidth","in":"query","required":false,"description":"The width of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipHeight"]},{"name":"clipHeight","in":"query","required":false,"description":"The height of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"integer","minimum":1,"maximum":10000},"x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipWidth"]},{"name":"omitBackground","in":"query","required":false,"description":"Whether to render the page background transparent. Defaults to `false`. The format has to be able to hold transparency, so `png` or `webp`; sending it with `jpeg` is rejected rather than answered with a black background. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"boolean","default":false},"x-modes":["screenshot"]},{"name":"waitForSelector","in":"query","required":false,"description":"A CSS selector to wait for before capturing. The render continues once an element matching it exists. At most `512` characters, and under the same character restriction as `selector`: no single quotes, backslashes or control characters. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string"},"x-modes":["screenshot"]},{"name":"waitUntil","in":"query","required":false,"description":"The load event to wait for before capturing. Possible values are `load`, `domcontentloaded`, `networkidle0` (no network connections for half a second) and `networkidle2` (at most two). Defaults to `networkidle2`. Screenshots only; sending it with `pdf` is rejected.","schema":{"type":"string","enum":["load","domcontentloaded","networkidle0","networkidle2"],"default":"networkidle2"},"x-modes":["screenshot"]},{"name":"cache","in":"query","required":false,"description":"Whether an identical repeat of this request may be answered with the rendering we already made, and whether this rendering is kept for the next one. Defaults to `true`. A cached answer costs no credit, says so with `X-Cache: HIT`, and is counted against the account's cached-answer limit rather than against its renders; send `cache=0` to render the page again, which costs a credit and a render slot as any render does. Entries belong to your own account. See [caching](https://api.screenshotbuddy.io/documentation/caching) for the whole picture.","schema":{"type":"boolean","default":true},"x-modes":["screenshot","pdf"]},{"name":"cacheTtl","in":"query","required":false,"description":"How long the rendering is kept, in seconds, between `60` and `2592000` (thirty days). Defaults to `86400`. Sending it with `cache=0` is rejected, because there is no lifetime to set on a rendering that is not being kept.","schema":{"type":"integer","minimum":60,"maximum":2592000,"default":86400},"x-modes":["screenshot","pdf"]},{"name":"tokenId","in":"query","required":true,"description":"The id of the API token this URL is signed for. It is the number in front of the pipe in the token itself, and it is shown next to every token under Settings, API tokens. It is not a secret, and it is covered by the signature, so it cannot be swapped for another account's. See [signed URLs](https://api.screenshotbuddy.io/documentation/signed-urls).","schema":{"type":"integer"},"x-modes":["screenshot","pdf"]},{"name":"expires","in":"query","required":false,"description":"The Unix timestamp, in seconds, after which this URL stops working. Optional; a URL without one keeps working until the token is revoked or rotated. It is covered by the signature, so it cannot be pushed back without signing again, and a URL past it is refused with `signed_url_expired` rather than rendered.","schema":{"type":"integer"},"x-modes":["screenshot","pdf"]},{"name":"signature","in":"query","required":true,"description":"The HMAC-SHA256 of the canonical query, keyed with the signing secret of the token, in lowercase hex. Exactly 64 characters. Every other query parameter is covered by it, so anything appended to the URL, a tracking parameter included, invalidates it. The [signed URLs](https://api.screenshotbuddy.io/documentation/signed-urls) page spells out the canonicalisation step by step.","schema":{"type":"string"},"x-modes":["screenshot","pdf"]}],"responses":{"200":{"description":"The rendered file. The body is the image or the PDF itself rather than JSON, so write it to a file instead of parsing it. The Content-Type names the format that was rendered.","x-retryable":false,"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"X-Cache":{"description":"Whether this answer came out of the cache. `HIT` means nothing was rendered and no credit was spent; `MISS` means the page was rendered for this request and the result was kept for the next one. Absent when the request sent `cache=0`, which declines the cache rather than missing it.","schema":{"type":"string","enum":["HIT","MISS"]}},"ETag":{"description":"The quoted digest of the body. Send it back as `If-None-Match` on the same request and bytes that match it are answered `304` with no body: free when the cache could answer, and costing the render but not the download when the page had to be loaded to find out.","schema":{"type":"string"}},"X-Target-Status":{"description":"The HTTP status the captured page itself answered while it was being loaded, which is not the status of this response: a rendered `404` page is a successful `200` answer carrying `X-Target-Status: 404`. Check it before storing a capture, because a login wall and a 404 page both render perfectly. Present on screenshot answers only, and absent whenever it is not known: a PDF render is never told one, and neither is an answer served out of a cache entry stored before this header existed. Absent means not known, not `200`.","schema":{"type":"integer"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"The rendering is byte for byte the copy already held by the caller, named in the `If-None-Match` of the request, so no body is sent. What it costs depends on how that was established. An answer the cache could give is free: no credit, and counted against neither per-minute budget. One that had to render the page first, because the request sent `cache=0` or because the entry had lapsed, costs exactly what that render costs, a credit and a slot of the render budget, and what the caller saves is the download. `X-Cache: HIT` marks the free one; anything else was rendered, and carries the X-RateLimit headers to say so.","x-retryable":false,"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"X-Cache":{"description":"Whether this answer came out of the cache. `HIT` means nothing was rendered and no credit was spent; `MISS` means the page was rendered for this request and the result was kept for the next one. Absent when the request sent `cache=0`, which declines the cache rather than missing it.","schema":{"type":"string","enum":["HIT","MISS"]}},"ETag":{"description":"The quoted digest of the body. Send it back as `If-None-Match` on the same request and bytes that match it are answered `304` with no body: free when the cache could answer, and costing the render but not the download when the page had to be loaded to find out.","schema":{"type":"string"}},"X-Target-Status":{"description":"The HTTP status the captured page itself answered while it was being loaded, which is not the status of this response: a rendered `404` page is a successful `200` answer carrying `X-Target-Status: 404`. Check it before storing a capture, because a login wall and a 404 page both render perfectly. Present on screenshot answers only, and absent whenever it is not known: a PDF render is never told one, and neither is an answer served out of a cache entry stored before this header existed. Absent means not known, not `200`.","schema":{"type":"integer"}}}},"401":{"description":"The signature did not verify, or the URL has passed the `expires` it was signed with. The code says which: `signed_url_expired` is fixed by signing a fresh URL, `invalid_signature` by fixing how the URL is built. Every other way a signed request can fail to authenticate, an unknown token id and a revoked token included, answers `invalid_signature` as well, so that the endpoint cannot be used to find out which tokens exist.","x-retryable":false,"x-error-codes":["invalid_signature","signed_url_expired"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_signature","message":"This signed URL is not valid. Check that the signature covers every query parameter, and that the token it names still exists.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"402":{"description":"The account has no active plan, or it has used every credit in the current period. The code says which of the two it is.","x-retryable":false,"x-error-codes":["no_active_plan","credit_limit_reached"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"credit_limit_reached","message":"You have reached the credit limit of your plan.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"403":{"description":"The email address of the account has not been verified yet, or the token is not allowed to ask for the mode the request is in. The code says which of the two it is: a token carries a permission per mode, and one narrowed to screenshots cannot print a PDF.","x-retryable":false,"x-error-codes":["email_unverified","missing_ability"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"email_unverified","message":"You must verify your email address.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"422":{"description":"A query parameter did not validate, or the target URL could not be reached or loaded (\"The target URL could not be reached or loaded. Check that it is publicly available and try again.\"). The errors object names the parameter at fault. No credit is charged.","x-retryable":false,"x-error-codes":["validation_failed","invalid_url","blocked_host","target_unreachable"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_url","message":"The url query parameter is required.","errors":{"url":["The url query parameter is required."]},"request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"429":{"description":"The account went over one of its two per-minute limits, its renders or its cached answers, or more than 30 signatures failed to verify from this address in a minute. Wait for the number of seconds in the Retry-After header before sending the request again.","x-retryable":true,"x-error-codes":["rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"Too Many Attempts.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"500":{"description":"The render failed for a reason we did not recognise. The credit is refunded.","x-retryable":false,"x-error-codes":["render_failed","server_error"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"render_failed","message":"Server error while taking screenshot.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"502":{"description":"The rendering service failed to process the request. The credit is refunded and the request is worth sending again.","x-retryable":true,"x-error-codes":["upstream_error","upstream_quota_exceeded"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"upstream_error","message":"The rendering service failed to process this request. Please try again shortly.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"503":{"description":"The rendering service was momentarily busy and stayed busy across our own retries. The credit is refunded. Unlike the 502 this is not a failure of anything: wait the few seconds in the Retry-After header and the same request goes through.","x-retryable":true,"x-error-codes":["upstream_rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"upstream_rate_limited","message":"The rendering service is momentarily busy. Please try again in a few seconds.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"504":{"description":"The render did not finish within its time budget. The credit is refunded and the request is worth sending again.","x-retryable":true,"x-error-codes":["render_timeout"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Cache-Control":{"description":"On a rendered answer, `public` with a `max-age` of the rendering's own lifetime, shortened to what is left of `expires` when the URL carries one, so nothing in front of us serves an image from a URL that no longer authenticates. On every refusal, and on a request that sent `cache=0`, `no-store`: a cached refusal would keep being served after the reason for it was fixed.","schema":{"type":"string"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"render_timeout","message":"The render did not complete in time. Please try again.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}}}}},"/renders":{"post":{"operationId":"submitRenderBatch","summary":"Submit a set of renders to be made in the background.","description":"Takes up to 20 captures in one JSON body and answers straight away with the batch that will carry them, every item `queued`. Nothing is rendered on this request, so it holds no connection open and cannot time out on a slow page. Each item is validated exactly as the `GET /snap` request it will be made as, and refused in the same words: an option combination this API rejects synchronously is rejected here too.\n\nEvery item then runs the same billed lifecycle as a synchronous call. The token that submitted the batch is the token each render is authorised by, so a token narrowed to screenshots still cannot print a PDF asynchronously, and a token revoked or rotated while the batch is queued fails the items it has not reached. One credit is taken before a render and returned when it fails, a cache hit costs nothing, and each item is paced against the account's own per-minute render limit: an item that meets that limit waits and asks again rather than failing, for up to a quarter of an hour. A daily or monthly ceiling is not something waiting fixes, so an item that meets one is failed with `rate_limited`.\n\nResults are delivered through the response cache, which is why an item may not send `cache=0` and why its `cacheTtl` has a floor of 3600 seconds: a rendering that lapsed while the batch was still draining would be one the caller could never collect. Two items describing the same capture are refused as well, because they would race each other for one render and the loser would be a second credit for one page.\n\nThe submission itself renders nothing and reads nothing, so it spends neither of the account's two snap budgets. It runs under a throttle of its own of 10 requests per minute, and an account may have at most 100 items outstanding across all of its batches at once.","security":[{"bearerToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","description":"The captures to make, between one and 20 of them. Two items that describe the same capture are refused, because one page rendered twice is one credit spent twice.","minItems":1,"maxItems":20,"items":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"The URL of the page to render. It must start with `http://` or `https://`, be at most `2048` characters long, and point at a publicly reachable host. Private, loopback and internal addresses are refused.","x-modes":["screenshot","pdf"]},"fullPage":{"type":"boolean","default":false,"description":"Whether to capture the entire scrollable page instead of just the viewport. Defaults to `false`. Screenshots only; sending it with `pdf` is rejected, because a PDF always prints the whole document.","x-modes":["screenshot"]},"pdf":{"type":"boolean","default":false,"description":"Set this to `true` to render a PDF instead of an image. It changes which other parameters are accepted, so see [creating PDFs](https://api.screenshotbuddy.io/documentation/creating-pdfs) for the options that belong to that mode. Defaults to `false`.","x-modes":["screenshot","pdf"]},"landscape":{"type":"boolean","default":false,"description":"Whether to use landscape orientation. Defaults to `false` (portrait). PDFs only; sending it without `pdf` is rejected. A screenshot is shaped by `width` and `height` instead.","x-modes":["pdf"]},"format":{"type":"string","enum":["png","jpeg","webp"],"default":"png","description":"The image format to return. Possible values are `png`, `jpeg` and `webp`. Defaults to `png`. Screenshots only; sending it with `pdf` is rejected. Use `paperFormat` to set the paper size of a PDF.","x-modes":["screenshot"]},"quality":{"type":"integer","minimum":1,"maximum":100,"description":"The quality of the image, between `1` and `100`. It applies to lossy formats only, so `format` has to be `jpeg` or `webp`; sending it with `png` is rejected. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"]},"paperFormat":{"type":"string","enum":["letter","legal","tabloid","ledger","a0","a1","a2","a3","a4","a5","a6"],"default":"a4","description":"The paper format. Possible values are `letter`, `legal`, `tabloid`, `ledger`, and `a0` through `a6`. Defaults to `a4`. PDFs only; sending it without `pdf` is rejected. Use `format` to set the image format of a screenshot.","x-modes":["pdf"]},"width":{"type":"number","minimum":1,"maximum":10000,"description":"The viewport width of a screenshot, or the paper width of a PDF, between `1` and `10000`. Must be used together with `height`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","x-modes":["screenshot","pdf"],"x-requires-together":["height"]},"height":{"type":"number","minimum":1,"maximum":10000,"description":"The viewport height of a screenshot, or the paper height of a PDF, between `1` and `10000`. Must be used together with `width`. For a PDF it overrides `paperFormat` and is read in the unit set by `marginUnit`.","x-modes":["screenshot","pdf"],"x-requires-together":["width"]},"marginTop":{"type":"number","minimum":0,"maximum":1000,"description":"Top margin, between `0` and `1000`, in the unit set by `marginUnit`. All four margins (`marginTop`, `marginRight`, `marginBottom`, `marginLeft`) have to be set together. PDFs only; sending a margin without `pdf` is rejected.","x-modes":["pdf"],"x-requires-together":["marginRight","marginBottom","marginLeft"]},"marginRight":{"type":"number","minimum":0,"maximum":1000,"description":"Right margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","x-modes":["pdf"],"x-requires-together":["marginTop","marginBottom","marginLeft"]},"marginBottom":{"type":"number","minimum":0,"maximum":1000,"description":"Bottom margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginLeft"]},"marginLeft":{"type":"number","minimum":0,"maximum":1000,"description":"Left margin, between `0` and `1000`, in the unit set by `marginUnit`. Set it together with the other three margins. PDFs only; sending a margin without `pdf` is rejected.","x-modes":["pdf"],"x-requires-together":["marginTop","marginRight","marginBottom"]},"marginUnit":{"type":"string","enum":["mm","cm","in","px"],"default":"mm","description":"The unit for the margins and for a custom paper size. Possible values are `mm`, `cm`, `in` and `px`. Defaults to `mm`. PDFs only; sending it without `pdf` is rejected, because a screenshot's `width` and `height` are viewport pixels.","x-modes":["pdf"]},"scale":{"anyOf":[{"type":"integer","minimum":1,"maximum":3,"x-modes":["screenshot"]},{"type":"number","minimum":0.1,"maximum":2,"x-modes":["pdf"]}],"default":1,"description":"The scale of the rendering, between `1` and `3` for screenshots and between `0.1` and `2` for PDFs. Screenshots take whole numbers only, because the browser renders at whole device scale factors; PDFs take fractions. Defaults to `1`. A value outside the range of the mode you are in is rejected.","x-modes":["screenshot","pdf"]},"delay":{"type":"integer","minimum":0,"maximum":10000,"description":"How long to wait before capturing, in milliseconds, between `0` and `10000`. Useful for pages that animate on load. Screenshots only; sending it with `pdf` is rejected, because the PDF renderer has no way to wait a fixed amount of time before it prints.","x-modes":["screenshot"]},"selector":{"type":"string","description":"A CSS selector. Only the first element that matches it is captured, instead of the page. At most `512` characters, and it may not contain single quotes, backslashes or control characters, so write attribute selectors with double quotes: `a[href=\"/pricing\"]`. It chooses what to capture, so it cannot be combined with a clip region or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"]},"clipX":{"type":"integer","minimum":0,"maximum":10000,"description":"The distance from the left edge of the page to the region to capture, in pixels, between `0` and `10000`. All four clip parameters (`clipX`, `clipY`, `clipWidth`, `clipHeight`) have to be set together. A clip region chooses what to capture, so it cannot be combined with `selector` or with `fullPage`. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"],"x-requires-together":["clipY","clipWidth","clipHeight"]},"clipY":{"type":"integer","minimum":0,"maximum":10000,"description":"The distance from the top edge of the page to the region to capture, in pixels, between `0` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"],"x-requires-together":["clipX","clipWidth","clipHeight"]},"clipWidth":{"type":"integer","minimum":1,"maximum":10000,"description":"The width of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipHeight"]},"clipHeight":{"type":"integer","minimum":1,"maximum":10000,"description":"The height of the region to capture, in pixels, between `1` and `10000`. Set it together with the other three clip parameters. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"],"x-requires-together":["clipX","clipY","clipWidth"]},"omitBackground":{"type":"boolean","default":false,"description":"Whether to render the page background transparent. Defaults to `false`. The format has to be able to hold transparency, so `png` or `webp`; sending it with `jpeg` is rejected rather than answered with a black background. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"]},"waitForSelector":{"type":"string","description":"A CSS selector to wait for before capturing. The render continues once an element matching it exists. At most `512` characters, and under the same character restriction as `selector`: no single quotes, backslashes or control characters. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"]},"waitUntil":{"type":"string","enum":["load","domcontentloaded","networkidle0","networkidle2"],"default":"networkidle2","description":"The load event to wait for before capturing. Possible values are `load`, `domcontentloaded`, `networkidle0` (no network connections for half a second) and `networkidle2` (at most two). Defaults to `networkidle2`. Screenshots only; sending it with `pdf` is rejected.","x-modes":["screenshot"]},"cache":{"type":"boolean","default":true,"description":"Whether an identical repeat of this request may be answered with the rendering we already made, and whether this rendering is kept for the next one. Defaults to `true`. A cached answer costs no credit, says so with `X-Cache: HIT`, and is counted against the account's cached-answer limit rather than against its renders; send `cache=0` to render the page again, which costs a credit and a render slot as any render does. Entries belong to your own account. See [caching](https://api.screenshotbuddy.io/documentation/caching) for the whole picture. An async item may not turn it off: the result is delivered through the cache, so `cache=0` would leave nothing to collect. Send that capture to `GET /snap` instead.","x-modes":["screenshot","pdf"]},"cacheTtl":{"type":"integer","minimum":3600,"maximum":2592000,"default":86400,"description":"How long the rendering is kept, in seconds, between `60` and `2592000` (thirty days). Defaults to `86400`. Sending it with `cache=0` is rejected, because there is no lifetime to set on a rendering that is not being kept. An async item has a higher floor of `3600` seconds, because the rendering has to still be there when the batch it belongs to finishes draining.","x-modes":["screenshot","pdf"]}}}},"webhookUrl":{"type":"string","format":"uri","maxLength":2048,"description":"Where to POST the finished batch, once, when every item of it is terminal. Optional; polling works whether or not it is given. It has to be an `http://` or `https://` URL on a publicly reachable host, HTTPS in production, and it is vetted again at delivery, because a name that resolved publicly at submission can resolve somewhere internal by the time the batch finishes."}}},"example":{"items":[{"url":"https://example.com/pricing","width":1200,"height":630},{"url":"https://example.com/blog","fullPage":true,"cacheTtl":86400}],"webhookUrl":"https://example.com/hooks/renders"}}}},"responses":{"202":{"description":"The batch was accepted and queued. The body is the same document `GET /renders/{id}` serves, with every item `queued`.","headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderBatch"},"example":{"id":"01k1x8w0k3n6qv2r7y9c4h5t8m","status":"pending","created_at":"2026-08-05T10:15:00+00:00","finished_at":null,"prunes_at":"2026-08-12T10:15:00+00:00","webhook":{"url":"https://example.com/hooks/renders","delivered_at":null},"items":[{"id":"01k1x8w0k4a2be7d9f1g3h5j7k","url":"https://example.com/pricing","status":"queued","error":null,"etag":null,"target_status":null,"expires_at":null,"signed_url":null,"credit_cost":null},{"id":"01k1x8w0k5m4np6qr8s0t2u4v6","url":"https://example.com/blog","status":"queued","error":null,"etag":null,"target_status":null,"expires_at":null,"signed_url":null,"credit_cost":null},{"id":"01k1x8w0k6w8xy0za2b4c6d8e0","url":"https://example.com/changelog","status":"queued","error":null,"etag":null,"target_status":null,"expires_at":null,"signed_url":null,"credit_cost":null}]}}}},"401":{"description":"The request carried no bearer token, or a token that is unknown or revoked.","x-retryable":false,"x-error-codes":["unauthenticated"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"Unauthenticated.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"403":{"description":"The email address of the account has not been verified yet. The token's permissions are not checked here: they are checked per item at render time, so a batch mixing screenshots and PDFs is accepted and the items the token may not ask for are the ones that fail.","x-retryable":false,"x-error-codes":["email_unverified"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"email_unverified","message":"You must verify your email address.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"422":{"description":"The envelope or one of the items did not validate. The `errors` object names what is at fault under the index of the item it belongs to, as `items.0.width`, so a batch of twenty is corrected without guessing which one was wrong. Nothing is queued and nothing is charged.","x-retryable":false,"x-error-codes":["validation_failed","invalid_url","blocked_host"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"validation_failed","message":"The given data was invalid.","errors":{"items.0.cacheTtl":["The cacheTtl of an async item must be at least 3600 seconds, so the result is still there when the batch finishes."]},"request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"429":{"description":"More than 10 submissions in a minute. This throttle belongs to this endpoint alone: going over it neither spends nor depends on either of the per-minute budgets of /snap. The separate cap on how much may be waiting at once, 100 items across all of an account's batches, is a 422 rather than a 429, because waiting a minute is not what fixes it.","x-retryable":true,"x-error-codes":["rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"Too Many Attempts.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}}}}},"/renders/{id}":{"get":{"operationId":"showRenderBatch","summary":"Report where a batch has got to and hand back the results it has.","description":"Answers with the same document the submission answered with, brought up to date. `status` is `pending` until every item is terminal and `complete` afterwards, and each item carries its own `status`, its `error` when it has one, and a `signed_url` once it has a rendering to hand over. Poll as often as you like: this endpoint renders nothing and has no throttle of its own, and the results it points at are fetched over the signed URLs, which are counted against the cached-answer budget like any other hit.\n\nA batch belonging to another account answers exactly what an id that never existed answers, `404`, so the endpoint cannot be used to find out which batch ids are real. A batch that has been pruned answers `404` as well; `prunes_at` in every earlier answer is what said that moment was coming.\n\nThe links are minted with the token doing the polling rather than with the one that submitted the batch, so a link carries the permissions of whoever is asking now and stops working when their token is revoked or rotated. A token with no signing secret, which is what a browser session authenticates with, is answered with the document and `signed_url` left null; the result is still there to be fetched by repeating the identical `GET /snap` request, which is a cache hit and costs no credit.","security":[{"bearerToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The id of the batch, as the submission answered with it.","schema":{"type":"string"}}],"responses":{"200":{"description":"The batch as it stands. Answered whatever state it is in, so a caller polls this one endpoint from submission to completion.","headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderBatch"},"example":{"id":"01k1x8w0k3n6qv2r7y9c4h5t8m","status":"pending","created_at":"2026-08-05T10:15:00+00:00","finished_at":null,"prunes_at":"2026-08-12T10:15:00+00:00","webhook":{"url":"https://example.com/hooks/renders","delivered_at":null},"items":[{"id":"01k1x8w0k4a2be7d9f1g3h5j7k","url":"https://example.com/pricing","status":"done","error":null,"etag":"\"8f14e45fceea167a\"","target_status":200,"expires_at":"2026-08-06T10:16:04+00:00","signed_url":"https://api.screenshotbuddy.io/v1/snap/signed?url=https%3A%2F%2Fexample.com%2Fpricing&width=1200&height=630&tokenId=42&expires=1786097764&signature=6f1c...","credit_cost":1},{"id":"01k1x8w0k5m4np6qr8s0t2u4v6","url":"https://example.com/blog","status":"failed","error":{"code":"target_unreachable","message":"The target URL could not be reached or loaded. Check that it is publicly available and try again."},"etag":null,"target_status":null,"expires_at":null,"signed_url":null,"credit_cost":0},{"id":"01k1x8w0k6w8xy0za2b4c6d8e0","url":"https://example.com/changelog","status":"queued","error":null,"etag":null,"target_status":null,"expires_at":null,"signed_url":null,"credit_cost":null}]}}}},"401":{"description":"The request carried no bearer token, or a token that is unknown or revoked.","x-retryable":false,"x-error-codes":["unauthenticated"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"Unauthenticated.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"404":{"description":"No batch with that id belongs to this account. A batch of another account, an id that never existed and a batch that has been pruned are all answered the same way, so the endpoint says nothing about which ids are real.","x-retryable":false,"x-error-codes":["not_found"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"not_found","message":"No render batch with that id exists for this account. A finished batch is kept for 7 days and then deleted.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}}}}},"/usage":{"get":{"operationId":"usage","summary":"Report the credits, plan, period and rate limits of the account.","description":"Says what the account currently has, so a caller can find out where it stands before spending a credit. It reports state rather than refusing on it: an account with no plan, or with every credit spent, is described with a 200 and the numbers that say so, where `/snap` answers 402. It runs under a throttle of its own of 60 requests per minute, separate from both of the limits it reports, so polling this endpoint never eats into what `/snap` will accept.","security":[{"bearerToken":[]}],"responses":{"200":{"description":"The current state of the account. Answered even when the account is on no plan or has spent every credit.","headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"X-Credits-Limit":{"description":"Credits the current period was granted. Sent on every answer to a request that authenticated.","schema":{"type":"integer"}},"X-Credits-Remaining":{"description":"Credits left to spend, counted after this request was accounted for, so a render that succeeded is already subtracted and a render that failed is already refunded. Floored at zero.","schema":{"type":"integer"}},"X-Credits-Reset":{"description":"When the credits are granted again, as a Unix timestamp in seconds.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["credits","plan","period","rate_limit"],"properties":{"credits":{"type":"object","description":"The credits of the current period.","required":["remaining","used","total"],"properties":{"remaining":{"type":"integer","description":"Credits left to spend. Floored at zero, so an overspent period reads as empty rather than as a negative allowance."},"used":{"type":"integer","description":"Credits spent in the current period."},"total":{"type":"integer","description":"Credits the period was granted."}}},"plan":{"type":["object","null"],"description":"The plan the account is on, or null when it is on none.","properties":{"name":{"type":"string","description":"The plan as it is written for a person."},"slug":{"type":"string","description":"The stable identifier of the plan."}}},"period":{"type":["object","null"],"description":"The current billing period, or null when the account has none yet.","properties":{"started_at":{"type":"string","format":"date-time","description":"When the current period began."},"resets_at":{"type":"string","format":"date-time","description":"When the credits are granted again."}}},"rate_limit":{"type":"object","description":"The per-minute limits of the account.","required":["requests_per_minute","cached_requests_per_minute"],"properties":{"requests_per_minute":{"type":"integer","description":"How many fresh renders the account may ask /snap for per minute. This is the account's own limit, whatever plan or override it came from. A request answered from the cache is not one of these."},"cached_requests_per_minute":{"type":"integer","description":"How many requests /snap may answer out of the cache per minute. The same for every account, and counted separately from the renders."}}}}},"example":{"credits":{"remaining":8432,"used":1568,"total":10000},"plan":{"name":"Business","slug":"business"},"period":{"started_at":"2026-08-01T00:00:00+00:00","resets_at":"2026-09-01T00:00:00+00:00"},"rate_limit":{"requests_per_minute":40,"cached_requests_per_minute":300}}}}},"401":{"description":"The request carried no bearer token, or a token that is unknown or revoked.","x-retryable":false,"x-error-codes":["unauthenticated"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"Unauthenticated.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"403":{"description":"The email address of the account has not been verified yet.","x-retryable":false,"x-error-codes":["email_unverified"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"email_unverified","message":"You must verify your email address.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}},"429":{"description":"More than 60 requests to this endpoint in a minute. This throttle belongs to the usage endpoint alone: going over it does not spend, or depend on, either of the per-minute budgets of /snap.","x-retryable":true,"x-error-codes":["rate_limited"],"headers":{"X-Request-Id":{"description":"Identifies this one answer, and is in the server log for the request that produced it. An error envelope carries the same value in its `request_id`, so a rendered file and a refusal are quoted at support the same way.","schema":{"type":"string","format":"uuid"}},"Retry-After":{"description":"The number of seconds to wait before sending the request again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"rate_limited","message":"Too Many Attempts.","request_id":"1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"}}}}}}}}}