API for apps
Two endpoints, no authentication, no SDK. One asks what to show, the other says what happened. If your app already renders a hardcoded array of cross-promotion cards, the only change is where the array comes from.
Ask what to show
One request per app, cached at the edge.
/api/serve?app=YOUR_APP_IDReturns the cards this app should render right now: the paid card for the hour that is running, if it is sold and approved and paying, then the house entries. Your own app is never in its own list.
Response
{ "ads": [ { "id": "b6f1c0a2-...", "name": "Notemill", "url": "https://notemill.com", "domain": "notemill.com", "tagline": "Notes that work offline", "body": "A notebook that keeps working when the connection does not.", "accent": "text-gray-300 group-hover:text-gray-100", "monogram": "N", "paid": true }, { "id": "sellular", "name": "Sellular", "url": "https://sellular.online", "domain": "sellular.online", "tagline": "Get the company found", "body": "Submit your product to 30+ directories with pre-filled profiles.", "accent": "text-lime-400 group-hover:text-lime-300", "monogram": "S", "discount": { "percent": 40, "code": "LAUNCH40", "expiresAt": null } } ], "slot": { "forSale": false, "pricingUrl": "https://sparkpay.dev/spark/sparkads", "from": "$9/month" } }
Each entry in ads:
| Field | Type | Description |
|---|---|---|
id | string | Stable key. A creative UUID, or a house entry slug. |
name | string | App name, up to 40 characters. |
url | string | Where the card links. Always https. |
domain | string | Bare hostname. The logo and accent colour are resolved from it. |
tagline | string | Four or five words, under the name. |
body | string | One sentence, up to 160 characters. |
accent | string | Tailwind text colour pair for the name, resting then hover. A fallback only. |
monogram | string | One letter, for when no logo file resolves. |
appId | string? | The SparkPay app_id, present on house entries whose slug differs from it. |
paid | boolean? | True on the one paid card, when this hour is sold, approved and paying. |
discount | object? | A live public coupon: percent, code, expiresAt. House entries only. |
The slot object is what a consumer needs to decide whether to append its own "your app here" card: forSale is true whenever this hour has no paid ad in it, and pricingUrl is where to send whoever clicks.
Cache and CORS
Five minutes on the CDN, and open to every origin.
| Field | Type | Description |
|---|---|---|
Cache-Control | header | public, s-maxage=300, stale-while-revalidate=600 |
Access-Control-Allow-Origin | header | * on both endpoints |
Every consumer app is another origin and the payload is public inventory, so the response is wide open on purpose. There is no credential to leak because there is no credential.
Report what happened
Fire and forget. No auth, tiny body, 204 back.
/api/trackCounts one impression or one click. The count lands in an hourly aggregate row keyed by hour, app, creative and source, so this is an increment rather than an event log.
| Field | Type | Description |
|---|---|---|
apprequired | string | Your SparkPay app_id. Slug characters only, 40 max. |
creativeIdrequired | string | The id from the ad you are reporting on. |
eventrequired | string | Exactly "impression" or "click". Anything else is a 400. |
source | string? | Either "sellular" or "direct". Anything else becomes "network". |
From a consumer carousel
navigator.sendBeacon(
'https://sparkads.dev/api/track',
JSON.stringify({
app: 'tax-ducks',
creativeId: 'b6f1c0a2-...',
event: 'impression',
source: 'direct'
})
);The body is parsed as text, not by content type, so a sendBeacon with its text/plain default stays a simple request and skips the preflight. A normal fetch with JSON headers works the same.
| Field | Type | Description |
|---|---|---|
204 | status | Counted, or quietly dropped. Nothing back. |
400 | status | Missing field, unknown event, or an id that is not slug or UUID shaped. |
429 | status | Over 300 requests a minute from one IP. |
Discounts on the cards
Resolved once on the network, never hardcoded by a consumer.
A house entry can carry a discount, which the serve route resolves from SparkPay and caches for fifteen minutes. Consumers never look coupons up themselves: that would be ten apps asking the same question, and a percentage baked into a fallback array outlives the sale it advertises.
One hour, every dashboard
Twenty four hours in the day, one advertiser in each. $9 a month while you hold yours.