Publishing

Table of Content

Table of Content

Table of Content

API Reference

If you can't use the SDK or prefer to make direct HTTP requests, you can interact with the Specify API directly.

API Endpoint

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

Authentication

Headers

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 Format

Request Body

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

Parameters

Parameter

Type

Required

Description

walletAddresses

string[]

Yes

Array of Ethereum wallet addresses (1-50 addresses)

imageFormat

string

Yes

Image format required for placement. Will match best ad with supported image format. See below for all formats.

adUnitId

string

No

Id used to identify individual ad placements / units. Useful for performance analytics.

Image Formats

Format

Aspect Ratio

Resolution

Use Case

LANDSCAPE

16:9

640x360

Hero banners, featured placements

LONG_BANNER

8:1

728x90

Header/footer placements, leaderboards

SHORT_BANNER

16:5

320x100

Inline 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)

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 (no markdown)

Ad headline text

content

string (markdown)

less than 400 characters

Ad content with Markdown formatting (see Content Formatting section)

ctaUrl

string

Call-to-action URL

ctaLabel

string (no markdown)

Call-to-action button text

imageUrl

string

URL to the ad image

communityName

string (no markdown)

Name of the advertising community

communityLogo

string

URL to the community logo

imageFormat

string

The requested image format (LANDSCAPE, LONG_BANNER, or SHORT_BANNER)

Content Formatting

The content field in ad responses uses simplified Markdown formatting:

Format

Syntax

Example

Bold text

**text**

**Join today** for exclusive benefits

  • Bullet points

* item

* Feature 1\n* Feature 2\n* Feature 3

Line breaks

\n

First 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

Code

Description

200

Success - Returns ad content

400

Bad Request - Invalid input data

401

Unauthorized - Invalid API key

404

Not Found - No ad available for the wallet address(es)

500

Internal 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

©

2025

The Internet Community Company. All rights reserved.

©

2025

The Internet Community Company. All rights reserved.

©

2025

The Internet Community Company. All rights reserved.