API reference
Serve Specify ads through the HTTP API when you can't use the SDK or prefer raw requests.
Use the Specify HTTP API if you can't use the JavaScript SDK or prefer to manage requests yourself.
The SDK handles validation, the consent flag and the credentialed request for you. The HTTP API suits languages without a Specify SDK, and integrations that need control over the request lifecycle.
Endpoint
POST https://spfsrv.com/v1/adsAuthentication
Specify authenticates each request with the publisher key in its 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_1234567890abcdef1234567890abcd
Request
Body
{
"walletAddresses": ["0x1111111111111111111111111111111111111111"],
"imageFormat": "LANDSCAPE",
"adUnitId": "header-landscape-1",
"cookieConsent": false
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
walletAddresses | string[] | Yes* | Up to 50 EVM-compatible wallet addresses. For a user with multiple wallets, include all of them. See why. *Optional when the request carries Specify's identity cookie |
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 |
cookieConsent | boolean | No | Whether the visitor consented to Specify's identity cookie. Defaults to false. Without it no cookie is read or set, and the request is matched on walletAddresses alone |
The identity cookie
Specify can set one cookie on spfsrv.com that recognises a returning visitor across every site in the network. It is what fills a placement for someone who has connected a wallet elsewhere but not on your page.
This only applies to browser requests, and only with cookieConsent: true. Send the request with credentials (credentials: 'include' in fetch) so the cookie can be set.
Wallet address validation
- Must be valid Ethereum / EVM-compatible addresses
- Must start with
0xfollowed by 40 hexadecimal characters - Case-insensitive
- Send at most 50 addresses per request. The server removes duplicates.
- Every address in a request must belong to the same person. This is not a bulk endpoint: asking for one ad on behalf of unrelated visitors mixes their onchain footprints and returns an ad that fits none of them. One request per visitor. See why.
Image formats
| Format | Ratio | Pixels | Best for |
|---|---|---|---|
LANDSCAPE | 16:9 | 640 × 360 | Hero banners, featured placements |
LONG_BANNER | 8.09:1 | 1456 × 180 | Header and footer placements, leaderboards |
SHORT_BANNER | 16:5 | 640 × 200 | Inline content, sidebars, mobile banners |
NO_IMAGE | n/a | text only | Dense lists, text feeds, places an image would not fit |
For help choosing a format or building a native placement, see the Placements page.
Response
Success (200)
{
"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 |
|---|---|---|
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 | null | URL to the ad image (null for NO_IMAGE placements) |
communityName | string | Name of the advertising community (plain text, no markdown) |
communityLogo | string | URL to the community logo |
imageFormat | string | The format returned (LANDSCAPE, LONG_BANNER, SHORT_BANNER, or NO_IMAGE) |
adUnitId | string | Echoes the adUnitId from the request, when one was sent |
Specify returns the headline, content, CTA, and community branding as separate fields. Use them to build native placements as well as 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 |
204 | No ad available | Expected outcome, with no response body. Handle with fallback content |
400 | Bad Request | Invalid input. Check the details array |
401 | Unauthorized | Invalid or missing publisher key |
503 | Serving unavailable | Specify could not resolve the request. Show fallback content |
A 204 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. Anything other than a 200 with a body means render nothing.
Examples
curl -X POST https://spfsrv.com/v1/ads \
-H "Content-Type: application/json" \
-H "x-api-key: spk_your_publisher_key_here" \
-d '{
"walletAddresses": ["0x1111111111111111111111111111111111111111"],
"imageFormat": "LANDSCAPE",
"adUnitId": "homepage-banner"
}'