Annual Ads

Developer documentation

Build directly on the Annual Ads platform — create advertisers, publish ads, trigger payments, and track rank, entirely through the API.

See the full pricing grid

You keep 70% of what your Connect-mode advertisers pay for their ads — paid automatically to your wallet. See how it works below.

Base URL

https://api.adhub365.com
OpenAPI 3

Authentication

Every request is authenticated with a secret key in the Authorization header, using the Bearer scheme.

POST https://api.adhub365.com/v1/partner/ads
Authorization: Bearer sk_sandbox_...
Content-Type: application/json

Sandbox & production

Sandbox and production keys are fully isolated from each other — a sandbox key can never read or write data created by a production key, and vice versa.

Scopes

Each key is limited to the scopes it was issued with — a key never has more access than the partner account that created it.

API keys are issued to approved partner accounts by the Annual Ads team.

Create a partner account

Ad revenue sharing

If your API keys create advertiser accounts for your own users (Connect mode — see Authentication above), you earn a share of what those advertisers pay for their ads. The split below is read live from this same endpoint, never hardcoded, and is entirely separate from the referral commission further down this page.

GET https://api.adhub365.com/v1/partner-program
{
  "partner_share_percentage": 0.7,
  "platform_share_percentage": 0.3
}

70%

Goes to you

Paid automatically to your configured payout wallet — no withdrawal request needed.

30%

Goes to Annual Ads

Covers moderation, hosting, and the ranking infrastructure your ads run on.

How it works

  1. One of your Connect-mode advertisers pays for an ad through your integration.
  2. The ad is reviewed and approved — automatically, or by our moderation team.
  3. Your share is queued for automatic payout to your wallet, same mechanism as the referral program below.
A share is never created before the ad is actually approved — if moderation rejects it, nothing is owed on that payment. A top-up on an already-active ad carries no such risk and is shared immediately.

Payout conditions

  • A crypto payout wallet is configured on your partner account.
  • No KYC is required on your side — your partner account is already vetted at creation.

Example: reading accumulated shares

GET https://api.adhub365.com/v1/partner/ad-revenue/earnings
Authorization: Bearer sk_sandbox_...
{
  "shares": [
    {
      "id": "share_1a2b...",
      "payment_id": "pay_9f2a...",
      "ad_id": "ad_7c31...",
      "partner_amount_usd": 140.0,
      "platform_amount_usd": 60.0,
      "status": "paid",
      "payable_after": "2026-08-03T00:00:00Z",
      "paid_at": "2026-08-05T10:12:00Z"
    }
  ],
  "total_payable_pending_usd": 0.0,
  "total_payable_usd": 0.0,
  "total_processing_usd": 0.0,
  "total_paid_usd": 140.0
}

Endpoints

Accounts

POST/v1/partner/advertisers

Create an advertiser account on behalf of one of your users (Connect mode).

advertisers:write
GET/v1/partner/advertisers/{id}

Look up an advertiser account created by this partner.

advertisers:read

Ads

POST/v1/partner/ads

Create an ad. It starts out in draft status. Optional advertiser_type, promotion_type, link_type, and promoted_brand fields describe affiliate, referral, creator, or individual advertising — see the note below.

ads:write
GET/v1/partner/ads/{id}

Look up an ad.

ads:read
PATCH/v1/partner/ads/{id}

Update editorial content — title, description, link, advertiser type, promotion type, link type, and promoted brand. Category, geography, and anything read by the ranking engine can never be changed here.

ads:write
POST/v1/partner/ads/{id}/image

Upload an ad image directly (JPEG/PNG/WebP, 5 MB max). Required before the first payment — see the payments group below.

ads:write
POST/v1/partner/ads/{id}/image-url

Set an ad's image from a URL instead of uploading a file — the server fetches and re-hosts it itself. Same requirement: needed before the first payment.

ads:write
GET/v1/partner/ads/{id}/rank

