API Reference
If you can’t use the JavaScript/TypeScript SDK or prefer to make direct HTTP requests, the Specify API can be called directly.
For most publishers the SDK is the easier path — it handles caching, validation, and error classes for you. Use the API directly when you’re integrating from a language we don’t ship an SDK for, or when you want full control over the request lifecycle.
Endpoint
POST https://app.specify.sh/api/adsAuthentication
Requests are authenticated with a publisher key passed in the x-api-key header.
| Header | Value | Required |
|---|---|---|
Content-Type | application/json | Yes |
x-api-key | Your publisher key (starts with spk_) | Yes |
Publisher key format
- Must start with
spk_ - Must be exactly 34 characters long
- Example:
spk_1234567890abcdef1234567890abcdef
Request
Body
{
"walletAddresses": ["0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510"],
"imageFormat": "LANDSCAPE",
"adUnitId": "header-landscape-1"
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
walletAddresses | string[] | Yes | 1–50 EVM-compatible wallet addresses. For a user with multiple wallets, include all of them — see why |
imageFormat | string | Yes | Required image format for the placement. Specify matches the best ad available in that format. See formats |
adUnitId | string | No | Identifier for this specific placement. Used for per-placement analytics and A/B testing. Learn more |
localId | string | No | Random ID used by the browser SDK to cache a user’s wallet across sessions. Not recommended for direct API use — the SDK handles this automatically when cacheMostRecentAddress is enabled |
Wallet address validation
- Must be valid Ethereum / EVM-compatible addresses
- Must start with
0xfollowed by 40 hexadecimal characters - Case-insensitive
- Maximum 50 per request; duplicates are removed automatically
Image formats
| Format | Aspect Ratio | Resolution | Use case |
|---|---|---|---|
LANDSCAPE | 16:9 | 640×360 | Hero banners, featured placements |
LONG_BANNER | 8.09:1 | 1456×180 | Header/footer placements, leaderboards |
SHORT_BANNER | 16:5 | 640×200 | Inline content, sidebars, mobile banners |
For guidance on choosing between formats — and on building native placements beyond standard banners — see the Placements page.
Response
Success (200)
{
"walletAddress": "0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510",
"campaignId": "campaign_123",
"adId": "ad_456",
"headline": "Join the Future of DeFi",
"content": "Experience next-generation decentralized finance with our platform.",
"ctaUrl": "https://example.com/signup",
"ctaLabel": "Get Started",
"imageUrl": "https://cdn.specify.sh/ads/image_123.png",
"communityName": "DeFi Protocol",
"communityLogo": "https://cdn.specify.sh/logos/community_123.png",
"imageFormat": "LANDSCAPE"
}Response fields
| Property | Type | Description |
|---|---|---|
walletAddress | string | The wallet address that matched for this ad |
campaignId | string | Unique identifier for the ad campaign |
adId | string | Unique identifier for this specific ad |
headline | string | Ad headline text (plain text, no markdown) |
content | string | Ad body content. Supports simplified markdown. Max 400 characters |
ctaUrl | string | Call-to-action URL |
ctaLabel | string | Call-to-action button text (plain text, no markdown) |
imageUrl | string | URL to the ad image |
communityName | string | Name of the advertising community (plain text, no markdown) |
communityLogo | string | URL to the community logo |
imageFormat | string | The image format returned (LANDSCAPE, LONG_BANNER, or SHORT_BANNER) |
The response is richer than a single image — headline, content, CTA, and community branding are all returned separately so you can build native placements that go beyond basic banners.
Content formatting
The content field supports a simplified markdown subset:
| Format | Syntax | Example |
|---|---|---|
| Bold | **text** | **Join today** |
| Italic | *text* | *Italic* |
| Underline | __text__ | __Out now__ |
| Bullet points | * item | * Feature 1\n* Feature 2 |
| Line breaks | \n | First line\nSecond line |
Error response
{
"error": "Error message describing what went wrong",
"details": [
{
"field": "walletAddresses",
"message": "Invalid wallet address format"
}
]
}HTTP status codes
| Code | Meaning | Notes |
|---|---|---|
200 | Success | Returns ad content |
400 | Bad Request | Invalid input — check the details array |
401 | Unauthorized | Invalid or missing publisher key |
404 | No ad available | Normal outcome, not a bug — handle with fallback content |
500 | Internal server error | Retry or show fallback |
A 404 just means Specify had nothing relevant to serve to this user right now. Plan your UI around this case rather than treating it as failure.
Multiple wallet addresses
For a user with multiple wallets, include every address you know about in a single request (up to 50). The API returns a single ad that best matches the combined onchain footprint.
const response = await fetch('https://app.specify.sh/api/ads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'spk_your_publisher_key_here'
},
body: JSON.stringify({
walletAddresses: [
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
'0x8ba1f109551bD432803012645Hac136c0532925a',
'0x1234567890123456789012345678901234567890'
],
imageFormat: 'LONG_BANNER'
})
});This is one of the highest-leverage things you can do for targeting accuracy and conversion attribution — see why.
Examples
cURL
curl -X POST https://app.specify.sh/api/ads \
-H "Content-Type: application/json" \
-H "x-api-key: spk_your_publisher_key_here" \
-d '{
"walletAddresses": ["0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510"],
"imageFormat": "LANDSCAPE",
"adUnitId": "homepage-banner"
}'JavaScript (fetch)
async function getAd(walletAddress, imageFormat = 'LANDSCAPE', adUnitId = null) {
const requestBody = {
walletAddresses: [walletAddress],
imageFormat
};
if (adUnitId) requestBody.adUnitId = adUnitId;
const response = await fetch('https://app.specify.sh/api/ads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'spk_your_publisher_key_here'
},
body: JSON.stringify(requestBody)
});
if (response.status === 404) return null; // No ad — expected, handle with fallback
if (response.status === 401) throw new Error('Invalid API key');
if (response.status === 400) {
const errorData = await response.json();
throw new Error(`Validation error: ${errorData.error}`);
}
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
// Usage
const ad = await getAd(
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
'LONG_BANNER',
'sidebar-ad'
);
if (ad) {
console.log('Headline:', ad.headline);
console.log('CTA:', ad.ctaLabel, '->', ad.ctaUrl);
}Python (requests)
import requests
def get_ad(wallet_address, image_format='LANDSCAPE', api_key='spk_your_publisher_key_here', ad_unit_id=None):
data = {
'walletAddresses': [wallet_address],
'imageFormat': image_format
}
if ad_unit_id:
data['adUnitId'] = ad_unit_id
response = requests.post(
'https://app.specify.sh/api/ads',
headers={
'Content-Type': 'application/json',
'x-api-key': api_key
},
json=data
)
if response.status_code == 404:
return None # No ad — expected, handle with fallback
if response.status_code == 401:
raise Exception('Invalid API key')
if response.status_code == 400:
error_data = response.json()
raise Exception(f"Validation error: {error_data.get('error', 'Invalid request')}")
response.raise_for_status()
return response.json()
# Usage
ad = get_ad(
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
image_format='SHORT_BANNER',
ad_unit_id='header-banner'
)
if ad:
print('Headline:', ad['headline'])
print('CTA:', ad['ctaLabel'], '->', ad['ctaUrl'])PHP
<?php
function getAd($walletAddress, $imageFormat = 'LANDSCAPE', $apiKey = 'spk_your_publisher_key_here', $adUnitId = null) {
$data = [
'walletAddresses' => [$walletAddress],
'imageFormat' => $imageFormat
];
if ($adUnitId !== null) {
$data['adUnitId'] = $adUnitId;
}
$options = [
'http' => [
'header' => [
'Content-Type: application/json',
'x-api-key: ' . $apiKey
],
'method' => 'POST',
'content' => json_encode($data),
'ignore_errors' => true // Allow reading 4xx response bodies
]
];
$response = file_get_contents('https://app.specify.sh/api/ads', false, stream_context_create($options));
$httpCode = intval(substr($http_response_header[0], 9, 3));
switch ($httpCode) {
case 200:
return json_decode($response, true);
case 404:
return null; // No ad — expected, handle with fallback
case 401:
throw new Exception('Invalid API key');
case 400:
$errorData = json_decode($response, true);
throw new Exception('Validation error: ' . ($errorData['error'] ?? 'Invalid request'));
default:
throw new Exception('HTTP error: ' . $httpCode);
}
}
// Usage
$ad = getAd('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'LANDSCAPE', 'spk_your_publisher_key_here', 'footer-banner');
if ($ad) {
echo 'Headline: ' . $ad['headline'] . PHP_EOL;
echo 'CTA: ' . $ad['ctaLabel'] . ' -> ' . $ad['ctaUrl'] . PHP_EOL;
}
?>Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
type AdRequest struct {
WalletAddresses []string `json:"walletAddresses"`
ImageFormat string `json:"imageFormat"`
AdUnitId string `json:"adUnitId,omitempty"`
}
type AdResponse struct {
WalletAddress string `json:"walletAddress"`
CampaignID string `json:"campaignId"`
AdID string `json:"adId"`
Headline string `json:"headline"`
Content string `json:"content"`
CTAUrl string `json:"ctaUrl"`
CTALabel string `json:"ctaLabel"`
ImageUrl string `json:"imageUrl"`
CommunityName string `json:"communityName"`
CommunityLogo string `json:"communityLogo"`
ImageFormat string `json:"imageFormat"`
}
func getAd(walletAddress, imageFormat, apiKey, adUnitId string) (*AdResponse, error) {
body, err := json.Marshal(AdRequest{
WalletAddresses: []string{walletAddress},
ImageFormat: imageFormat,
AdUnitId: adUnitId,
})
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://app.specify.sh/api/ads", bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
resp, err := (&http.Client{}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
switch resp.StatusCode {
case 200:
var ad AdResponse
if err := json.Unmarshal(respBody, &ad); err != nil {
return nil, err
}
return &ad, nil
case 404:
return nil, nil // No ad — expected, handle with fallback
case 401:
return nil, fmt.Errorf("invalid API key")
case 400:
return nil, fmt.Errorf("validation error: %s", string(respBody))
default:
return nil, fmt.Errorf("HTTP error: %d", resp.StatusCode)
}
}
func main() {
ad, err := getAd(
"0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510",
"LANDSCAPE",
"spk_your_publisher_key_here",
"main-content",
)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if ad != nil {
fmt.Printf("Headline: %s\n", ad.Headline)
fmt.Printf("CTA: %s -> %s\n", ad.CTALabel, ad.CTAUrl)
}
}Limits
- Maximum 50 wallet addresses per request
- Duplicates are automatically deduplicated server-side
- Need higher limits? Contact support at support@specify.sh