/api/analyze-adRun an AI vision analysis comparing an ad creative with its landing page. Each successful call uses one evaluation from your monthly pool; failed calls are refunded.
Scope: analyze
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| adData.imageUrl | string | No* | URL of the ad image or creative |
| adData.adUrl | string | No* | Direct URL to the ad (screenshotted automatically) |
| adData.platform | string | Recommended | meta, tiktok, linkedin, google, or reddit (facebook and instagram map to meta). Picks the scoring weights; if omitted, AdAlign detects the platform from the URLs or falls back to default weights. |
| adData.mediaType | string | No | Hint for adData.adUrl: image, video, or unknown. Video is otherwise detected from the URL. |
| adData.adTextFields | object | No | The ad copy as text, if you have it: headline, bodyText, description, ctaType (e.g. SHOP_NOW), pageName. Improves message match over reading the copy from the image. |
| landingPageData.url | string | Yes | Landing page URL to analyze |
| viewport | string | No | mobile or desktop. Defaults to mobile for Meta and TikTok, desktop otherwise. |
| audienceData | object | No | Targeting context: ageRange, gender, location, interests (strings). |
* Provide adData.imageUrl or adData.adUrl. With neither, the request fails with 400 NO_AD_ASSET.
Example Request
curl -X POST https://app.adalign.io/api/analyze-ad \
-H "Authorization: Bearer adal_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"adData": {
"imageUrl": "https://example.com/ad.png",
"platform": "meta"
},
"landingPageData": {
"url": "https://example.com/landing"
}
}'Response
Scores run from 1 to 10 in 0.5 steps. The overall score is the weighted sum of the four dimensions using the weights in appliedProfile.weights.
{
"overallScore": 7,
"componentScores": {
"visualMatch": 6.5,
"messageMatch": 8,
"aboveTheFoldContinuity": 7,
"toneAlignment": 7,
"contextualMatch": 7.5,
"informationScent": 7
},
"rawScores": { "...": "dimension scores before penalties and caps" },
"detailedScores": {
"visualMatch": { "score": 6.5, "band": "MODERATE", "reasoning": "..." },
"messageMatch": { "score": 8, "band": "GOOD", "reasoning": "..." },
"aboveTheFoldContinuity": { "score": 7, "band": "GOOD", "reasoning": "..." },
"toneAlignment": { "score": 7, "band": "GOOD", "reasoning": "..." }
},
"suggestions": {
"visual": ["Match hero image style with ad creative..."],
"contextual": ["Echo the specific offer from the ad headline..."],
"tone": ["Align CTA urgency with ad messaging..."]
},
"elementComparisons": [
{
"element": "Primary Headline",
"category": "message",
"adValue": "Get 50% Off Today",
"landingPageValue": "Welcome to Our Store",
"status": "mismatch",
"severity": "HIGH",
"recommendation": "Match the headline to the ad offer"
}
],
"conversionRisks": [
{
"type": "HEADLINE_DISCONNECT",
"message": "Ad headline doesn't appear on landing page - high bounce risk",
"impact": "HIGH",
"element": "Primary Headline"
}
],
"fixes": [
{
"id": "3f6c…",
"element": "Primary Headline",
"fixType": "headline",
"target": "landing_page",
"direction": "change_lp",
"priority": "HIGH",
"currentValue": "Welcome to Our Store",
"suggestedValue": "Get 50% Off Today",
"cssSelector": "h1",
"reasoning": "The ad's offer is missing above the fold",
"expectedImpact": "Visitors see the offer they clicked for",
"implementationSteps": ["..."],
"confidenceScore": 0.85,
"tradeoff": "..."
}
],
"appliedProfile": {
"id": "meta_feed",
"platform": "meta",
"surfaceLabel": "Feed",
"displayLabel": "Meta Feed",
"weights": { "visualMatch": 0.4, "messageMatch": 0.225, "aboveTheFoldContinuity": 0.225, "toneAlignment": 0.15 },
"detectionReason": "...",
"detectionConfidence": "high"
},
"detectionMode": "single",
"alternateScores": null,
"adScreenshotUrl": "https://...",
"landingPageScreenshotUrl": "https://...",
"mediaType": "image",
"viewport": "mobile",
"analysisModel": "...",
"pageSpeedData": { "performanceScore": 82, "lcp": 2100, "...": "..." },
"evaluationId": "uuid-here"
}elementComparisons[].status: match, partial_match, mismatch, missing, implicit (e.g. a product page's implied CTA) or obscured (hidden behind a cookie banner, not scored). category: visual, message, scent or tone.
fixes[].target: landing_page or ad. direction: change_lp, change_ad, change_routing (send the ad to a better page) or change_both.
alternateScores: when an ad runs on several placements (e.g. Advantage+), the headline score is the weakest placement and the others are listed here.
contextualMatch (average of message match and above-the-fold continuity) and informationScent (old name for above-the-fold continuity) are kept for older clients. Use the four named dimensions.
Video ads add videoFrames and videoMetadata.
When the landing page can't be captured
Some sites block automated browsers. If every capture attempt fails, the API still returns 200 with no scores, and the evaluation is not counted against your limit:
{
"success": true,
"evaluationId": "uuid-here",
"lpCaptureStatus": "url_only",
"lpUnavailableReason": "site blocked our captures (bot wall) — no landing page available to score",
"scores": null,
"detailedScores": null
}Check for lpCaptureStatus === "url_only" before reading scores.
When the landing page is broken
A page that answers 404 or 410, a domain that doesn't exist, a refused certificate or a page titled “not found” has nothing to score. The API returns 200 without calling the model, and the evaluation is not counted against your limit:
{
"success": true,
"evaluationId": "uuid-here",
"lpCaptureStatus": "link_broken",
"overallScore": null,
"componentScores": null,
"linkBroken": {
"url": "https://example.com/sale",
"finalUrl": "https://example.com/sale",
"httpStatus": 404,
"reason": "Returns 404 (page not found)"
}
}A server error (5xx) or a timeout is still scored: one bad answer doesn't make a page dead.
Error Codes
Errors return { error, errorCode }. 401 and 403 return only error. Failed requests are not counted against your limit.
400 UNSAFE_URL: a URL is not a public http(s) address
400 NO_AD_ASSET: neither adData.imageUrl nor adData.adUrl was given
400 AD_SCREENSHOT_FAILED: adData.adUrl could not be captured; send the image URL instead
401 Invalid, expired or revoked API key
403 Key lacks the analyze scope
429 USER_RATE_LIMIT_EXCEEDED: monthly evaluation limit reached (see rate limits)
429 IP_RATE_LIMIT_EXCEEDED: no API key was sent and the anonymous limit (1 per month per IP) is used up
500 ANALYSIS_FAILED: analysis error; safe to retry