REST API
The management API behind the Developer Portal.
Applies to Appylar v1.0
The Appylar platform API is the REST API behind the Developer Portal. Everything the portal does — apps, campaigns, creatives, analytics and billing — is available here. This page documents every endpoint that exists in v1. There are no fictional or future endpoints.
#Conventions
- Base URL: all routes are under the
/v1prefix, e.g.https://api.appylar.com/v1/campaigns. - Auth: send your token as
Authorization: Bearer <token>. Get one from register or login. - Content type: send
Accept: application/json; send JSON bodies asContent-Type: application/json(creative upload usesmultipart/form-data). - Tenancy: every request is scoped to your organization. Rows from other organizations are invisible, so requesting one returns
404.
curl https://api.appylar.com/v1/me \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json"
#Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid token |
403 | Admin-only endpoint, non-admin caller |
404 | Not found (or not in your organization) |
422 | Validation failed |
429 | Rate limit exceeded |
Validation errors use the standard Laravel shape:
{
"message": "The name field is required.",
"errors": { "name": ["The name field is required."] }
}
Collections that paginate (apps, campaigns) return { "data": [...], "links": {...}, "meta": {...} } at 15 per page. A single resource returns { "data": {...} }. Auth, reports and billing reads return raw JSON (no data wrapper), as noted per endpoint.
#Rate limits & stability
Auth, token, serve and events endpoints are rate-limited per minute; exceeding a limit returns 429 — back off and retry. This is v1: the surface documented here is stable, and any breaking change will be announced in the Changelog with advance notice and a migration note before it ships. Build against the documented contract and you won't be surprised.
#Authentication
#POST /v1/auth/register
Create a user, its default organization, and return an API token.
| Field | Rules |
|---|---|
name | required, string, max 255 |
email | required, email, unique |
password | required, min 8 |
organization_name | optional, string, max 255 |
curl -X POST https://api.appylar.com/v1/auth/register \
-H "Accept: application/json" \
-d "name=Ada Lovelace&email=ada@studio.com&password=secret123"
{
"token": "1|abc...",
"user": {
"id": 1, "name": "Ada Lovelace", "email": "ada@studio.com",
"is_admin": false,
"organization": { "id": 1, "name": "Ada Lovelace's Organization" }
}
}
Returns 201. Errors: 422, 429.
#POST /v1/auth/login
Exchange email + password for a token. Body: email (required), password (required). Returns 200 with the same shape as register. Bad credentials return 422.
#POST /v1/auth/logout
Revoke the current token. Requires auth. Returns 200 { "message": "Logged out." }.
#GET /v1/me
The current user and their organization. Requires auth.
{ "user": { "id": 1, "name": "Ada Lovelace", "email": "ada@studio.com", "is_admin": false, "organization": { "id": 1, "name": "…" } } }
#Organization
#GET /v1/orgs/current
Your current organization. Returns raw { "id": 1, "name": "…", "network_opt_in": true }.
#PATCH /v1/orgs/current
Update org settings. Body: network_opt_in (optional, boolean). Returns the updated organization.
#Apps
An app resource:
{
"id": 1, "name": "My Game", "platform": "android",
"store_url": null, "category": null,
"app_key": "app_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"network_opt_in": true,
"created_at": "…", "updated_at": "…"
}
#GET /v1/apps
List your apps (paginated, 15/page, newest first).
#POST /v1/apps
Create an app; the app_key is generated for you.
| Field | Rules |
|---|---|
name | required, string, max 255 |
platform | optional, android |
store_url | optional, url, max 2048 |
category | optional, string, max 255 |
network_opt_in | optional, boolean |
Returns 201 { "data": App }.
#GET /v1/apps/{id}
Show one app. Returns { "data": App } or 404.
#PATCH /v1/apps/{id}
Update name, store_url, category or network_opt_in (all optional). platform can't be changed. Returns the updated app.
#POST /v1/apps/{id}/key/rotate
Issue a new app_key, invalidating the previous one immediately. Returns the app with its new key.
Rotating a key breaks any released build still using the old one until you ship an update. See Troubleshooting.
#Campaigns
A campaign resource:
{
"id": 1, "name": "Summer promo", "status": "draft",
"reach_type": "network",
"target_geo": ["US", "GB"], "frequency_cap_per_day": 3,
"promoted_app_id": 1, "promoted_app": { "id": 1, "name": "My Game" },
"created_at": "…", "updated_at": "…"
}
#GET /v1/campaigns
List campaigns (paginated, 15/page).
#POST /v1/campaigns
Create a campaign. Starts in draft.
| Field | Rules |
|---|---|
name | required, string, max 255 |
promoted_app_id | required, must be one of your apps |
reach_type | required, portfolio or network |
target_geo | optional, array of ISO-3166 alpha-2 codes (US, GB, …) |
frequency_cap_per_day | optional, int 1–1000 |
curl -X POST https://api.appylar.com/v1/campaigns \
-H "Authorization: Bearer <token>" -H "Accept: application/json" \
-d "name=Summer promo&promoted_app_id=1&reach_type=network" \
--data-urlencode "target_geo[0]=US"
Returns 201 { "data": Campaign }.
#GET /v1/campaigns/{id}
Show one campaign. Returns { "data": Campaign } or 404.
#PATCH /v1/campaigns/{id}
Partial update — same fields as create, all optional. status is not settable here (use activate/pause).
#DELETE /v1/campaigns/{id}
Delete a campaign. Returns 204. An active campaign can't be deleted — pause it first, or you get 422:
{ "errors": { "status": ["An active campaign cannot be deleted; pause it first."] } }
#POST /v1/campaigns/{id}/activate
Move draft/paused → active. Returns the campaign. 422 if already active.
#POST /v1/campaigns/{id}/pause
Move active → paused. Returns the campaign. 422 if not active.
#Creatives
Each campaign has at most one creative. A creative resource:
{
"id": 1, "campaign_id": 1,
"moderation_status": "pending", "moderation_reason": null,
"original_url": "https://…/original.png",
"variants": [
{ "format": "banner", "width": 320, "height": 50, "url": "https://…" },
{ "format": "interstitial", "width": 320, "height": 480, "url": "https://…" }
],
"created_at": "…", "updated_at": "…"
}
#GET /v1/campaigns/{id}/creative
Show the campaign's creative. Returns { "data": Creative }, or 404 if the campaign has no creative yet.
#POST /v1/campaigns/{id}/creative
Upload or replace the creative (multipart/form-data).
| Field | Rules |
|---|---|
image | required, image, mimes jpeg,jpg,png,webp,gif, max 5120 KB |
curl -X POST https://api.appylar.com/v1/campaigns/1/creative \
-H "Authorization: Bearer <token>" -H "Accept: application/json" \
-F "image=@banner.png"
Returns 201 on first upload, 200 when replacing. See Creatives.
#DELETE /v1/campaigns/{id}/creative
Delete the creative. Returns 204.
#Reports
#GET /v1/reports
Aggregated impressions, clicks, installs and derived CTR.
| Query | Rules |
|---|---|
dimension | total (default), app, campaign, day |
from | YYYY-MM-DD (default: 29 days ago) |
to | YYYY-MM-DD, ≥ from (default: today) |
app_id | optional filter |
campaign_id | optional filter |
curl "https://api.appylar.com/v1/reports?dimension=day&from=2026-07-01&to=2026-07-15" \
-H "Authorization: Bearer <token>" -H "Accept: application/json"
{
"dimension": "day", "from": "2026-07-01", "to": "2026-07-15",
"rows": [
{ "date": "2026-07-01", "impressions": 1800, "clicks": 48, "installs": 6, "ctr": 0.0267 }
]
}
app rows add app_id + label; campaign rows add campaign_id + label; total is a single summed row. ctr is clicks / impressions rounded to 4 decimals (0 when there are no impressions). See Analytics.
#Billing
Everything is measured in impressions. Network serving draws from the current period's plan allowance first, then the top-up balance. See Billing for the model.
#GET /v1/billing/summary
The current picture: plan, this period's allowance (granted/used/remaining), top-up balance and total available. Raw JSON.
{
"plan": { "key": "growth", "name": "Growth", "monthly_impressions": 500000 },
"subscription": { "status": "active", "current_period_start": "…", "current_period_end": "…", "cancel_at_period_end": false },
"allowance": { "granted": 500000, "used": 148200, "remaining": 351800 },
"topup_balance": 47000,
"total_available": 398800
}
#GET /v1/billing/plans · GET /v1/billing/packs
The active plan catalog and top-up pack catalog. Each returns { "data": [ … ] }.
#POST /v1/billing/subscribe
Start a subscription checkout. Body: plan (required, an active plan key). Returns raw { "url": "https://checkout…" } — redirect the user there.
#POST /v1/billing/top-up
Start a one-off top-up checkout. Body: pack (required, an active pack key). Returns raw { "url": "https://checkout…" }.
#GET /v1/billing/balance
The top-up balance only: raw { "balance": 47000 }. (The full picture is in /billing/summary.)
#GET /v1/billing/transactions
The latest 100 top-up ledger entries.
{ "data": [
{ "id": 2, "delta": -320, "reason": "network_impression", "balance_after": 46680, "created_at": "…" },
{ "id": 1, "delta": 50000, "reason": "purchase", "balance_after": 47000, "created_at": "…" }
] }
reason is purchase (top-up bought) or network_impression (impressions drawn from the top-up balance).
#Data plane (used by the SDKs)
These endpoints are what the SDKs call internally. You normally don't call them directly — they're documented for completeness. Together they form the serving protocol; each request carries the Appylar-Protocol and Appylar-SDK headers (both optional and ignored if absent — legacy clients keep working).
- POST /v1/token — exchange an
app_keyfor a short-lived serving token ({ "token": "…", "expires_in": 3600 }). Public. - GET /v1/sdk/config — SDK config after init, including
protocol_version. Serving token required. - POST /v1/serve — return at most one eligible ad, or
{ "fill": false }. Accepts optionaldevice_id,os(android/ios) andstore(e.g.google_play,apple_app_store,amazon_appstore) to route the ad'sdestination_urlto the right store listing. Network serves consume 1 impression from the advertiser (allowance first, then top-up). Serving token required. - POST /v1/events — batch impression/click/install events (idempotent). Serving token required.
See the Serving Protocol for the full field reference, store-routing rules, versioning and backward-compatibility guarantees.
#Admin
Moderation endpoints under /v1/admin/* require an administrator account (403 otherwise) and are operated by the Appylar team — creatives are approved or rejected here. They aren't part of the developer-facing surface.