Skip to Content
PublishingAPI Reference

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/ads

Authentication

Requests are authenticated with a publisher key passed in the x-api-key header.

HeaderValueRequired
Content-Typeapplication/jsonYes
x-api-keyYour 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

ParameterTypeRequiredDescription
walletAddressesstring[]Yes1–50 EVM-compatible wallet addresses. For a user with multiple wallets, include all of them — see why
imageFormatstringYesRequired image format for the placement. Specify matches the best ad available in that format. See formats
adUnitIdstringNoIdentifier for this specific placement. Used for per-placement analytics and A/B testing. Learn more
localIdstringNoRandom 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 0x followed by 40 hexadecimal characters
  • Case-insensitive
  • Maximum 50 per request; duplicates are removed automatically

Image formats

FormatAspect RatioResolutionUse case
LANDSCAPE16:9640×360Hero banners, featured placements
LONG_BANNER8.09:11456×180Header/footer placements, leaderboards
SHORT_BANNER16:5640×200Inline 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

PropertyTypeDescription
walletAddressstringThe wallet address that matched for this ad
campaignIdstringUnique identifier for the ad campaign
adIdstringUnique identifier for this specific ad
headlinestringAd headline text (plain text, no markdown)
contentstringAd body content. Supports simplified markdown. Max 400 characters
ctaUrlstringCall-to-action URL
ctaLabelstringCall-to-action button text (plain text, no markdown)
imageUrlstringURL to the ad image
communityNamestringName of the advertising community (plain text, no markdown)
communityLogostringURL to the community logo
imageFormatstringThe 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:

FormatSyntaxExample
Bold**text****Join today**
Italic*text**Italic*
Underline__text____Out now__
Bullet points* item* Feature 1\n* Feature 2
Line breaks\nFirst line\nSecond line

Error response

{ "error": "Error message describing what went wrong", "details": [ { "field": "walletAddresses", "message": "Invalid wallet address format" } ] }

HTTP status codes

CodeMeaningNotes
200SuccessReturns ad content
400Bad RequestInvalid input — check the details array
401UnauthorizedInvalid or missing publisher key
404No ad availableNormal outcome, not a bug — handle with fallback content
500Internal server errorRetry 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
Last updated on