Current rank, category, and geo scope for an ad.

ads:read
GET/v1/partner/ads/{id}/stats

Total views and clicks for an ad — days elapsed/remaining come from the activated_at/expires_at fields already on GET /{id}, and rank from GET /{id}/rank.

ads:read

Payments

POST/v1/partner/payments

Start a crypto payment for an initial purchase or a top-up. An initial payment fails with 422 unless the ad already has an image — see uploadAdImage/setAdImageUrl above.

payments:write
GET/v1/partner/payments/{id}

Check the status of a payment.

payments:read

Referrals

POST/v1/partner/referrals

Create a referral link.

referrals:write
GET/v1/partner/referrals/{code}/earnings

Cumulative referral earnings, broken down by status.

referrals:read

Ad revenue sharing

GET/v1/partner/ad-revenue/earnings

Your 70% share of what the advertisers you created in Connect mode paid for their ads, broken down by status.

ad-revenue:read

Access log

GET/v1/partner/access-log

Full call history for this key — method, path, IP, timestamp.

Any

Public endpoints

GET/v1/rankings?category={id}&geo={scope}

Read-only ranking for a category and geo scope.

Public
GET/v1/tiers

The 7 configured pricing tiers (threshold, unlocked perks).

Public
GET/v1/referral-program

The commission percentages currently active for the referral cascade and Leaders Pool.

Public
GET/v1/partner-program

The current ad revenue split (Connect mode) between you and Annual Ads.

Public
GET/v1/search?q={query}

Natural-language search — routes a query like "furniture advertisers in Kenya" to the matching category and geo scope, then returns that ranking, in its exact real order.

Public

Affiliate & referral advertising

advertiser_type, promotion_type, link_type, and promoted_brand are optional fields on POST and PATCH /v1/partner/ads — Annual Ads is not limited to businesses advertising themselves. When link_type is affiliate_link or referral_invitation_link, or promotion_type is affiliate_offer or referral_opportunity, affiliate_terms_accepted must be true or the request is rejected with a 422. title is capped at 35 characters and description at 80 — both enforced server-side, not just in the dashboard UI.

AI Tools

Every advertiser account gets a set of built-in AI tools — an ad content and visual generator, a conversational assistant, a budget advisor, and an external SEO auditor — paid for with AI credits, on top of the flat annual pricing.

These run through the advertiser's own dashboard login (a session access token), not a partner API key — a third-party integration cannot call them on an advertiser's behalf.
POST/v1/advertisers/{id}/ai/assistant

Ask Annual Ads — a floating conversational assistant, informational only, read-only on account data.

Public
POST/v1/advertisers/{id}/ai/creative-studio

Generate an ad title, description, and keywords from a short business description.

2 credit(s)
POST/v1/advertisers/{id}/ai/creative-studio/image

Generate a listing visual (PNG) from the same business description, hosted and ready to attach to an ad.

8 credit(s)
POST/v1/advertisers/{id}/ai/budget-advisor

A real statistical projection — never a generative guess — of the odds of keeping a given rank at 30/90/365 days.

1 credit(s)
POST/v1/advertisers/{id}/ai/seo-audit

Analyze the advertiser's own external website and suggest concrete SEO improvements.

2 credit(s)

Example — generate ad content

The same category and business description also power the image generator below.

POST https://api.adhub365.com/v1/advertisers/{advertiser_id}/ai/creative-studio
Authorization: Bearer <session access token>
Content-Type: application/json

{
  "category_name": "Furniture",
  "business_description": "We sell handmade oak dining tables"
}
{
  "title": "Handmade Oak Dining Tables — Built to Last",
  "description": "Solid oak dining tables crafted by hand, built to last a lifetime.",
  "keywords": ["oak furniture", "dining table", "handmade"],
  "credits_remaining": 8
}

Generate a matching visual for the same ad:

POST https://api.adhub365.com/v1/advertisers/{advertiser_id}/ai/creative-studio/image
Authorization: Bearer <session access token>
Content-Type: application/json

