Skip to Content
PublishingAPI Reference

API Endpoint

POST https://app.specify.sh/api/ads

Authentication

Headers

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 Format

Request Body

{ "walletAddresses": ["0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510"], "imageFormat": "LANDSCAPE", "adUnitId": "header-landscape-1" }

Parameters

ParameterTypeRequiredDescription
walletAddressesstring[]YesArray of Ethereum wallet addresses (1-50 addresses)
imageFormatstringYesImage format required for placement. Will match best ad with supported image format. See below for all formats.
adUnitIdstringNoId used to identify individual ad placements / units. Useful for performance analytics.
localIdstringNoRandom ID assigned for more efficient serving even after a user disconnects wallet (false by default). Supported only on browser clients. Recommended to not use directly with API.

Image Formats

FormatAspect RatioResolutionUse Case
LANDSCAPE16:9640x360Hero banners, featured placements
LONG_BANNER8.09:1728x90Header/footer placements, leaderboards
SHORT_BANNER16:5320x100Inline content, sidebars, mobile banners

Wallet Address Validation

  • Must be valid Ethereum/EVM-compatible addresses
  • Must start with 0x followed by 40 hexadecimal characters
  • Case-insensitive
  • Example: 0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510

Response Format

Returned Data (200)

PropertyTypeDescription
walletAddressstringThe wallet address that matched for this ad
campaignIdstringUnique identifier for the ad campaign
adIdstringUnique identifier for this specific ad
headlinestring (no markdown)Ad headline text
contentstring (markdown) / less than 400 charactersAd content with Markdown formatting (see Content Formatting section)
ctaUrlstringCall-to-action URL
ctaLabelstring (no markdown)Call-to-action button text
imageUrlstringURL to the ad image
communityNamestring (no markdown)Name of the advertising community
communityLogostringURL to the community logo
imageFormatstringThe requested image format (LANDSCAPE, LONG_BANNER, or SHORT_BANNER)

Content Formatting

The content field in ad responses uses simplified Markdown formatting:

FormatSyntaxExample
Bold text**text****Join today** for exclusive benefits
Bullet points* item* Feature 1\n* Feature 2\n* Feature 3
Line breaks\nFirst line\nSecond line
Underlined Text__text____Out now__
Italic Text*text**Italic*

Success Response (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" }

Error Response (4xx/5xx)

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

HTTP Status Codes

CodeDescription
200Success - Returns ad content
400Bad Request - Invalid input data
401Unauthorized - Invalid API key
404Not Found - No ad available for the wallet address(es)
500Internal Server Error

Usage 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 API)

