# Polymorphism > Software engineering consultancy (Sheffield, UK). Also publishes a free Open Graph / social > card preview API, the candor family of code-analysis engines, and a small number of > technical papers. ## Machine-readable - [OpenAPI 3.1 for the social card API](https://www.poly.io/social-card-preview/api/openapi.json): every parameter, all 21 error codes with their retryability, and the 87 stable check ids the API can return. ## Social card preview API Base: `https://api.poly.io` - `GET /og?url={URL}` — reads a page's Open Graph and Twitter card tags, fetches and measures the image, and returns a checklist of what is wrong. Fetches the page TWICE, once as a browser and once as a crawler that says what it is, and reports where they differ — the failure an author cannot see, because they always look with a browser. - `GET /og?url={URL}&url={URL}` — up to five pages in one request, each counted as one request. The answer is `{ok, batch: true, count, results}`, one result per url in the order asked, each with its own `ok`. More than five is refused whole with `too_many_urls`. - `GET /og?site={ORIGIN}` — reads the sitemap, clusters URLs by shape, and returns a plan of one representative page per template rather than checking every page. - `GET /og/quota` — today's allowance for whoever is asking. Does not count against it. - `GET /og?url={URL}&fresh=1` — the same check with the cache bypassed. For the deploy → check → fix → redeploy → re-check loop, where the second check otherwise lands inside the five-minute window and reports the state before your fix. Costs a quota unit like any other request. Free, no key, CORS-enabled. **The API itself always answers HTTP 200** — check the `ok` field, not the status code. Responses cache for five minutes per URL unless you ask for `fresh=1`. Two statuses come from in front of the API rather than from it, and neither carries an `ok` field: **429** `{"message":"Too Many Requests"}` if you exceed the shared burst limit of 10 requests a second (bursting to 20), and **503** `{"message":"Service Unavailable"}` if the compute layer is saturated. Both are worth retrying after a moment. If you are writing a client, treat a non-200 as "try again shortly" and a 200 with `ok:false` as an answer about the page. ### A whole response, so you need not fetch anything else ```json { "ok": true, "finalUrl": "https://example.com/post", "host": "example.com", "title": "A post", "description": "What it is about.", "imageUrl": "https://example.com/card.png", "imageOk": true, "imageWidth": 1200, "imageHeight": 630, "checks": [ { "id": "image.twitter-only", "level": "error", "message": "Only twitter:image is set. Add og:image pointing at the same file." } ], "source": "https://www.poly.io/social-card-preview/api/", "spec": "https://www.poly.io/llms.txt", "specVersion": "1.1.0", "specNext": null, "quota": "https://api.poly.io/og/quota" } ``` A failure carries `ok: false`, a `code`, and `retryable`. Every response carries `spec`, which points back here — so one call is enough to learn the contract, and the HTML reference is for people rather than programs. ### Knowing when it changes - `GET https://www.poly.io/social-card-preview/api/changes.json` — dated contract changes, newest first, plus the CURRENT check inventory as `{id, level}` so you can diff it against the ids your gate names. - Every response carries `specVersion`, and `specNext` when a breaking change is scheduled. Surface `specNext` as a warning. **Do not pin `specVersion` in CI** — every ordinary release would fail your build on our schedule, which is not what either of us wants. New checks land at `warn` or `tip` and never at `error`, because a gate of "no checks at level error" is the common one and a new error-level check would break it silently. Raising an existing check to `error` is a BREAKING change: announced in `changes.json` before it ships. ### Reading a failure Every failure carries `code` and `retryable`. **`retryable: false` means the request will never succeed as written** — a blocked address, a hostname that does not resolve, a malformed URL — so a retry loop should stop rather than spend somebody else's bandwidth on it. - `no_url` — No url (or site) parameter was given. - `bad_url` — The url could not be parsed. - `bad_scheme` — The url is not http or https. - `bad_ua` — The ua parameter is not one of the supported crawler names. - `bad_key` — The key is not shaped like an API key. - `unknown_key` — The key is well-formed but not recognised. - `revoked_key` — The key was recognised and has been revoked. - `bad_site` — The site parameter could not be parsed as an origin. - `no_sitemap` — No sitemap was found — neither a Sitemap: line in robots.txt nor /sitemap.xml answered. - `empty_sitemap` — A sitemap was found but held no usable URLs. - `other_origin` — The sitemap lists pages on a different host; the error names which one to scan instead. - `blocked_address` — The URL resolves to an address this API refuses on principle (loopback, link-local, private ranges). Permanent. - `host_not_found` — The hostname does not resolve. Usually a typo — distinct from an address we refuse. - `gated_host` — Not fetched, for the reason given in error: the host blocks unauthenticated fetches, so no crawler can read its card (reddit.com); the page is on TikTok, Instagram, Facebook, X or LinkedIn and the request carries no API key (from 27 September 2026); or ua= names the crawler of the site being fetched. Permanent for that request. - `not_html` — The URL returned something that is not HTML — an image or a PDF, say. - `target_http_error` — The page returned an HTTP error. Retryable only on 5xx; the status is in the response. - `target_unreachable` (retryable) — The page could not be reached — a timeout or a refused connection. - `target_too_large` — The page exceeded the fetch ceiling. - `target_failed` — The fetch failed for a reason with no more specific code. - `quota_exceeded` (retryable) — The daily allowance is spent. Resets on the UTC day; see the quota endpoint. - `too_many_urls` — The request repeats url more times than one request may. Nothing was checked; split the pages across several requests. ### Reading the checks Each entry in `checks` is `{ level, id, message }`. **Key off `id`, never off `message`.** The id names the condition and is stable; the message is prose and is rewritten whenever a clearer or more accurate wording is found — four were reworded in a single afternoon in September 2026 because they claimed more than the tool had measured. The full id list is in the OpenAPI document. ### What it deliberately does not model Per-platform crop ratios and truncation limits. Those are constants about other companies' products, they change without notice, and a checker that renders them as fact is wrong quietly. Where a platform documents a limit the check cites it; where it does not, the check says so. ## Documentation - [API reference](https://www.poly.io/social-card-preview/api/) - [The tool itself](https://www.poly.io/social-card-preview/) — same checks, in a browser - [goosey](https://goosey.poly.io/) — the same checks, remembered in the browser, with what changed since last time and a CI gate - [Changelog](https://www.poly.io/changelog/) - [Status](https://www.poly.io/status/) ## Writing Essays, case studies and working papers, newest first. The same list as https://www.poly.io/writing/, built from it, so nothing here can be missing from there or the other way round. - [The Heat Death of the Codebase?](https://www.poly.io/writing/heat-death-of-the-codebase/) (2026-09-29) — How many AI coding agents to run, and why review sets the limit. A working paper with a rule of thumb, a model built on published data, and a calculator. - [The funding platform, taken international](https://www.poly.io/work/eureka-funding-platform/) (2026-09-17) — The Innovation Funding Service platform, taken international for the EUREKA network across 40+ countries and funding models. - [Inside Innovate UK's Innovation Funding Service](https://www.poly.io/work/innovation-funding-service/) (2026-09-17) — Among the most active engineers on the Innovation Funding Service, the open-source GOV.UK platform behind Innovate UK’s grants. - [Spring Boot 2.7 end of life: what the upgrade to 3 costs](https://www.poly.io/writing/spring-boot-end-of-life-migration/) (2026-08-27) — Spring Boot 2.7 is out of open-source support, and Boot 3 brings the javax-to-jakarta rename. Two Java estates show what decides the cost of the upgrade. - [The platform behind quality flexible work](https://www.poly.io/work/flexible-work-platform/) (2026-06-10) — How we build and run GoodFlexi: quality flexible work through public agencies and private employers. One platform, two regions. - [Modernising legacy systems without the big-bang rewrite](https://www.poly.io/writing/modernising-legacy-systems/) (2026-06-02) — How we modernise rigid, risky legacy systems one slice at a time, and why that de-risks the work more than a from-scratch rewrite. - [What is polymorphism?](https://www.poly.io/about/polymorphism/) (2026-06-01) — Polymorphism in programming, explained with interactive Java examples: overriding vs overloading, generics (parametric polymorphism) and ad-hoc polymorphism. - [The Seven Hills of Sheffield](https://beaky.poly.io/geoff/) — Sheffield’s seven hills, by prominence: High Neb, Back Tor, Howden Edge, Edge Mount, Parkwood Springs, Lodge Moor and Backmoor. The measure that settles them. - [Why Seven Hills? Rome, Sheffield, and four thousand books](https://beaky.poly.io/seven/) — Sheffield is said to be built on seven hills. So are Rome, Durham and Bristol. Four thousand books say it was never a count. Feed: https://www.poly.io/writing/feed.xml ## Other work - [candor](https://candor.poly.io/) — capability maps and CI gates for code written by AI agents - [Products](https://www.poly.io/products/) — candor, ebman, pgman, goosey and carnot