Skip to content
Football images

Football team crests API for club badges

Crests make football interfaces recognisable at a glance. The team image service is a JSON lookup: for a team identifier it returns the identifier and the supplied image payload inside the standard envelope. This page covers reading that response, turning the payload into a validated image on your own server, keeping the key private, and the accessibility and rights questions that come with club badges.

Primary endpoint

GET/api/v1/football/team-images

  • teamId required; JSON image payload on success

Access: Growth and Pro. Header: X-API-Key.

Illustration of three blank shield outlines on a shelf beside a padlock, symbolising crest lookups handled with a private key on the server.
The lookup returns JSON with an image payload; your server decodes, validates and serves it. The shields are blank placeholders.
01

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.
02

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.

Crest lookup to served imageFlow: your server requests team-images with the API key and receives JSON with data.id and data.image, decodes and validates the payload, detects the real image type, caches it, then serves it from your own URL.01team-imagesJSON, server-side02data.imagedecode payload03Validate typeinspect bytes04Your URLcached image
Crest lookup to served image. The REST lookup is JSON; turning its payload into an image is your server's job, and the key never leaves it.
03

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: retrieve the crest lookup as JSONbash
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.json
04

Serving 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.

Node.js (Express): look up, decode, validate and servejavascript
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);
});
05

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.

06

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.

07

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.

08

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.

09

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.

10

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.

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.