A JSON lookup, like the rest of the catalogue
The team image service behaves like every other service under /api/v1/football. It takes one required parameter, teamId, a positive numeric team identifier sent as a string, and on success returns JSON in the standard envelope. The data object carries two fields: id, the team identifier you asked for, and image, the image payload as the source supplies it. The meta object records the feed, the UTC fetch time, the cache time and the qualification state, exactly as on other services.
It is important to be precise about what the image field is. It is the supplied payload, not a public link you can drop into a page. It is not an immediately usable URL, and you should not assume it is one. Your server has to read it, turn it into image bytes according to how the payload is encoded in your saved sample, check the result is really an image, and then serve that image from your own address.
- Success: JSON with data.id, data.image and meta.
- The image field is a payload to decode, not a public link.
- Failure: structured JSON error with code, error and requestId.
Decoding and validating the payload
Before writing any image handling, save one real successful response and look at the image field. Write your decoder from that sample, not from assumptions. Once decoded, never trust a file extension or a guess about the format. Inspect the first bytes of the result to identify the actual raster type, and choose the stored extension and the content type you will serve from that inspection. If the bytes do not match a raster format you accept, treat the record as unusable, log it and fall back.
Also apply sensible limits. Reject payloads that decode to something implausibly large, refuse anything that is not a raster image, and never write the JSON response itself to disk with an image extension. A file called crest.png that actually contains JSON is a common and confusing bug, and it is entirely avoidable if the only thing your code ever saves is validated decoded bytes.
Fetching the JSON record
The first example simply retrieves the JSON so you can inspect a real response. Run it from a terminal with the key in an environment variable. Read the data and meta objects, then save the body as a JSON sample for writing your decoder and your tests.
curl -s "https://footballapi.net/api/v1/football/team-images?teamId=YOUR_TEAM_ID" \
-H "X-API-Key: $FOOTBALLAPI_KEY" \
-H "Accept: application/json" \
-o team-image-sample.json
# Inspect the envelope: data.id, data.image, meta
cat team-image-sample.jsonServing crests from your own server
The browser must never call the API directly, because that would expose your key. Instead, your server performs the lookup, decodes and validates the payload, caches the resulting image and serves it from your own URL. Readers see only your address; the key stays in your server environment. The sketch below shows the shape of that route. The decoding step is a helper you write from your saved sample, and the type check inspects the decoded bytes rather than trusting any name.
Crests change rarely, so cache validated images for days or weeks, and keep the identifier and fetch time with each cached copy so you know where it came from.
app.get("/crests/:teamId", async (req, res) => {
const teamId = req.params.teamId;
if (!/^[1-9]\d{0,11}$/.test(teamId)) return res.sendStatus(400);
let img = await cache.get(`crest:${teamId}`); // { bytes, type }
if (!img) {
const r = await fetch(`https://footballapi.net/api/v1/football/team-images?teamId=${teamId}`, {
headers: { "X-API-Key": process.env.FOOTBALLAPI_KEY, Accept: "application/json" },
});
const body = await r.json(); // success and error bodies are both JSON
if (!r.ok) {
log.warn(body.code, body.requestId);
return res.sendStatus(body.code === "image_unavailable" ? 404 : 502);
}
const bytes = decodeImagePayload(body.data.image); // written from your saved sample
const type = detectRasterType(bytes); // inspect magic bytes: png, jpeg, webp...
if (!type) return res.sendStatus(502); // not a raster image: never serve it
img = { bytes, type };
await cache.set(`crest:${teamId}`, img, { ttlDays: 14 });
}
res.type(img.type).set("Cache-Control", "public, max-age=86400").send(img.bytes);
});Errors, absence and entitlement
Failures return the gateway's structured JSON error. A 404 with the image_unavailable code means no crest image is available for that team, typically for lower-division clubs, youth sides and newly formed teams. A 403 with subscription_required means the account is not entitled. A 400 invalid_parameter usually means a malformed teamId. A 429 asks you to wait for the Retry-After period, and 502 or 504 responses are temporary upstream problems.
Treat these differently. Cache an image_unavailable result for a shorter period than a real crest, so a missing crest does not trigger a request on every page view, and re-check occasionally. Never cache an entitlement or upstream error as an absence; those need fixing or retrying, not remembering. Log the requestId from every error body so support can trace individual calls.
Fallbacks that look intentional
Plan for missing crests from the start. A neutral shield shape with the team's initials, or simply the team name without an image, reads as deliberate. Avoid generic stock badges that could be mistaken for a real crest, and never reuse one team's crest for another with a similar name. Key every cached image by team identifier and nothing else.
Build-time crests for static pages
If your league pages are pre-rendered, perform the lookups during the build instead of through a runtime route. The build script requests each team's JSON record, decodes and validates the payload, writes the image to your asset folder with an extension chosen from the detected type and named by team identifier, and records image_unavailable responses so templates can use the fallback. The key exists only in the build environment.
Static crests are fast and cache perfectly on a CDN. The trade-off is that a crest change reaches your site only on the next build, which for most clubs is fine.
Alt text, sizing and backgrounds
In a score tile where the team name is also shown, give the crest empty alt text so screen readers do not announce the team twice. Where the crest stands alone, use the supplied team name followed by the word crest. Set explicit width and height to prevent layout shift, and resize once on your server for the sizes your design uses rather than asking browsers to scale large images down.
Some crests nearly vanish on dark backgrounds. Rather than editing a crest, which you should not do, place it on a light neutral container. Preserve transparency when you resize or convert, because many crests rely on it.
Rights and context
Club crests are often trademarks. Access to the image service supports displaying crests in the context of football data, such as identifying teams in fixtures, tables and match pages. It is not a licence for merchandise, advertising or uses that suggest a club endorses your product. Do not alter, recolour or combine crests into new marks, and read our data licensing page for anything beyond editorial and data display.
Plan scope
Team images are a global service that is not included in Starter. The service requires Growth at EUR 99 a month or Pro at EUR 249 and activation for your account. A Starter key receives a 403 response with the subscription_required code.
With long caching, usage is small: one lookup per team when first shown and occasional refreshes. Each successful lookup counts towards the daily inclusion of 25,000 on Growth or 250,000 on Pro; above it, requests are EUR 0.002 each on the monthly EUR invoice. Failed and denied requests, including image_unavailable responses, are not billed. There is no free trial.
Questions developers ask
- What does a successful team-images response contain?
- JSON in the standard envelope: data.id with the team identifier, data.image with the supplied image payload, and meta.
- Is data.image a URL I can put in an image tag?
- No. It is a payload to decode and validate on your server, which then serves the image from your own URL.
- How do I know which file extension to use?
- Inspect the decoded bytes to detect the actual raster type, then choose the extension and content type from that.
- What if a team has no crest?
- The service returns a 404 JSON error with the image_unavailable code. Show an intentional fallback and cache the absence briefly.
- Is the crest service on Starter?
- No. It requires Growth or Pro and separate service activation.
Guides and documentation
Check the plan before you write the integration.
Starter EUR 29, Growth EUR 99, Pro EUR 249 per month. No free trial. No VAT currently charged.