{
  "category_name": "Furniture",
  "business_description": "We sell handmade oak dining tables"
}
{
  "visual_url": "https://cdn.uploadscenter.com/file_01m1...",
  "credits_remaining": 4
}

Widget

Drop a ready-made ad unit onto your own site — no build step, no iframe. The script renders directly into the page inside an isolated Shadow DOM, so its styles never leak into your site, and your site's styles never leak into it.

Add it to your page

<div
  class="annualads-widget"
  data-category="YOUR_CATEGORY_ID"
  data-geo="global"
  data-count="4"
  data-columns="2"
></div>
<script async src="https://adhub365.com/widget.js"></script>

By default this shows the full public ranking for the category — every advertiser on the platform, not just the ones you brought in. To show only the ads from advertisers you created through Connect mode (the ones generating your share), add data-partner with your partner ID (find it on the Developers page of your own dashboard):

<div
  class="annualads-widget"
  data-category="YOUR_CATEGORY_ID"
  data-geo="global"
  data-partner="YOUR_PARTNER_ID"
></div>
<script async src="https://adhub365.com/widget.js"></script>

If your advertisers span several categories, drop data-category entirely — with data-partner alone, the widget shows every one of your ads across all categories in one grid, instead of needing one widget block per category:

<div
  class="annualads-widget"
  data-geo="global"
  data-partner="YOUR_PARTNER_ID"
></div>
<script async src="https://adhub365.com/widget.js"></script>

Want a footer-style unit alongside your in-content one, each showing different ads? Add a second widget block with data-layout="compact" (a single ad, collapsible into a small pill) and data-offset set to how many ads your first widget already shows:

<!-- in your content -->
<div
  class="annualads-widget"
  data-category="YOUR_CATEGORY_ID"
  data-geo="global"
  data-count="4"
  data-columns="2"
></div>

<!-- in your footer, showing different ads via data-offset -->
<div
  class="annualads-widget"
  data-category="YOUR_CATEGORY_ID"
  data-geo="global"
  data-layout="compact"
  data-offset="4"
></div>
<script async src="https://adhub365.com/widget.js"></script>

Attributes

