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.urlcURL 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.bodyPHP 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))
}import mql from '@microlink/mql'
const { data } = await mql('https://example.com/post', {
screenshot: true,
embed: "screenshot.url"
})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?.urlThe 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:
-
Use
embed=image.url— this re-serves the asset throughapi.microlink.io, with permissive CORS and no referrer check.<img src="https://api.microlink.io?url=https://stripe.com&embed=image.url" /> -
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' }} />See lazy reference.
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:
| Symptom | Likely cause |
|---|---|
| The image updated but Slack still shows the old one | Slack's image cache is sticky — change the URL by appending &v=N. |
| Twitter shows the title but no image | Twitter Cards requires meta tags on your page, not on the API URL. Reference the embed URL in twitter:image. |
| Email client renders a placeholder | Many 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.comafter fetching by replacing the host insideiframe.htmlif 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.