Skip to content

Embed: Troubleshooting

When an embed renders wrong, the cause is usually one of five things: the field you needed wasn't returned, the iframe HTML wasn't injected with its scripts, the source page hot-link-blocks og:image, the SDK fetched too late, or the request needed auth or proxy.
For timeouts, blocked sites, auth/plan errors, and debug headers that apply to all workflows, see common troubleshooting.

The iframe field is missing

Not every URL has an oEmbed endpoint. When discovery fails, the response has no iframe field — the rest of the metadata is still returned.
Plan for both shapes in your renderer:
const { data } = await mql(url, { iframe: true })

if (data.iframe) {
  container.innerHTML = data.iframe.html
  data.iframe.scripts.forEach(injectScript)
} else {
  container.innerHTML = renderCustomCard(data) // fall back
}
The full provider list lives in the iframe parameter reference.

The iframe HTML renders blank or unstyled

Twitter, Instagram, Reddit, and similar widgets need their script tag to actually paint. If you only inject iframe.html, you'll see a styled blockquote instead of the real widget.
Inject iframe.scripts too:
data.iframe.scripts.forEach(({ src, async, charset }) => {
  if (document.querySelector(`script[src="${src}"]`)) return // dedupe
  const script = document.createElement('script')
  script.src = src
  if (async) script.async = true
  if (charset) script.charset = charset
  document.head.appendChild(script)
})
Dedupe matters because the same script (platform.twitter.com/widgets.js) gets returned for every Tweet embed on the page.

og:image is missing or low-resolution

Many sites either skip og:image entirely or ship a small social-media-only thumbnail. Replace it with a real capture:

The following examples show how to use the Microlink API with CLI, cURL, JavaScript, Python, Ruby, PHP & Golang, targeting 'https://example.com/post' URL with 'screenshot' & 'embed' API parameters:

CLI Microlink API example

microlink https://example.com/post&screenshot&embed=screenshot.url

cURL Microlink API example

curl -G "https://api.microlink.io" \
  -d "url=https://example.com/post" \
  -d "screenshot=true" \
  -d "embed=screenshot.url"

JavaScript Microlink API example

import mql from '@microlink/mql'

const { data } = await mql('https://example.com/post', {
  screenshot: true,
  embed: "screenshot.url"
})

Python Microlink API example

import requests

url = "https://api.microlink.io/"

querystring = {
    "url": "https://example.com/post",
    "screenshot": "true",
    "embed": "screenshot.url"
}

response = requests.get(url, params=querystring)

print(response.json())

Ruby Microlink API example

require 'uri'
require 'net/http'

base_url = "https://api.microlink.io/"

params = {
  url: "https://example.com/post",
  screenshot: "true",
  embed: "screenshot.url"
}

uri = URI(base_url)
uri.query = URI.encode_www_form(params)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Get.new(uri)
response = http.request(request)

puts response.body

PHP Microlink API example

<?php

$baseUrl = "https://api.microlink.io/";

$params = [
    "url" => "https://example.com/post",
    "screenshot" => "true",
    "embed" => "screenshot.url"
];

$query = http_build_query($params);
$url = $baseUrl . '?' . $query;

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET"
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
    echo "cURL Error #: " . $err;
} else {
    echo $response;
}

Golang Microlink API example

package main

import (
    "fmt"
    "net/http"
    "net/url"
    "io"
)

func main() {
    baseURL := "https://api.microlink.io"

    u, err := url.Parse(baseURL)
    if err != nil {
        panic(err)
    }
    q := u.Query()
    q.Set("url", "https://example.com/post")
    q.Set("screenshot", "true")
    q.Set("embed", "screenshot.url")
    u.RawQuery = q.Encode()

    req, err := http.NewRequest("GET", u.String(), nil)
    if err != nil {
        panic(err)
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(body))
}
The API URL itself becomes the image. Use this in <img src> or <meta property="og:image">.
Or fall back inside your renderer:
const heroImage = data.image?.url ?? data.screenshot?.url

The image works in the browser but not in <img>

Some sites block hot-linking based on Referer. The screenshot stays loadable from the browser's address bar but breaks inside another page's <img>.
Two options:
  1. Use embed=image.url — this re-serves the asset through api.microlink.io, with permissive CORS and no referrer check.
    <img src="https://api.microlink.io?url=https://stripe.com&embed=image.url" />
  2. Use a screenshot capture instead — Microlink renders the page itself, so the resulting image is hosted on Microlink's CDN and never re-requests the source.
    <img src="https://api.microlink.io?url=https://stripe.com&screenshot&meta=false&embed=screenshot.url" />

The SDK preview never loads

The SDK is lazy by default — it only fetches when the card enters the viewport. If you embed it inside a hidden tab, accordion, or display: none container, it never triggers.
Two fixes:
// 1. Disable lazy when the container is conditionally hidden
<Microlink url='...' lazy={false} />

// 2. Or expand the rootMargin so it fires earlier
<Microlink url='...' lazy={{ rootMargin: '600px' }} />

Inconsistent results across runs

If the same URL returns different metadata on different days, the cause is usually one of:
  • The source changed. Default cache TTL is 24h. Pin to a fixed window with ttl='7d'.
  • The page is JS-rendered. Some metadata only appears after hydration. The default rendering mode handles most cases, but for SPA-heavy targets you may need waitForSelector or waitUntil.
  • Different IP regions see different content. Localized pricing, A/B tests, or geofences. Pin region with proxy.

Embed URL works in some contexts but not others

Slack, Discord, X, LinkedIn, and email clients each cache embed URLs differently. Common gotchas:
SymptomLikely cause
The image updated but Slack still shows the old oneSlack's image cache is sticky — change the URL by appending &v=N.
Twitter shows the title but no imageTwitter Cards requires meta tags on your page, not on the API URL. Reference the embed URL in twitter:image.
Email client renders a placeholderMany email clients block remote images by default. Use a screenshot capture and a stable URL.

Provider-specific quirks

A short list:
  • YouTube — switch to youtube-nocookie.com after fetching by replacing the host inside iframe.html if you need cookieless embeds.
  • Spotify — the Spotify embed expects fixed dimensions; pass iframe={{ maxWidth: 600 }} when discovery returns a too-narrow value.
  • X / Twitter — Twitter sometimes returns a blockquote-only embed when the tweet is geo-restricted. Test with multiple URLs from different accounts.
  • Instagram — the Instagram widget script is large; defer it until the first Instagram embed enters the viewport.

Still stuck

Check the full error codes reference, the iframe parameter reference, the embed parameter reference, or see common troubleshooting for timeouts, auth, and plan errors.

Back to guides

See the guides overview for more Microlink guides.