data-categoryCategory ID to display. Required — unless data-partner is set, in which case omitting it shows that partner's ads across every category.
data-geoGeo scope: local, regional, or global. Defaults to global.
data-countNumber of ads to show. Defaults to 4.
data-columnsNumber of grid columns. Defaults to 2.
data-layoutgrid, list, or compact. Defaults to grid. compact shows a single ad (data-count is ignored) with a button to collapse it into a small pill and bring it back — a footer-style unit, never positioned fixed by the script itself, you place and style the container div however you like on your own page.
data-offsetNumber of top-ranked ads to skip. Defaults to 0. Lets a second widget on the same page (e.g. a compact one in the footer plus a grid one further up) show different ads instead of repeating the same one twice — pass the number of ads the other widget already shows.
data-partnerYour partner ID (find it on your own dashboard's Developers page). Optional — without it, the widget shows the full public ranking for that category, every advertiser on the platform. With it, only ads from advertisers you brought in through Connect mode — the ones that actually generate your share.

Revenue share

How a partner's referral commission actually reaches them — the percentage, the payout mechanism, and the preconditions.

GET https://api.adhub365.com/v1/referral-program
{
  "levels": [
    {
      "level": 1,
      "percentage": 0.1
    }
  ],
  "leaders_pool_percentage_of_gmv": 0.05,
  "founding_advertiser_pool_percentage_of_gmv": 0.05,
  "payout_verification_window_hours": 48,
  "min_payout_usd": 1
}

Not a fixed number

The commission percentage is configured on our side and can change — always read it live from this endpoint rather than hardcoding a value.

Fully automatic

There is no withdrawal endpoint. A scheduled job matures payable earnings, batches them per advertiser, and pays out automatically once all conditions below are met.

Payout conditions

  • The advertiser’s total payable earnings reach the minimum payout amount.
  • A crypto payout wallet is configured on their account.
  • Their KYC status is verified.

Example: reading accumulated earnings

GET https://api.adhub365.com/v1/partner/referrals/{code}/earnings
Authorization: Bearer sk_sandbox_...
{
  "code": "ann-2f8c",
  "earnings": [
    {
      "id": "earn_1a2b...",
      "payment_id": "pay_9f2a...",
      "amount_usd": 30.0,
      "status": "paid",
      "payable_after": "2026-08-01T00:00:00Z",
      "paid_at": "2026-08-03T14:22:00Z"
    }
  ],
  "total_payable_pending_usd": 0.0,
  "total_payable_usd": 0.0,
  "total_processing_usd": 0.0,
  "total_paid_usd": 30.0
}

Pricing tiers (live)

Read live from this endpoint — never hardcode these values, they can change on our side. Build a tier picker for your own users instead of a free amount field: each price shown is already the exact amount to send when creating the payment, and the unlocked benefits shown here tell users exactly what that price gets them, so they choose a price they understand instead of guessing a number.

TierPriceUnlocks
Bronze$50.00

Basic visibility

Silver$300.00

Clickable link unlocked

Clickable link
Gold$500.00

Animation unlocked

Clickable linkAnimation
Platinum$1,000.00

Enhanced exposure

Clickable linkAnimation
Diamond$2,500.00

Premium placement

Clickable linkAnimation
Elite$5,000.00

Top-tier visibility

Clickable linkAnimation
Legendary$10,000.00

Maximum visibility & branding

Clickable linkAnimation

Rate limits

Requests are capped per key, per minute. Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers; going over the limit returns 429 Too Many Requests with a Retry-After header.

IP allowlist

Optional, per partner. Until you add an entry, your keys accept requests from any IP — the first entry switches all of that partner's keys to allowlist-only.

Webhooks

Every webhook is signed with HMAC-SHA256 using a secret issued once, at creation time — verify the signature before trusting the payload. Events are delivered only to the partner that owns the related advertiser.

payment.succeededA payment is confirmed.
payment.refundedA refund is executed.
ad.activatedAn ad goes active, automatically or after admin review.
invoice.issuedAn invoice is issued.
referral.payout.completedA referral commission reaches paid status.
referral.payout.failedA referral payout batch fails at the provider — earnings return to payable and are retried.
rank.changedAn ad's rank changes — including when another advertiser's payment causes it.
ad.expiring_soon30, 7, or 1 day(s) before an ad expires.
partner_ad_revenue.payout.completedAn ad revenue share payout reaches paid status.
partner_ad_revenue.payout.failedAn ad revenue share payout batch fails at the provider — shares return to payable and are retried.

SDKs

Official JavaScript/TypeScript and Python SDKs, generated from this same API specification, are planned but not yet published — call the HTTP API directly until then.

Quickstart

No SDK yet — these call the HTTP API directly and work today in any language.

cURL

curl -X POST https://api.adhub365.com/v1/partner/ads \
  -H "Authorization: Bearer sk_sandbox_..." \
  -H "Content-Type: application/json" \
  -d '{"advertiser_id":"adv_9f2a...","category_id":"cat_furniture","geo_scope":"local","title":"..."}'

JavaScript

await fetch("https://api.adhub365.com/v1/partner/ads", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_sandbox_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    advertiser_id: "adv_9f2a...",
    category_id: "cat_furniture",
    geo_scope: "local",
    title: "...",
  }),
});

Python

import requests

requests.post(
    "https://api.adhub365.com/v1/partner/ads",
    headers={"Authorization": "Bearer sk_sandbox_..."},
    json={
        "advertiser_id": "adv_9f2a...",
        "category_id": "cat_furniture",
        "geo_scope": "local",
        "title": "...",
    },
)