async function getAd(walletAddress, imageFormat = 'LANDSCAPE', adUnitId = null) { try { const requestBody = { walletAddresses: [walletAddress], imageFormat: imageFormat }; // Add adUnitId only if provided 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) { console.log('No ad found for this wallet'); return null; } 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! status: ${response.status}`); } const adData = await response.json(); return adData; } catch (error) { console.error('Error fetching ad:', error); throw error; } } // Usage examples getAd('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'LONG_BANNER') .then(ad => { if (ad) { console.log('Ad data:', ad); console.log('Headline:', ad.headline); console.log('CTA:', ad.ctaLabel, '->', ad.ctaUrl); } else { console.log('No ad available'); } }) .catch(error => { console.error('Failed to get ad:', error); }); // Usage with adUnitId getAd('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'LONG_BANNER', 'sidebar-ad') .then(ad => { if (ad) { console.log('Ad data:', ad); console.log('Headline:', ad.headline); console.log('CTA:', ad.ctaLabel, '->', ad.ctaUrl); } else { console.log('No ad available'); } }) .catch(error => { console.error('Failed to get ad:', error); });

Python (requests)

import requests import json def get_ad(wallet_address, image_format='LANDSCAPE', api_key='spk_your_publisher_key_here', ad_unit_id=None): url = 'https://app.specify.sh/api/ads' headers = { 'Content-Type': 'application/json', 'x-api-key': api_key } data = { 'walletAddresses': [wallet_address], 'imageFormat': image_format } # Add adUnitId only if provided if ad_unit_id: data['adUnitId'] = ad_unit_id try: response = requests.post(url, headers=headers, json=data) if response.status_code == 404: print('No ad found for this wallet') return None 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() # Raises an HTTPError for bad responses return response.json() except requests.RequestException as e: print(f'Error fetching ad: {e}') raise # Usage examples try: ad = get_ad('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'SHORT_BANNER') if ad: print('Ad headline:', ad['headline']) print('Ad content:', ad['content']) print('CTA:', ad['ctaLabel'], '->', ad['ctaUrl']) else: print('No ad available') except Exception as e: print(f'Failed to get ad: {e}') # Usage with adUnitId try: ad = get_ad('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'SHORT_BANNER', ad_unit_id='header-banner') if ad: print('Ad headline:', ad['headline']) print('Ad content:', ad['content']) print('CTA:', ad['ctaLabel'], '->', ad['ctaUrl']) else: print('No ad available') except Exception as e: print(f'Failed to get ad: {e}')

PHP

<?php function getAd($walletAddress, $imageFormat = 'LANDSCAPE', $apiKey = 'spk_your_publisher_key_here', $adUnitId = null) { $url = 'https://app.specify.sh/api/ads'; $data = [ 'walletAddresses' => [$walletAddress], 'imageFormat' => $imageFormat ]; // Add adUnitId only if provided if ($adUnitId !== null) { $data['adUnitId'] = $adUnitId; } $options = [ 'http' => [ 'header' => [ 'Content-Type: application/json', 'x-api-key: ' . $apiKey ], 'method' => 'POST', 'content' => json_encode($data) ] ]; $context = stream_context_create($options); $response = file_get_contents($url, false, $context); if ($response === FALSE) { throw new Exception('Failed to fetch ad'); } // Get HTTP response code $httpCode = intval(substr($http_response_header[0], 9, 3)); switch ($httpCode) { case 200: return json_decode($response, true); case 404: echo 'No ad found for this wallet'; return null; 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! status: ' . $httpCode); } } // Usage examples try { $ad = getAd('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'LANDSCAPE'); if ($ad) { echo 'Ad headline: ' . $ad['headline'] . PHP_EOL; echo 'Ad content: ' . $ad['content'] . PHP_EOL; echo 'CTA: ' . $ad['ctaLabel'] . ' -> ' . $ad['ctaUrl'] . PHP_EOL; } else { echo 'No ad available' . PHP_EOL; } } catch (Exception $e) { echo 'Failed to get ad: ' . $e->getMessage() . PHP_EOL; } // Usage with adUnitId try { $ad = getAd('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', 'LANDSCAPE', 'spk_your_publisher_key_here', 'footer-banner'); if ($ad) { echo 'Ad headline: ' . $ad['headline'] . PHP_EOL; echo 'Ad content: ' . $ad['content'] . PHP_EOL; echo 'CTA: ' . $ad['ctaLabel'] . ' -> ' . $ad['ctaUrl'] . PHP_EOL; } else { echo 'No ad available' . PHP_EOL; } } catch (Exception $e) { echo 'Failed to get ad: ' . $e->getMessage() . 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"` // omitempty excludes field if empty } 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) { url := "https://app.specify.sh/api/ads" requestBody := AdRequest{ WalletAddresses: []string{walletAddress}, ImageFormat: imageFormat, AdUnitId: adUnitId, // Will be omitted from JSON if empty due to omitempty tag } jsonData, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("x-api-key", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } switch resp.StatusCode { case 200: var ad AdResponse if err := json.Unmarshal(body, &ad); err != nil { return nil, err } return &ad, nil case 404: return nil, nil // No ad found case 401: return nil, fmt.Errorf("invalid API key") case 400: return nil, fmt.Errorf("validation error: %s", string(body)) default: return nil, fmt.Errorf("HTTP error! status: %d", resp.StatusCode) } } func main() { // Usage without adUnitId ad, err := getAd("0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510", "LANDSCAPE", "spk_your_publisher_key_here", "") if err != nil { fmt.Printf("Error: %v\n", err) return } if ad != nil { fmt.Printf("Ad headline: %s\n", ad.Headline) fmt.Printf("Ad content: %s\n", ad.Content) fmt.Printf("CTA: %s -> %s\n", ad.CTALabel, ad.CTAUrl) } else { fmt.Println("No ad available") } // Usage with adUnitId adWithUnit, err := getAd("0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510", "LANDSCAPE", "spk_your_publisher_key_here", "main-content") if err != nil { fmt.Printf("Error: %v\n", err) return } if adWithUnit != nil { fmt.Printf("Ad headline: %s\n", adWithUnit.Headline) fmt.Printf("Ad content: %s\n", adWithUnit.Content) fmt.Printf("CTA: %s -> %s\n", adWithUnit.CTALabel, adWithUnit.CTAUrl) } else { fmt.Println("No ad available") } }

Multiple Wallet Addresses

You can send multiple wallet addresses in a single request (maximum 50 addresses):

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' }) });

The API will return a single ad that best matches the provided wallet addresses. To be used for one user with multiple wallets

Rate Limits

  • Maximum 50 wallet addresses per request
  • Duplicate addresses are automatically removed
  • Contact support if you need higher rate limits
Last updated on