Control plane
SmartNews and Mintegral Clients-API
Clients, auto-import worker, auth profiles, audit logs and access settings.
firestore db clients 0 auto 0 role — user —
Admin console
Manage client config, auth profiles, auto-import presets, sheet account discovery and audit logs from one panel.
/admin/api/clients /admin/api/auto-imports /admin/api/auth-profiles /admin/api/audit-logs
Runtime
Current backend config.
Clients
0
Profiles
0
Auto-imports
0
Backend
Database
Recent audit log
Latest entries.
No logs loaded yet.
Last API result
Response preview.
No result yet.
Clients
Stored client configs.
ClientStatusModeTrackerAccountsToken
Selected client
Summary and actions.
Select a client to inspect it.
No client selected.
Client editor
Select a client to edit, or create a new one.
SmartNews ad account IDs allowed for this client. One per line or comma-separated.
Lets the client fire Snapchat Conversions API events without seeing the shared P2W asset_id + access_token. Leave blank to disable the proxy.
Result
Server response after saving.
Auto-import
Scheduled tracker data imports.
NameClientTrackerNext runStatus
Auto-import setup
Click + New to create a preset, or pick one from the table to edit.
Click + New in the auto-import panel or select an existing preset from the table.
SmartNews profiles
SmartNews credentials or Secret Manager refs.
NameClient IDSecret refUpdated
Profile editor
Create or update auth profile.
No actions yet.
Mintegral auth profiles
Mintegral API key + Access key, or Secret Manager refs.
NameAPI keyAccess keyUpdated
Mintegral profile editor
Plain credentials or Secret Manager refs.
No actions yet.
Bigo providers
Bigo Ads advertiser API: Client ID + Secret + Refresh token (from the Bigo account manager).
NameModeEmailAccountAgencyAuthUpdated
Bigo provider editor
Credentials issued by the Bigo account manager. Leave a secret blank to keep the stored value.
No actions yet.
MediaGo auth profiles
MediaGo API token (+ optional scope / account id), or Secret Manager ref.
NameAPI tokenScopeAccount idUpdated
MediaGo profile editor
Plain token or Secret Manager ref.
No actions yet.
SmartNews Pixel auth profiles
P2W partner credentials for the SmartNews Pixel Conversion API (numeric partner_name + UUID access_token), shared across all SmartNews clients. Per-client isolation runs through allowed_pixel_ids on each client.
Namepartner_nameaccess_tokenUpdated
SmartNews Pixel profile editor
Plain credentials or Secret Manager refs. partner_name is a numeric id (issued by SmartNews as int). access_token is a UUID v4, static — rotate by issuing a new value from SmartNews.
No actions yet.
AppsFlyer partner profiles
AppsFlyer Bearer tokens for our P2W partner account in AppsFlyer Partner Marketplace, used by InCost API auto-import. One profile per ad-network-account; "default" is fine for most setups.
NameAPI tokenmedia_sourcechannelUpdated
AppsFlyer profile editor
Get the API token from AppsFlyer Dashboard → Account → Security Center → V2.0 API token. media_source is the network identifier on advertisers' AppsFlyer attribution links — the value of the pid query parameter, fixed by AppsFlyer when our partner account was set up. For P2W this is point2web_int (the value also used by clients on their attribution-link templates). Cost rows are matched to clicks via this identifier — wrong media_source → 0% match rate.
Sent as the channel field on every cost row. Surfaces in client AppsFlyer dashboards as a sub-dimension under media_source — letting clients see the upstream source (e.g. mintegral) even though all cost lands under point2web_int. Cap: 20 characters. Leave blank to omit.
No actions yet.
Documentation
SmartNews and Mintegral Clients-API — technical & user reference. Bundled offline with this admin UI.
On this page63 sections
Overview CHAPTER 1. High-level architecture Step 1. Main data flow Step 2. Runtime components Step 3. Supported source modes CHAPTER 2. API endpoints Step 1. Health check Step 2. Legacy sync endpoints Step 3. SmartNews cost import endpoint CHAPTER 3. Admin API endpoints Step 1. Session and runtime endpoints Step 2. Client endpoints Step 3. Auth profile endpoints Step 4. Audit logs and templates Step 5. Auto-import endpoints Step 6. Access settings endpoints CHAPTER 4. Firestore configuration Step 1. Collections Step 2. Client document structure Step 3. Extractor configurations Step 4. Auth profile document Step 5. Auto-import document structure Step 6. Worker state CHAPTER 5. Auto-import worker behavior Step 1. Scheduling logic Step 2. RedTrack push behavior Step 3. ClickFlare manual cost behavior Step 4. All-time cost mode CHAPTER 6. Web interface documentation Step 1. Login flow Step 2. Roles Step 3. Overview screen Step 4. Clients screen Step 5. Client editor Step 6. Auto-import screen Step 7. Auth profiles screen Step 8. Audit logs screen Step 9. Settings screen CHAPTER 7. Project structure Step 1. Root files Step 2. smartnews_cost_api/ package Step 3. Static admin assets Step 4. Config examples Step 5. Scripts CHAPTER 8. Environment variables and deployment Step 1. Required production variables Step 2. Optional variables Step 3. Deployment smoke checks Step 4. Cloud Scheduler worker CHAPTER 9. Testing and troubleshooting Step 1. Test all-time API without tracker push Step 2. Test old date range mode Step 3. Test invalid partial range Step 4. Test ClickFlare auto-import Step 5. Common errors Step 6. Useful log query CHAPTER 10. Operational examples Step 1. Leadxpression client config Step 2. Clickgency client config Step 3. Patch one client account list from CLI Step 4. Store tracker token in Secret Manager CHAPTER 11. Mintegral client endpoints Step 1. Overview and isolation model Step 3. account/balance — refused Step 4. Get Campaign List Step 5. Retrieve Offer List Step 6. Create / Manage Offer Step 7. Update Offer Budget Step 8. Manage Bid Rate Step 9. Update Offer Promotion Status Step 10. Manage Publisher Targets Step 11. Update Audience Targets Step 12. Apply Creatives to Offer Step 13. Update Target Goals Step 14. Retrieve Creative Info Step 15. Create / Manage Creative Set Step 16. Retrieve Creative Set Data Step 17. Retrieve Creative Sources Step 18. Obtain Event Bid Goals Step 19. Audience Operations Step 20. Audience Upload (Presigned) Step 21. Update Tracking URL Step 22. Advanced Performance Reporting Step 23. AppsFlyer cost auto-import via InCost API Important Notes Summary

SmartNews and Mintegral Clients-API — Technical & User Documentation

Point2Web Technical Department

2026-04-28

SmartNews and Mintegral Clients-API — Technical & User Documentation

Overview

SmartNews Cost Import API is an internal service for collecting advertising costs from SmartNews, validating client-specific campaign ID extraction rules, aggregating costs by external tracker campaign ID, and delivering the result either through an API response or through an auto-import worker into supported trackers.

The service is designed for Cloud Run and Firestore-backed configuration. It supports multiple clients, separate SmartNews authentication profiles, API-key based client access, Google SSO for the admin interface, manager/admin roles, scheduled auto-imports, and audit logging.

Main supported workflows:

Manual SmartNews cost import through API.

All-time SmartNews cost import when dateFrom and dateTo are omitted.

RedTrack hourly/window-based cost push.

ClickFlare manual cost upload through CSV multipart request.

Client and auto-import configuration through the web interface.

Firestore-based audit trail for user and worker actions.

The current production context used during setup:

ParameterValue
GCP projectleads-cf
Cloud Run regioneurope-west1
Main servicesmartnews-cost-import-api
Firestore databasewebhooks
Configuration backendfirestore
Default app timezoneEurope/Kyiv or service env value
Manager/admin UI/admin

CHAPTER 1. High-level architecture

Step 1. Main data flow

The core import flow is:

A client, manager, admin, or worker triggers an import.

The service authenticates the caller.

The service loads the client document from Firestore.

The service resolves the SmartNews auth profile from Firestore and Secret Manager.

SmartNews insights are requested for the configured ad account IDs and date range.

Each SmartNews row is normalized into an internal cost record.

The configured extractor reads the external tracker campaign ID from landing_page_url.

Costs are aggregated by external tracker campaign ID.

The service returns JSON rows or pushes data into the configured tracker.

The normalized API response always uses the external tracker campaign ID, not the internal SmartNews campaign ID:

[
  {
    "campaign_id": "68e7c3f319b0b70012c4f1ac",
    "cost": 123.45
  }
]

Step 2. Runtime components

ComponentResponsibility
app_factory.pyCreates Flask app, exposes public import API and health endpoints.
admin_routes.pyExposes admin UI and admin API endpoints.
services.pyAuthenticates clients and coordinates cost import.
sources.pyReads data from Google Sheets or SmartNews API and normalizes records.
smartnews.pySmartNews OAuth, pagination, insights and campaign API client.
extractors.pyExtracts external campaign IDs from landing page URLs.
auto_import_worker.pyExecutes scheduled tracker pushes.
firestore.pyReads/writes Firestore clients, profiles, auto-imports, settings and audit logs.
secret_manager.pyResolves Secret Manager references.
models.pyDataclasses for configs, requests and normalized records.
utils.pyDate parsing, hashing, secure compare and helper functions.

Step 3. Supported source modes

Source modeDescriptionTypical use
smartnews-api-backedReads costs directly from SmartNews API insights.Current preferred mode for new clients.
sheet-backedReads an existing Google Sheet export and optionally writes result sheets.Legacy or client-specific flows.

For smartnews-api-backed, allowed_ad_account_ids is mandatory. If the API request does not specify ad_account_id, the service uses all IDs from allowed_ad_account_ids.

For sheet-backed, ad account discovery and sync are available from the admin UI because source rows are available in the configured Google Sheet.

CHAPTER 2. API endpoints

Step 1. Health check

MethodEndpointAuthPurpose
GET/healthzNoneChecks whether the service is running and returns basic runtime info.

Example:

curl "$SERVICE_URL/healthz"

Successful response:

{
  "ok": true,
  "service": "smartnews-cost-import-api",
  "config_backend": "firestore",
  "firestore_clients_collection": "smartnews_cost_import_clients",
  "firestore_database": "webhooks"
}

Step 2. Legacy sync endpoints

MethodEndpointAuthPurpose
GET or POST/Authorization: Bearer <HTTP_BEARER_TOKEN>Runs legacy sync.
GET or POST/legacy/sync-smartnews-costsAuthorization: Bearer <HTTP_BEARER_TOKEN>Runs legacy sync using explicit path.

These endpoints are kept for backward compatibility. New integrations should use /costs/{client_slug}/smartnews/data/import.

Step 3. SmartNews cost import endpoint

MethodEndpointAuth
POST/costs/<client_slug>/smartnews/data/importX-Api-Key: <client_api_key>

This endpoint imports SmartNews costs for one client and returns aggregated rows grouped by external tracker campaign ID.

Supported query parameters:

ParameterRequiredDescription
dateFromNoStart datetime. Must be sent together with dateTo.
dateToNoEnd datetime. Must be sent together with dateFrom.
allTimeSinceNoStart datetime for all-time mode. Used only when dateFrom and dateTo are both omitted. Default: 2020-01-01 00:00:00.
all_time_sinceNoAlternative snake_case name for allTimeSince.
timezoneNoRequest timezone. Example: America/New_York.
timeZoneNoAlternative camelCase name for timezone.
ad_account_idNoOptional whitelist-filtered ad account ID. Can be repeated. If omitted, all configured allowed accounts are used.
debugNoBoolean. If true, returns rows plus debug metadata, warnings and raw records.

Rules for dates:

If both dateFrom and dateTo are provided, the endpoint uses the explicit range.

If both are omitted, the endpoint enters all-time mode.

If only one of dateFrom or dateTo is provided, the endpoint returns 400.

If dateFrom > dateTo, the endpoint returns 400.

Explicit date range example:

curl -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?dateFrom=2026-04-28%2000:00:00&dateTo=2026-04-28%2023:59:59&timezone=America/New_York" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY" | jq .

All-time mode example:

curl -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?timezone=America/New_York" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY" | jq .

All-time mode with custom start date:

curl -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?timezone=America/New_York&allTimeSince=2026-04-01%2000:00:00" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY" | jq .

Expected normal response:

[
  {
    "campaign_id": "68e7c3f319b0b70012c4f1ac",
    "cost": 123.45
  }
]

Expected debug response shape:

{
  "rows": [
    {
      "campaign_id": "68e7c3f319b0b70012c4f1ac",
      "cost": 123.45
    }
  ],
  "debug": {
    "client_slug": "leadxpression",
    "source_mode": "smartnews-api-backed",
    "effective_ad_account_ids": ["110565805", "110707857"],
    "skipped_records": 0,
    "warnings": [],
    "raw_records": [],
    "smartnews_campaign_rows": []
  }
}

CHAPTER 3. Admin API endpoints

Step 1. Session and runtime endpoints

MethodEndpointAuthDescription
GET/adminBrowserReturns the admin UI.
GET/admin/api/login-bootstrapNoneReturns Google OAuth client ID and allowed domain.
POST/admin/api/session/loginGoogle ID token + access tokenCreates signed admin session.
GET/oauth/redirect/startNoneStarts PointBoard SSO (no access token needed).
GET/POST/auth/google-oauthGoogle ID tokenGoogle SSO: GET starts the flow, Google posts the ID token back (no access token needed).
GET/admin/api/session/meAdmin session bearerReturns current session.
GET/admin/api/runtime-infoAdmin session bearerReturns runtime Firestore and service info.

Admin session auth header:

Authorization: Bearer <admin_session_token>

The admin session token is the browser session token stored after successful Google sign-in and access token validation. It is not the same as ADMIN_BEARER_TOKEN.

Step 2. Client endpoints

MethodEndpointRoleDescription
GET/admin/api/clientsmanager/adminLists clients.
POST/admin/api/clientsmanager/adminCreates client. Generates client API token if not provided.
GET/admin/api/clients/<client_slug>manager/adminReads one client.
PUT/admin/api/clients/<client_slug>manager/adminUpdates one client.
POST/admin/api/clients/<client_slug>/regenerate-tokenadminRegenerates client API token.
DELETE/admin/api/clients/<client_slug>adminDeletes client.
GET/admin/api/clients/<client_slug>/discover-ad-accountsmanager/adminDiscovers account IDs from sheet-backed source.
POST/admin/api/clients/<client_slug>/sync-ad-accountsmanager/adminWrites discovered sheet account IDs to the client.

Important behavior:

Client API keys are stored only as SHA-256 hashes.

Plaintext client API key is shown only once on create/regenerate.

Manager role cannot delete clients or regenerate client tokens.

Manager role does not see or edit SmartNews auth profiles.

Step 3. Auth profile endpoints

MethodEndpointRoleDescription
GET/admin/api/auth-profilesadminLists SmartNews auth profiles.
POST/admin/api/auth-profilesadminCreates profile.
PUT/admin/api/auth-profiles/<name>adminUpdates profile.

Recommended profile storage uses Secret Manager references instead of plaintext credentials.

Step 4. Audit logs and templates

MethodEndpointRoleDescription
GET/admin/api/audit-logsmanager/adminReads audit logs. Supports filters.
GET/admin/api/tracker-templatesmanager/adminReturns tracker extractor templates.
GET/admin/api/auto-import-schemasmanager/adminReturns auto-import tracker presets.

Audit log filters:

Query parameterDescription
limitMaximum rows, capped at 500.
actionExact action filter. Example: auto_import_run.
client_slugExact client filter.
levelinfo or error.

Step 5. Auto-import endpoints

MethodEndpointRoleDescription
GET/admin/api/auto-importsmanager/adminLists auto-import presets.
POST/admin/api/auto-importsmanager/adminCreates preset.
GET/admin/api/auto-imports/<config_slug>manager/adminReads preset.
PUT/admin/api/auto-imports/<config_slug>manager/adminUpdates preset.
DELETE/admin/api/auto-imports/<config_slug>adminDeletes preset.
POST/admin/api/auto-imports/<config_slug>/run-nowmanager/adminRuns one preset immediately.
POST/admin/api/auto-imports/run-duemanager/adminRuns due presets from UI/admin API.
POST/admin/api/worker/auto-imports/run-dueworker tokenScheduler endpoint.

run-now is the safest manual test because it forces one config to execute immediately.

Worker endpoint auth header:

Authorization: Bearer <WORKER_BEARER_TOKEN>

Step 6. Access settings endpoints

MethodEndpointRoleDescription
GET/admin/api/settings/authadminReads admin/manager token status.
POST/admin/api/settings/auth/rotateadminRotates admin or manager access token.

Rotation request body:

{
  "role": "manager"
}

The new access token is returned only once and stored as a hash in Firestore settings.

CHAPTER 4. Firestore configuration

Step 1. Collections

The service uses these Firestore collections:

CollectionDefault namePurpose
Clientssmartnews_cost_import_clientsPer-client import configuration.
Auth profilessmartnews_auth_profilesSmartNews OAuth credentials or secret refs.
Auto-importssmartnews_cost_import_auto_importsScheduled tracker push presets.
Audit logssmartnews_cost_import_audit_logsOperational and admin audit trail.
Settingssmartnews_cost_import_settingsAccess-token hashes and global admin settings.

In the current setup, the Firestore database is webhooks.

Step 2. Client document structure

Document ID must equal client_slug.

Typical smartnews-api-backed ClickFlare client:

{
  "client_slug": "leadxpression",
  "display_name": "Leadxpression",
  "api_key_sha256": "sha256-of-client-api-key",
  "is_active": true,
  "platform": "smartnews",
  "source_mode": "smartnews-api-backed",
  "tracker_type": "clickflare",
  "allowed_ad_account_ids": ["110565805", "110707857"],
  "smartnews_auth_profile": "default",
  "strict_extraction": true,
  "allow_internal_id_fallback": false,
  "sheet_config": null,
  "extractor": {
    "kind": "path_segment",
    "query_param": null,
    "query_params": [],
    "regex": "^[A-Za-z0-9]{24}$",
    "path_segment_index": -1,
    "decode_passes": 3,
    "allow_path_fallback": false,
    "path_regex": "^[A-Za-z0-9]{24}$"
  }
}

Field descriptions:

FieldDescription
client_slugStable machine ID. Also used in endpoint path.
display_nameHuman-readable name in UI.
api_key_sha256SHA-256 hash of the client API key.
is_activeIf false, client API access is rejected.
platformUsually smartnews.
source_modesmartnews-api-backed or sheet-backed.
tracker_typeredtrack, clickflare, voluum, keitaro, binom, or custom name.
allowed_ad_account_idsWhitelist of SmartNews ad accounts. Store IDs as strings.
smartnews_auth_profileName of auth profile used for SmartNews OAuth.
strict_extractionIf true, records without extracted external ID are skipped.
allow_internal_id_fallbackShould normally be false. Prevents SmartNews internal IDs from being returned as tracker IDs.
sheet_configRequired only for sheet-backed clients.
extractorRules for extracting external campaign ID from landing page URL.

Step 3. Extractor configurations

ClickFlare URL format:

https://tracking-domain.example/cf/r/68e7c3f319b0b70012c4f1ac?click_id=...

Recommended ClickFlare extractor:

{
  "kind": "path_segment",
  "query_param": null,
  "query_params": [],
  "regex": "^[A-Za-z0-9]{24}$",
  "path_segment_index": -1,
  "decode_passes": 3,
  "allow_path_fallback": false,
  "path_regex": "^[A-Za-z0-9]{24}$"
}

RedTrack or mixed URL format:

https://domain.example/path?cmpid=69d788a779179f70ec9691e2 https://domain.example/path?rtkcmpid=69d788a779179f70ec9691e2 https://domain.example/cf/r/69d788a779179f70ec9691e2?click_id=...

Recommended mixed extractor:

{
  "kind": "query_param",
  "query_param": null,
  "query_params": ["cmpid", "rtkcmpid"],
  "regex": "^[A-Za-z0-9]{24}$",
  "path_segment_index": -1,
  "decode_passes": 3,
  "allow_path_fallback": true,
  "path_regex": "^[A-Za-z0-9]{24}$"
}

Why there are two regex fields:

regex validates values extracted from query parameters.

path_regex validates a path segment when path fallback is enabled.

Both can be identical because they validate the same 24-character ID in different extraction branches.

Step 4. Auth profile document

Document ID is the profile name, for example default.

Recommended version:

{
  "name": "default",
  "client_id_secret_ref": "smartnews-client-id",
  "client_secret_secret_ref": "smartnews-client-secret"
}

Plaintext version is supported but not preferred:

{
  "name": "default",
  "client_id": "plain-client-id",
  "client_secret": "plain-client-secret"
}

Secret refs can be short names or fully-qualified Secret Manager refs. Short name example:

leadxpression-clickflare-api-key

Fully-qualified example:

projects/leads-cf/secrets/leadxpression-clickflare-api-key/versions/latest

Step 5. Auto-import document structure

Document ID must equal config_slug.

RedTrack example:

{
  "config_slug": "redtrack-clickgency-hourly",
  "name": "Clickgency → RedTrack",
  "client_slug": "clickgency",
  "is_active": true,
  "tracker_schema": "redtrack",
  "tracker_name": "RedTrack",
  "tracker_api_key": "",
  "tracker_api_key_secret_ref": "clickgency-redtrack-api-key",
  "schedule": {
    "mode": "cron",
    "timezone": "America/New_York",
    "interval_minutes": 60,
    "run_times": ["00:15"],
    "cron_expression": "15 * * * *"
  },
  "request": {
    "endpoint": "https://api.redtrack.io/tracks/cost",
    "method": "POST",
    "query_fields": [
      {"name": "api_key", "required": true},
      {"name": "time_from", "required": true},
      {"name": "time_to", "required": true},
      {"name": "cost", "required": true},
      {"name": "campaign_id", "required": true}
    ]
  },
  "request_defaults": {
    "currency": "USD"
  }
}

ClickFlare manual cost example:

{
  "config_slug": "leadxpression-clickflare",
  "name": "Leadxpression → ClickFlare",
  "client_slug": "leadxpression",
  "is_active": true,
  "tracker_schema": "clickflare",
  "tracker_name": "ClickFlare",
  "tracker_api_key": "",
  "tracker_api_key_secret_ref": "leadxpression-clickflare-api-key",
  "schedule": {
    "mode": "cron",
    "timezone": "America/New_York",
    "interval_minutes": 60,
    "run_times": ["00:15"],
    "cron_expression": "15 * * * *"
  },
  "request": {
    "endpoint": "https://public-api.clickflare.io/api/campaigns/manual/cost",
    "method": "PATCH",
    "query_fields": [],
    "json_body": {}
  },
  "request_defaults": {
    "currency": "USD",
    "cost_mode": "all_time",
    "all_time_since": "2020-01-01 00:00:00",
    "timezone": "America/New_York",
    "file_field": "file",
    "filename": "smartnews_costs.csv"
  },
  "notes": "ClickFlare manual cost upload via CSV."
}

Important for ClickFlare:

The endpoint must be /api/campaigns/manual/cost.

Do not use /api/campaigns/{id} for cost import; that endpoint edits campaign settings.

The worker sends one multipart CSV file per run.

The CSV has columns campaign_id,cost.

api-key is sent as an HTTP header.

startDate, endDate, and timezone are sent as query parameters.

With cost_mode: all_time, the worker imports SmartNews costs from all_time_since to the scheduled run time.

Step 6. Worker state

Each auto-import document can contain worker_state.

Common fields:

FieldDescription
last_run_started_atUTC timestamp when worker started.
last_run_finished_atUTC timestamp when worker finished.
last_run_statusrunning, success, or error.
last_errorError text if run failed.
last_http_statusTracker response status, if a tracker request was sent.
last_response_excerptFirst 1000 characters from tracker response.
last_scheduled_forScheduled occurrence that was executed.
last_successful_scheduled_forLast occurrence completed successfully.
last_window_fromEffective import window start, UTC.
last_window_toEffective import window end, UTC.
last_rowsNumber of imported/aggregated rows.
last_requests_sentNumber of tracker requests sent.
next_due_atNext calculated due time, UTC.

If rows = 0 and requests_sent = 0, the worker did not push anything to tracker. This usually means SmartNews returned no cost rows or the extractor did not find external tracker campaign IDs.

CHAPTER 5. Auto-import worker behavior

Step 1. Scheduling logic

The worker supports these schedule modes:

ModeDescription
manualNo automatic due run. Only run-now works.
cronUses cron_expression. Example: 15 * * * *.
daily_timesUses run_times, e.g. ["09:00", "18:00"].
intervalRuns by interval buckets from midnight in schedule timezone.

Worker due calculation uses the schedule timezone, then stores state timestamps in UTC.

Step 2. RedTrack push behavior

RedTrack receives one HTTP request per cost row.

Request format:

POST https://api.redtrack.io/tracks/cost

Query params include:

api_key=<tracker_api_key> time_from=<UTC ISO time> time_to=<UTC ISO time> cost=<cost rounded to 0.01> campaign_id=<external campaign id> currency=USD

RedTrack is window-based, so hourly or scheduled windows are valid.

Step 3. ClickFlare manual cost behavior

ClickFlare receives one batch request per run.

Request format:

PATCH https://public-api.clickflare.io/api/campaigns/manual/cost

Headers:

Accept: application/json api-key: <tracker_api_key>

Query params:

startDate=YYYY-MM-DD HH:mm:ss endDate=YYYY-MM-DD HH:mm:ss timezone=America/New_York

Multipart file:

campaign_id,cost 68e7c3f319b0b70012c4f1ac,123.45

The worker filters invalid CSV rows before upload:

missing campaign_id is skipped;
campaign ID not 24 alphanumeric characters is skipped;
missing cost is skipped;
non-numeric cost is skipped;

cost less than or equal to 0 is skipped.

If no valid positive-cost rows remain, the run fails with a validation error.

Step 4. All-time cost mode

For ClickFlare, use all-time mode because the manual cost endpoint is designed around a reporting period and CSV upload.

Required defaults:

{
  "cost_mode": "all_time",
  "all_time_since": "2020-01-01 00:00:00",
  "timezone": "America/New_York"
}

When enabled, the worker changes the import window from normal scheduled window to:

all_time_since → scheduled_for

Example:

all_time_since: 2020-01-01 00:00:00 America/New_York scheduled_for: 2026-04-28T15:15:00Z

The worker imports SmartNews costs for that full range, aggregates by ClickFlare campaign ID, generates CSV, and uploads it to ClickFlare.

CHAPTER 6. Web interface documentation

Step 1. Login flow

The admin panel is available at:

/admin

Login requires two steps:

Google sign-in with an account from the allowed Google Workspace domain.

Admin or manager access token.

After successful login, the browser stores a signed session token in local storage. This token is used as Authorization: Bearer <session> for admin API calls.

Step 2. Roles

RoleCapabilities
managerView/edit clients, view/edit auto-imports, run imports, view logs.
adminAll manager capabilities plus auth profiles, deletes, token regeneration and settings.

Manager restrictions:

no Auth profiles screen;
no Settings screen;
no delete actions;
no client token regeneration;

no direct SmartNews auth profile editing.

Step 3. Overview screen

The Overview screen shows runtime status and shortcuts:

refresh all data;
open clients;
create new client;
open auto-imports;
run due auto-imports;
open audit logs;

reload runtime info.

Runtime badges show backend, database, number of clients, number of auto-imports, role and user.

Step 4. Clients screen

The Clients screen lists Firestore client configs.

Displayed fields:

client slug;
display name;
active/inactive status;
source mode;
tracker type;

number of allowed ad accounts.

Available actions:

refresh client list;
create new client;
open selected client in editor;
discover/sync ad accounts for sheet-backed clients;

regenerate client token, admin only.

Step 5. Client editor

Default manager-facing fields:

Client selector;
Client slug for new clients;
Display name for new clients;
Tracker dropdown;
Active checkbox;
Notes;

Allowed ad account IDs.

Advanced mode is available for admins. It exposes:

source mode;
SmartNews auth profile;
strict extraction;
internal ID fallback;
extractor configuration;
sheet config;

client token override.

Important behavior after the latest patch:

Existing advanced config is loaded into editor fields when selecting a client.

If advanced mode is not opened, saving does not overwrite the existing extractor or sheet config with defaults.

allowed_ad_account_ids are loaded and saved as strings.

path_segment_index = -1 is preserved.

Step 6. Auto-import screen

The Auto-import screen manages scheduled tracker imports.

Main fields:

client slug;
tracker preset;
tracker API key;
schedule picker;

active checkbox.

Tracker presets configure hidden technical fields such as endpoint, method, query fields and request defaults.

For ClickFlare, the preset should generate:

{
  "request": {
    "endpoint": "https://public-api.clickflare.io/api/campaigns/manual/cost",
    "method": "PATCH",
    "query_fields": [],
    "json_body": {}
  },
  "request_defaults": {
    "currency": "USD",
    "cost_mode": "all_time",
    "all_time_since": "2020-01-01 00:00:00",
    "timezone": "America/New_York",
    "file_field": "file",
    "filename": "smartnews_costs.csv"
  }
}

For RedTrack, the preset should generate:

{
  "request": {
    "endpoint": "https://api.redtrack.io/tracks/cost",
    "method": "POST"
  },
  "request_defaults": {
    "currency": "USD"
  }
}

Step 7. Auth profiles screen

This screen is admin-only.

It allows creating and updating SmartNews auth profiles. Prefer Secret Manager refs instead of plaintext credentials.

Required values:

either client_id or client_id_secret_ref;

either client_secret or client_secret_secret_ref.

Step 8. Audit logs screen

The logs screen shows service and admin actions.

Common actions:

ActionMeaning
cost_importDirect cost import API call.
auto_import_runWorker executed an auto-import.
auto_import_run_nowUser manually triggered an auto-import.
auto_import_run_dueAdmin API triggered due auto-imports.
client_createClient was created.
client_updateClient was updated.
client_token_regenerateClient token was regenerated.
auth_profile_createAuth profile was created.
auth_access_token_rotateAdmin or manager access token was rotated.

Use response_excerpt and last_error in audit details for tracker troubleshooting.

Step 9. Settings screen

This screen is admin-only.

Functions:

view whether admin and manager access tokens are configured;
rotate admin access token;

rotate manager access token.

New tokens are displayed only once.

CHAPTER 7. Project structure

Step 1. Root files

PathPurpose
app.pyCloud Run entrypoint. Imports and exposes Flask app.
ProcfileRuntime process command for Gunicorn.
requirements.txtPython dependencies.
README.mdProject summary and basic usage.
DEPLOY.mdCloud Run deployment guide.
openapi.yamlPublic OpenAPI subset for main API endpoints.
.gcloudignoreFiles ignored by Cloud Run source deployment.
admin.jsLegacy or auxiliary admin asset depending on packaging.

Step 2. smartnews_cost_api/ package

FilePurpose
__init__.pyPackage marker.
app_factory.pyCreates app, error handlers and public endpoints.
admin_routes.pyAdmin UI/API blueprint, auth, CRUD, worker triggers.
auto_import_worker.pyScheduler/worker execution logic.
config.pyRuntime env config and Firestore/env config loading.
errors.pyAPI error classes.
extractors.pyURL campaign ID extraction logic.
firestore.pyFirestore repositories for config and logs.
google_sheets.pyGspread factory for sheet-backed flows.
legacy_sync.pyLegacy sync implementation.
models.pyDataclasses used across the service.
secret_manager.pySecret Manager resolver with resolve() and resolve_secret() compatibility.
services.pyClient auth and cost import aggregation.
smartnews.pySmartNews OAuth and API client.
sources.pySheet-backed and SmartNews API-backed sources.
utils.pyParsing, hashing, formatting and helpers.

Step 3. Static admin assets

PathPurpose
smartnews_cost_api/static/admin/index.htmlSelf-contained web admin UI.

The current UI is mostly a single HTML file with embedded CSS and JavaScript.

Step 4. Config examples

PathPurpose
config/clients.example.jsonLocal/env client config example.
config/auth_profiles.example.jsonLocal/env auth profile example.
config/deploy.env.exampleDeployment env example.
config/firestore_clients.seed.example.jsonFirestore seed example for clients.
config/firestore_auth_profiles.seed.example.jsonFirestore seed example for auth profiles.

Step 5. Scripts

PathPurpose
scripts/seed_firestore.pySeeds Firestore clients and auth profiles.
scripts/run_auto_import_worker.pyCLI runner for due auto-imports.
scripts/deploy_example.shExample deployment script.

CHAPTER 8. Environment variables and deployment

Step 1. Required production variables

VariableDescription
CONFIG_BACKENDShould be firestore in production.
FIRESTORE_PROJECT_IDGCP project ID. Example: leads-cf.
FIRESTORE_DATABASEFirestore database. Example: webhooks.
FIRESTORE_CLIENTS_COLLECTIONClient collection name.
FIRESTORE_AUTH_PROFILES_COLLECTIONAuth profile collection name.
FIRESTORE_AUTO_IMPORTS_COLLECTIONAuto-import collection name.
FIRESTORE_AUDIT_LOGS_COLLECTIONAudit log collection name.
FIRESTORE_SETTINGS_COLLECTIONSettings collection name.
SECRET_MANAGER_PROJECT_IDProject used for Secret Manager refs.
ADMIN_SESSION_SECRETSecret used to sign admin browser sessions.
GOOGLE_OAUTH_CLIENT_IDGoogle OAuth client ID for admin login.
GOOGLE_ALLOWED_DOMAINAllowed Google Workspace domain.
WORKER_BEARER_TOKENToken used by Cloud Scheduler worker endpoint.

Step 2. Optional variables

VariableDefaultDescription
APP_TIMEZONEAmerica/New_York in current code defaultUsed when request/schedule timezone is not provided.
LOG_LEVELINFOPython logging level.
HTTP_BEARER_TOKENemptyLegacy sync bearer token.
ADMIN_BEARER_TOKENlegacy token fallbackInitial admin access token.
MANAGER_BEARER_TOKENemptyInitial manager access token.
ADMIN_SESSION_MAX_AGE_SECONDS43200Admin session TTL.
CONFIG_CACHE_TTL_SECONDS300AppConfig cache duration.
ALLOW_ENV_CONFIG_FALLBACKtrueAllows fallback to env/file config if Firestore loading fails. Production should use false.

Step 3. Deployment smoke checks

After deployment:

export SERVICE_URL="https://smartnews-cost-import-api-jvwl2xgpva-ew.a.run.app"

curl "$SERVICE_URL/healthz"

Check direct SmartNews import:

curl -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?timezone=America/New_York&allTimeSince=2026-04-01%2000:00:00" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY" | jq .

Check admin config:

curl -sS \
  "$SERVICE_URL/admin/api/auto-imports/leadxpression-clickflare" \
  -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" | jq .

Run one auto-import manually:

curl -sS -X POST \
  "$SERVICE_URL/admin/api/auto-imports/leadxpression-clickflare/run-now" \
  -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" | jq .

Step 4. Cloud Scheduler worker

Scheduler should call:

POST /admin/api/worker/auto-imports/run-due?limit=50

Required header:

Authorization: Bearer <WORKER_BEARER_TOKEN>

Example schedule:

gcloud scheduler jobs create http smartnews-auto-import-worker \
  --project leads-cf \
  --location europe-west1 \
  --schedule "*/5 * * * *" \
  --uri "$SERVICE_URL/admin/api/worker/auto-imports/run-due?limit=50" \
  --http-method POST \
  --headers "Authorization=Bearer $WORKER_BEARER_TOKEN"

CHAPTER 9. Testing and troubleshooting

Step 1. Test all-time API without tracker push

curl -i -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?timezone=America/New_York&allTimeSince=2026-04-01%2000:00:00" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY"

Expected:

HTTP 200;
JSON array or debug payload;

non-empty rows if SmartNews has spend and extractor works.

If the response is [], check:

the date range;
SmartNews ad account access;
allowed_ad_account_ids;
landing page URL format;
extractor config;

whether SmartNews has spend in the requested range.

Step 2. Test old date range mode

curl -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?dateFrom=2026-04-28%2000:00:00&dateTo=2026-04-28%2023:59:59&timezone=America/New_York" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY" | jq .

This verifies backward compatibility.

Step 3. Test invalid partial range

curl -i -sS -X POST \
  "$SERVICE_URL/costs/leadxpression/smartnews/data/import?dateFrom=2026-04-28%2000:00:00&timezone=America/New_York" \
  -H "X-Api-Key: $LEADXPRESSION_API_KEY"

Expected: HTTP 400 because only one of dateFrom and dateTo was provided.

Step 4. Test ClickFlare auto-import

curl -sS -X POST \
  "$SERVICE_URL/admin/api/auto-imports/leadxpression-clickflare/run-now" \
  -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" | jq .

Expected success shape:

{
  "ok": true,
  "result": {
    "config_slug": "leadxpression-clickflare",
    "client_slug": "leadxpression",
    "tracker_schema": "clickflare",
    "rows": 1,
    "requests_sent": 1,
    "success": true,
    "last_http_status": 200,
    "tracker_endpoint": "https://public-api.clickflare.io/api/campaigns/manual/cost"
  }
}

Step 5. Common errors

SymptomLikely causeFix
rows: 0, requests_sent: 0No SmartNews rows or extraction failed.Test direct SmartNews endpoint with debug=true.
SecretResolver object has no attribute resolve_secretOld resolver/worker mismatch.Use patched secret_manager.py with resolve_secret() alias or call resolve().
ClickFlare HTTP 400Wrong multipart field, CSV format, campaign has no visits, or invalid dates.Check response_excerpt, verify file_field, startDate, endDate, timezone.
request.endpoint is requiredAuto-import request config missing endpoint.Reapply tracker preset or patch Firestore config.
Client editor wipes extractorOld frontend version.Use patched index.html that preserves advanced config.
New SmartNews account returns []Account not accessible or no spend in range.Test account individually and confirm SmartNews auth profile access.
Requested ad_account_id values are not allowedRequest account is not whitelisted.Add account ID to allowed_ad_account_ids.
Admin API 401/403Wrong token type.Use browser session token for admin API, worker token only for worker endpoint.

Step 6. Useful log query

gcloud logging read \
'resource.type="cloud_run_revision"
 resource.labels.service_name="smartnews-cost-import-api"
 textPayload:"ClickFlare manual cost upload"' \
--project leads-cf \
--limit 20 \
--format json

CHAPTER 10. Operational examples

Step 1. Leadxpression client config

{
  "client_slug": "leadxpression",
  "display_name": "Leadxpression",
  "source_mode": "smartnews-api-backed",
  "tracker_type": "clickflare",
  "allowed_ad_account_ids": ["110565805", "110707857"],
  "smartnews_auth_profile": "default",
  "strict_extraction": true,
  "allow_internal_id_fallback": false,
  "extractor": {
    "kind": "path_segment",
    "query_params": [],
    "regex": "^[A-Za-z0-9]{24}$",
    "path_segment_index": -1,
    "decode_passes": 3,
    "allow_path_fallback": false,
    "path_regex": "^[A-Za-z0-9]{24}$"
  }
}

Step 2. Clickgency client config

{
  "client_slug": "clickgency",
  "display_name": "P2W&Smartnews / Clickgency / Account-01",
  "source_mode": "smartnews-api-backed",
  "tracker_type": "redtrack",
  "allowed_ad_account_ids": ["109138607", "110527074"],
  "smartnews_auth_profile": "default",
  "strict_extraction": true,
  "allow_internal_id_fallback": false,
  "extractor": {
    "kind": "query_param",
    "query_param": null,
    "query_params": ["cmpid", "rtkcmpid"],
    "regex": "^[A-Za-z0-9]{24}$",
    "path_segment_index": -1,
    "decode_passes": 3,
    "allow_path_fallback": true,
    "path_regex": "^[A-Za-z0-9]{24}$"
  }
}

Step 3. Patch one client account list from CLI

python - <<'PY'
from datetime import datetime, timezone
from google.cloud import firestore

PROJECT_ID = "leads-cf"
DATABASE = "webhooks"
CLIENT_SLUG = "clickgency"

accounts = ["109138607", "110527074"]

db = firestore.Client(project=PROJECT_ID, database=DATABASE)
db.collection("smartnews_cost_import_clients").document(CLIENT_SLUG).set({
    "allowed_ad_account_ids": accounts,
    "updated_at": datetime.now(timezone.utc),
}, merge=True)

print("patched", CLIENT_SLUG, accounts)
PY

Step 4. Store tracker token in Secret Manager

export PROJECT_ID="leads-cf"
export SECRET_NAME="leadxpression-clickflare-api-key"

printf "Paste tracker token: "
read -s TRACKER_TOKEN
echo

printf '%s' "$TRACKER_TOKEN" | gcloud secrets create "$SECRET_NAME" \
  --project "$PROJECT_ID" \
  --data-file=- 2>/dev/null || \
printf '%s' "$TRACKER_TOKEN" | gcloud secrets versions add "$SECRET_NAME" \
  --project "$PROJECT_ID" \
  --data-file=-

unset TRACKER_TOKEN

Do not keep tracker tokens in plaintext Firestore fields if Secret Manager is available. Use:

{
  "tracker_api_key": "",
  "tracker_api_key_secret_ref": "leadxpression-clickflare-api-key"
}

CHAPTER 11. Mintegral client endpoints

Step 1. Overview and isolation model

All Mintegral client traffic is served through a single host path:

Our pathMethodPurpose
/platforms/mintegral/{client_slug}/proxy/{path}ANYGeneric proxy to https://ss-api.mintegral.com/api/{path}, scoped to the client's allowed_campaign_ids.

The proxy requires the client's X-Api-Key header. It auto-injects the three Mintegral auth headers (access-key, token, timestamp) from the bound Mintegral auth profile — callers must not send them.

Mintegral's Open API has no native sub-account scoping: the agency token sees every sub-user under the agency, and the API exposes no user_id filter on any endpoint, no owner field on any response, and the parent account/balance only returns the parent itself. Sub-user-level API keys are not issued by Mintegral. Isolation therefore runs on our side via a per-client allowlist (allowed_campaign_ids).

The proxy classifies every Mintegral path into one of three categories defined in mintegral_proxy.ENDPOINT_RULES:

CategoryBehavior
per_campaignValidates caller-supplied campaign_id against the allowlist; if absent, injects the full allowlist (chunked in ≤50-id batches and merged). Defence-in-depth response filter drops items whose campaign_id is not in the allowlist.
referencePass-through — endpoint returns dictionaries / enums that are not campaign-scoped.
account_onlyRefused with 403 (would expose the agency's parent account).

Any path not listed in ENDPOINT_RULES returns 403 by default-deny.

Step 3. account/balance — refused

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/account/balance

Returns 403 with error_code: account_only_endpoint. Mintegral's account/balance returns the agency parent only (is_sub_user: 2), regardless of which sub-user a client is mapped to. Exposing it through a client-scoped proxy is meaningless. Mintegral source: helpcenter / account-balance.

Step 4. Get Campaign List

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/campaign

Category: per_campaign. If the caller does not supply campaign_id, the proxy injects the client's allowlist (chunked into ≤50-id batches; responses are concatenated into one). If the caller supplies campaign_id, every value must be in the allowlist or the request is 403'd with isolation_denied before any upstream call.

ParamTypeRequiredNotes
campaign_idstring (CSV) or intauto-injected if missingComma-separated; max 50 per upstream call. Validated against allowed_campaign_ids.
campaign_namestringnoFilter by name.
package_namestringnoFilter by app bundle.
offer_idintno
offer_name / offer_uuidstringno
pageintnoDefault 1.
limitintnoDefault 10, max 50.

Response — { code, msg, data: { page, limit, total, list[] } } where each list item carries campaign_id, campaign_name, is_coppa, promotion_type, alive_in_store, preview_url, product_name, package_name, description, icon, platform, category, app_size, min_version, maintain_by, status. Items whose campaign_id is not in the allowlist are dropped before the response is returned to the caller; data.total is recomputed accordingly. Mintegral source: api-retrieve-campaign-list.

Step 5. Retrieve Offer List

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/offers

Category: per_campaign. Returns offers under the client's allowed campaigns. Auto-injection and chunking apply.

ParamTypeNotes
campaign_idint / CSVRequired for isolation; auto-injected if omitted.
offer_idintOptional filter.
page / limitintDefault 1 / 10. Max limit 50.

Response — { code, msg, data: { page, limit, total, list[] } }. Each item is an offer object including campaign_id, offer_id, offer_name, promote_timezone, start_time, end_time, budget, bid_rate, geos, OS targets, status. Mintegral source: api-retrieve-offer-list.

Step 6. Create / Manage Offer

POST /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer    (create)
PUT  /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer    (update)

Category: per_campaign. The body must include campaign_id for create (POST). For update (PUT), the body uses offer_id; the proxy still validates that the bound campaign is in the allowlist by inspecting the body. Body must be JSON.

Common fields (POST):

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_namestringyes
promote_timezoneintyesUTC offset hours.
start_time / end_timeint (unix)yesPromotion window.
budget / bid_ratenumberyesDaily budget and bid.
geosarray<string>yesTwo-letter country codes.
osstringyesANDROID, IOS.
billing_typestringyesCPI, CPA, CPC, etc.

Response — { code: 200, msg: "success", data: { offer_id } } on success; { code, msg } on error. Mintegral sources: api-create-offer, api-manage-offer.

Step 7. Update Offer Budget

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/budget

Category: per_campaign. Body must be JSON. The proxy validates campaign_id against the allowlist and runs an offer-id pre-flight (Mintegral upstream ignores campaign_id when offer_id is present, so we resolve the union of offer_ids belonging to this client's campaigns and refuse if the body's offer_id isn't in that set).

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
daily_capnumberyes (or total_budget)Daily spend cap in campaign currency.
daily_cap_typestringnoBUDGET (default) or INSTALL.
total_budgetnumber or "OPEN"yes (or daily_cap)Lifetime cap; "OPEN" disables the lifetime cap.
total_budget_effective_timeYYYY-MM-DDnoDate the lifetime cap starts counting from.
total_budget_effective_timezoneintnoUTC offset hours for the effective time.
country_codestringnoALL or two-letter ISO. Per-geo budget rows when omitted apply to all geos.

Returns { code, msg, data }. Mintegral source: api-update-budget.

Step 8. Manage Bid Rate

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/bid_rate

Category: per_campaign. Body must be JSON. Both campaign_id validation and offer_id pre-flight apply (see Step 7).

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
bid_ratenumberyes (or bid_rate_by_location)Single bid value in campaign currency. Used when no per-geo bids are set.
bid_rate_by_locationarray<{ country_code, bid_rate }>yes (or bid_rate)Per-geo overrides. Country codes are ISO-2.

For OCPI/OCPA billing types, see Step 13 (target_goal) instead — Mintegral rejects raw bid_rate changes when an offer is on a target-goal billing type. Returns { code, msg }. Mintegral source: api-manage-bid.

Step 9. Update Offer Promotion Status

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/status

Category: per_campaign. Body JSON. Both campaign_id validation and offer_id pre-flight apply.

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
statusstring enumyesRUNNING, STOPPED, PAUSED. Other Mintegral states (CAMPAIGN_STOPPED, COLD_STARTUP_ONGOING) are read-only and cannot be set via this endpoint.

Returns { code, msg }. See enumeration values for the full state machine. Mintegral source: api-update-promotion-status.

Step 10. Manage Publisher Targets

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/target
PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/target

Category: per_campaign. GET returns the configured publisher allowlist/blocklist for an offer; PUT replaces it. PUT also runs the offer-id pre-flight.

Query params (GET):

ParamTypeRequiredNotes
campaign_idintyesValidated against allowlist.
offer_idintyesMust belong to the campaign.

Body fields (PUT):

FieldTypeRequiredNotes
campaign_idintyes
offer_idintyes
target_publishersarray<string>yesList of mtg_id publisher identifiers (sub-publishers).
target_typestringyesWHITELIST or BLACKLIST.

Mintegral source: api-manage-publishers.

Step 11. Update Audience Targets

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/audience

Category: per_campaign. Body JSON. campaign_id validation + offer_id pre-flight apply. Binds an audience (created via Step 19) to an offer.

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
audience_idintyesThe audience created in Step 19.
audience_target_typestringyesINCLUDE or EXCLUDE.

Mintegral source: api-update-audience-target.

Step 12. Apply Creatives to Offer

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/offer/apply_creative

Category: per_campaign. Body JSON. campaign_id validation + offer_id pre-flight apply. Attaches creative sets (Step 15) to an offer.

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
creative_set_idsarray<int>yesOne or more creative-set ids from Step 15.

Mintegral source: api-update-creative.

Step 13. Update Target Goals

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v3/offer/target_goal

Category: per_campaign. Body JSON. campaign_id validation + offer_id pre-flight apply. Sets the offer's KPI goal (e.g. Target-CPE, Target-CPA, Target-ROAS). Use Step 18 first to discover which goal modes are supported for the campaign's tracking setup.

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
bid_goalstringyesGoal mode (e.g. Target-CPE, Target-CPA, Target-ROAS). Must be one of the values returned by Step 18.
target_goalnumberyesNumeric KPI value in campaign currency.
target_goal_windowstringnoAttribution window — D0, D1, D3, D7, D14, D30.
target_mtg_eventarray<string>conditionalMintegral event names (e.g. ["Purchase"], ["Start Trial"]). Required for event-based goals.
target_original_eventstringconditionalTracker-side event name (e.g. af_purchase) when tracking_method is APPSFLYER/ADJUST.

Mintegral source: api-update-target-goal.

Step 14. Retrieve Creative Info

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/creative-ad/list

Category: per_campaign. Lists individual creatives currently bound under a campaign's offers. Auto-injection of campaign_id applies, but Mintegral additionally requires the caller to scope the query by ad ids.

ParamTypeRequiredNotes
ad_idsstring (CSV) or intyesComma-separated creative-ad ids to inspect. Mintegral returns The ad ids field is required. if omitted.
campaign_idstring (CSV) or intauto-injected if missingValidated against allowed_campaign_ids.
page / limitintnoDefault 1 / 10. Max limit 50.

Returns a paged list of creatives with creative_id, type (VIDEO, IMAGE, HTML, etc.), resource_url, md5, duration_ms (video only), width / height. Mintegral source: api-retrieve-creative-info.

Step 15. Create / Manage Creative Set

POST /platforms/mintegral/{client_slug}/proxy/api/open/v1/creative_set    (create)
PUT  /platforms/mintegral/{client_slug}/proxy/api/open/v1/creative_set    (update)

Category: per_campaign. Body JSON. Creative sets are reusable bundles of creatives (videos + endcards + icons + titles + CTAs) attached to offers via Step 12.

Body fields:

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
creative_set_idintPUT onlyExisting creative-set id when updating.
creative_set_namestringyes (POST)Free-form display name.
languagestringyes (POST)BCP-47 code (e.g. en, pt-BR).
creativesarray<{ source_id, ad_type }>yes (POST)Array of source bindings; source_id from Step 17, ad_type per enumeration values.
endcard_idintnoOptional endcard creative.
title / description / ctastringnoOptional ad copy overrides.

Returns { code, msg, data: { creative_set_id } } on POST. Mintegral source: api-create-creative-set.

Step 16. Retrieve Creative Set Data

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/creative_sets

Category: per_campaign. Paginated list of creative sets bound to the client's campaigns. Auto-injection of campaign_id applies.

ParamTypeRequiredNotes
campaign_idstring (CSV) or intauto-injected if missingValidated against allowed_campaign_ids.
creative_set_idintnoFilter to a specific creative set.
page / limitintnoDefault 1 / 10. Max limit 50.

Each entry exposes the embedded creatives with their source_id + dimensions + status. Mintegral source: api-retrieve-creative-set-data.

Step 17. Retrieve Creative Sources

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/creatives/source

⚠ Blocked for client proxy use. Category: account_only. The proxy returns 403 with error_code: account_only_endpoint for any client. Mintegral's source bucket is agency-wide and the response carries no campaign or sub-user attribution, so it cannot be safely scoped to one client. Admins can fetch source ids directly through Mintegral's UI; the path remains documented here because the value of source_id is referenced in Step 15.

Response shape (when fetched outside the proxy): paginated { source_id, type, md5, resource_url, width, height, duration_ms?, language?, file_size }. Mintegral source: api-retrieve-creative-list.

Step 18. Obtain Event Bid Goals

GET /platforms/mintegral/{client_slug}/proxy/api/open/v3/event/bid_goal_supports

Category: per_campaign. Returns the event taxonomy and supported bid-goal modes (e.g. ROAS, RETENTION, PURCHASE) for a given campaign. Used before Step 13 to discover which target_goal values are valid for the campaign's tracking setup.

ParamTypeRequiredNotes
bid_goalstringyesGoal mode to query (ROAS, RETENTION, …).
campaign_idintyes (or package_name)Validated against allowlist. Must be a single integer — see warning below.
package_namestringyes (or campaign_id)App bundle alternative when campaign_id is not known.

⚠ CSV not supported by Mintegral on this endpoint. Unlike open/v1/campaign or open/v1/offers, Mintegral validates campaign_id here as a single integer and rejects comma-separated lists with { code: 13000, msg: "campaign_id: The campaign id must be an integer." }. The proxy's auto-injection feature emits CSV when a client has multiple allowed_campaign_ids, so for multi-campaign clients the caller must pass campaign_id explicitly (one id per request, in the allowlist). Single-campaign clients work without an explicit param because the auto-injected single value is a valid integer.

Mintegral source: event-api.

Step 19. Audience Operations

GET  /platforms/mintegral/{client_slug}/proxy/api/open/v1/audience    (list)
POST /platforms/mintegral/{client_slug}/proxy/api/open/v1/audience    (create)
PUT  /platforms/mintegral/{client_slug}/proxy/api/open/v1/audience    (update)

Category: per_campaign. Manage custom audiences (device-id lists, look-alike seeds) attached to a campaign. Bind them to specific offers via Step 11.

Query params (GET — list):

ParamTypeRequiredNotes
campaign_idstring (CSV) or intauto-injected if missingValidated against allowed_campaign_ids.
audience_idintnoFilter to a specific audience.
page / limitintnoDefault 1 / 10.

Body fields (POST — create):

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
audience_namestringyesFree-form display name.
audience_typestringyesCUSTOM_LIST, LOOKALIKE, etc.
device_id_typestringyesIDFA, GAID, OAID.
refresh_periodintnoRefresh interval in days.
descriptionstringno

The actual device-id list is uploaded separately — Step 20 returns a presigned URL for direct upload to Mintegral storage. Mintegral source: audience-api.

Step 20. Audience Upload (Presigned)

GET /platforms/mintegral/{client_slug}/proxy/api/open/v1/audience/presigned-upload-data

⚠ Blocked for client proxy use. Category: account_only. The proxy returns 403 with error_code: account_only_endpoint for any client. Presigned URLs are issued at the agency level and have no campaign attribution, so the proxy cannot tie a returned URL to a specific client. Admins fetch presigned URLs out-of-band and upload the device-id list directly to the storage bucket; the resulting audience_id is then bound to the client's campaign via Step 19.

Mintegral source: audience-upload-api.

Step 21. Update Tracking URL

PUT /platforms/mintegral/{client_slug}/proxy/api/open/v1/tracking

Category: per_campaign. Body JSON. campaign_id validation + offer_id pre-flight apply. Updates impression / click / install postback URLs for an offer.

FieldTypeRequiredNotes
campaign_idintyesMust be in allowed_campaign_ids.
offer_idintyesMust belong to one of the client's allowed campaigns.
tracking_methodstringyesAPPSFLYER, ADJUST, BRANCH, KOCHAVA, SINGULAR, MMP, or NONE.
click_urlstring (URL)yesMintegral macros ({click_id}, {advertising_id}, …) supported. See tracking.
impression_urlstring (URL)no
install_postback_urlstring (URL)noPostback fired when MMP confirms install.
postback_event_urlstring (URL)noPostback fired for in-app events (use {event_name} macro).
universal_linkstring (URL)noiOS Universal Link / Android App Link override.
deep_linkstringnoCustom-scheme deep link.

Mintegral source: api-update-tracking.

Step 22. Advanced Performance Reporting

GET /platforms/mintegral/{client_slug}/proxy/api/v2/reports/data

Category: per_campaign. Two-step async report. The cost endpoint (Step 2) wraps this; raw-proxy access is still allowed if a caller wants other dimensions or columns. Note that Mintegral does not accept campaign_id on this endpoint, so the request itself is not validated and auto-injection is skipped — isolation is enforced by filtering the response payload after the TSV is downloaded (the proxy drops rows whose Campaign Id column is not in the allowlist).

All five fields below are required by Mintegral; calling the endpoint without them returns { code: 10000, msg: "error", data: { start_time: "…should not be empty.", end_time: "…should not be empty." } }.

ParamTypeRequiredNotes
typeintyes1 — generate / poll until ready, 2 — download.
start_timeYYYY-MM-DDyesWindow start (≤7 days span per request).
end_timeYYYY-MM-DDyesWindow end inclusive.
dimension_optionstringyesOne or more of Offer, Campaign, CampaignPackage, Creative, AdType, Sub, Package, Location, Endcard, AdOutputType — comma-separated.
time_granularitystringyeshourly, daily, weekly, monthly.
timezoneintyesUTC offset hours (e.g. +8, -5).

Example (poll until ready, then download):

curl -G 'https://temp-api.p2w.tech/platforms/mintegral/{client_slug}/proxy/api/v2/reports/data' \
  -H 'X-Api-Key: <client-token>' \
  --data-urlencode 'type=1' \
  --data-urlencode 'start_time=2026-04-25' \
  --data-urlencode 'end_time=2026-04-30' \
  --data-urlencode 'dimension_option=Campaign,Offer' \
  --data-urlencode 'time_granularity=daily' \
  --data-urlencode 'timezone=0'

Step 1 returns { code, msg } where code: 200 means the report is ready, 201/202 means still generating. Step 2 returns a TSV byte stream with columns Date, Timestamp, Offer Id, Offer Uuid, Offer Name, Campaign Id, Campaign Package, Creative Id, Creative Name, Ad Type, Sub Id, Package Name, Location, Endcard ID, Endcard Name, Ad Output Type, Currency, Impression, Click, Conversion, Ecpm, Cpc, Ctr, Cvr, Ivr, Spend. The proxy parses the TSV, drops rows whose Campaign Id is not in the allowlist, and re-serialises the result as JSON when X-Accept is JSON (default for a Flask client). Mintegral source: Advanced-ad-delivery-report-api.

Step 23. AppsFlyer cost auto-import via InCost API

What this is. For Mintegral clients whose MMP is AppsFlyer, the auto-import worker fetches Mintegral cost on a schedule and pushes it to AppsFlyer's InCost API — the partner-facing endpoint for ad networks to send cost data to advertiser accounts programmatically. This is the only auto-import tracker_schema currently supported for Mintegral clients. RedTrack/ClickFlare/Voluum/Keitaro/Binom are web trackers that cannot ingest Mintegral campaign IDs.

Premium gating. InCost API is part of AppsFlyer ROI360, an add-on subscription on the advertiser side. Even after our partner-account is set up, each individual advertiser must (a) hold ROI360, and (b) explicitly enable "Get Cost Data" for our network on each app they want cost data for. If either is missing, our pushes will fail at the per-app level with 404 / 415 / "media source not found" — see the troubleshooting section below.

Why we built this. The AppsFlyer Mintegral cost integration form (Dashboard → Integrated Partners → Mintegral → Cost) expects direct Mintegral credentials — username + API token — and routes traffic straight to ss-api.mintegral.com. It does not support a base URL, cannot be pointed at our proxy, and a single agency-token in that form would lump every sub-user under one AppsFlyer "partner" record, making per-client attribution impossible. This auto-import is the workaround: instead of letting AppsFlyer pull from Mintegral, we pull from Mintegral via our proxy (with proper sub-user scoping) and push directly to AppsFlyer's InCost API as a registered partner.

Architectural model: P2W intercepts Mintegral cost. Clients do not run their own Mintegral cost integration. Instead, every client's AppsFlyer account lists P2W (Point2Web) as an Active Integration with a Cost tab, and their attribution links carry pid=point2web_int (visible in Active Integrations → Point2Web → Attribution link). All cost flows through us:

Mintegral Reporting API
        ↓ (we pull, with per-client sub-user scoping)
P2W cost-import service
        ↓ (we re-emit via InCost API as media_source=point2web_int)
AppsFlyer (per-client account)
        ↓
client dashboards: cost attributed to "Point2Web" media source

Consequences:

  • Cost lands under point2web_int, not mintegral_int, because that's the pid on the client's attribution links. Sending under mintegral_int would result in matched_records_percentage=0 — cost rows would not match any clicks.
  • Clients should NOT run Mintegral as a separate cost-partner in their AppsFlyer dashboard. Doing so would double-count cost (Mintegral pulling on its own AND P2W pushing). If a client previously had Mintegral cost integration enabled, disable the Cost toggle on their Mintegral integrated-partner page before activating P2W. Other Mintegral integration tabs (Integration, Attribution link, Permissions, in-app event postbacks) must stay enabled — Mintegral still needs postbacks to optimise campaigns; only the Cost tab is conflict.
  • Upstream source visibility: the AppsFlyer profile carries an optional channel_label field. When set (e.g. to mintegral), every cost row sent for that profile carries channel: "mintegral" in the JSON. AppsFlyer surfaces this as a sub-dimension under media_source in client dashboards — so a client's spend report shows Point2Web cost broken down into mintegral / tiktok / etc. without changing the matched pid. Empty channel_label → field omitted.
  • One global P2W partner credential covers every client. The InCost API token authenticates our partner account; advertisers grant per-app cost permission to P2W (not to Mintegral) in their AppsFlyer dashboard.
23.1. Initial setup (one-time, partner side)

Per the AppsFlyer partner guide, the InCost integration requires a seven-step onboarding process. P2W must complete steps 1–6 once before any production push will succeed:

  1. Apply for InCost. In AppsFlyer dashboard → Help → Contact our team → Partner assistant → "Enabling cost measurement". This opens a ticket and an AppsFlyer Partner Solution Engineer is assigned.
  2. Verify campaign hierarchies. Confirm that >90% of P2W's Mintegral attribution traffic carries af_c_id (campaign_id) on the click URL. Optionally also af_adset_id / af_ad_id for offer/creative-level cost. Mintegral's standard click templates already include af_c_id; we just need to verify volume.
  3. Get the API token. AppsFlyer dashboard → Account → Security Center → API Token V2.0. This is the token issued for our P2W partner account, NOT for any individual advertiser. Save it in Settings → AppsFlyer profiles (see 23.2).
  4. Implement the three API methods. All three are already implemented in this codebase:
    • GET /api/mng/apps?capabilities=cost — list apps the advertiser has authorised for our cost capability. Each item carries currency and time_zone.
    • POST /api/incost-uploader/v1/data/app/{app_id} — upload cost JSON. Returns a job_id.
    • GET /api/incost-jobstatus/v1/data/app/{app_id}/job/{job_id} — poll job status. Look for status="Applied" and matched_records_percentage.
  5. Test the integration. Tell AppsFlyer in the support ticket that the implementation is complete; they grant permission on two sandbox app IDs:
    • com.cost.app (Android sandbox)
    • id888123456 (iOS sandbox)
    Then in AppsFlyer dashboard → Collaborate → Partner Marketplace → P2W → Set up integration → choose a sandbox app → Cost tab → toggle "Get Cost Data" on → Save → Test Connection. Use the admin "Run now" button on a draft auto-import bound to a fake client whose appsflyer_app_id_mappings point to the sandbox apps.
  6. Confirm operational with AppsFlyer. Reply in the same ticket. AppsFlyer flips a flag that activates production cost ingestion for our partner account.
  7. Per-advertiser activation. Each P2W client (advertiser) must independently enable "Get Cost Data" for our network in their AppsFlyer dashboard → Integrated Partners → P2W → Cost tab. Without this, /api/mng/apps?capabilities=cost returns the app as not authorised, and our push fails with 404 for that app. This is per-advertiser, per-app; if a client adds a new app, they must repeat the toggle.
23.2. AppsFlyer partner profile (Settings)

In Settings → AppsFlyer profiles, create one profile per partner credential set (typically just default):

FieldValue
nameProfile identifier. default for most setups; create additional profiles only if AppsFlyer provisioned multiple media_source IDs for our partner account, or to keep separate sandbox/production tokens.
api_tokenThe V2.0 API token from AppsFlyer dashboard → Account → Security Center. Sent as Authorization: Bearer <token> on every InCost call. Stored encrypted in Secret Manager via api_token_secret_ref.
media_sourceNetwork identifier on advertisers' AppsFlyer attribution links — the value of the pid query parameter. For P2W this is point2web_int; you can verify by inspecting any client's Click attribution link in AppsFlyer dashboard → Active Integrations → Point2Web → Attribution link tab. Wrong value → InCost returns 415 "media source not found", or returns 200 but with matched_records_percentage=0 (cost doesn't link to any clicks).
channel_labelOptional. Sent as the InCost channel field on every cost row (≤20 chars). Surfaces in client AppsFlyer dashboards as a sub-dimension under media_source: lets clients distinguish upstream sources within our partner cost (e.g. mintegral, tiktok, etc.) without changing the matched pid. Leave blank to omit the field entirely.

⚠️ API V2 token revocation event. AppsFlyer revoked all API V2 tokens generated before March 10, 2026, 19:00 UTC. If we have a token from before this date, regenerate it before any auto-import runs.

23.3. Auto-import preset shape
FieldValue
tracker_schemaappsflyer
appsflyer_partner_profileName of the AppsFlyer profile from 23.2 (default: default). The worker resolves api_token + media_source from this profile at push time.
tracker_api_keyIgnored for AppsFlyer schema. The Tracker API key field is hidden in the editor when AppsFlyer is selected.
request.endpointhttps://hq1.appsflyer.com/api/incost-uploader/v1/data/app/{app_id} — must contain the literal {app_id} placeholder. The worker substitutes it per row from appsflyer_app_id_mappings.
request.methodPOST
request.defaults.currencyUSD fallback when neither the Mintegral row nor the app's metadata supplies one.
request.defaults.job_status_poll_delay_seconds60 — wait between POST and first job-status poll.
request.defaults.job_status_poll_max_attempts3 — give up after this many polls. matched_records_percentage is recorded on the last successful poll.
request.defaults.retroactive_days7 — each run includes the last 7 calendar days of cost. Per AppsFlyer guidance, single-day runs miss late corrections from Mintegral.
schedule.run_timesDefault: 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 UTC — six runs per day, the AppsFlyer-recommended frequency for data freshness.
23.4. Per-client app mappings

Each Mintegral client must have appsflyer_app_id_mappings populated in the client editor: a list of {campaign_id, app_id, derived_from?} entries mapping each allowed Mintegral campaign to the AppsFlyer app it promotes. One Mintegral sub-account can promote several apps; the worker routes each cost row to the right AppsFlyer endpoint based on this mapping.

Platformapp_id formatExample
iOSid<App-Store-numeric-id>id6759177171
AndroidApp package namecom.example.myapp

Two ways to populate:

  1. Fetch from Mintegral (recommended): backend hits POST /admin/api/clients/{slug}/derive-appsflyer-app-ids which pulls the agency-wide campaign list and walks each allowed_campaign_ids:
    • Android: uses package_name directly (e.g. com.example.myapp).
    • iOS: regex /id<digits>/ against preview_url first; falls back to the campaign's first offer's click_url (the AppsFlyer click attribution URL like https://app.appsflyer.com/id6759177171?...).
    Optional cross-check: if you pick an AppsFlyer profile in the dropdown next to the Fetch button, the backend also calls /api/mng/apps?capabilities=cost and tags each derived mapping with one of:
    • 🟢 OK — derived correctly AND advertiser has authorised cost capability for this app on our network
    • 🟡 Warning — derived correctly but app is NOT in the AppsFlyer cost-allowlist; advertiser must enable "Get Cost Data" before pushes succeed
    • 🔴 Error — app_id derive failed (no id<digits> pattern, missing package_name, etc.)
  2. Configure mappings manually: opens a modal where each row carries campaign_id and app_id as plain inputs. Mix of derived + manual rows is supported; the source column shows which is which.
23.5. JSON body shape

Sent as Content-Type: application/json, one POST per distinct app_id bucket. The body is a JSON array of row objects:

[
  {
    "date": "2026-04-25",
    "app_id": "id6759177171",
    "media_source": "point2web_int",
    "campaign_id": "170737",
    "campaign_name": "VP_MINT_PORTUGAL_CPS_11.04",
    "currency": "USD",
    "spend": "12.5",
    "adset_id": "532021",
    "adset_name": "Offer A",
    "ad_id": "9001",
    "ad_name": "Creative X",
    "geo": "US",
    "channel": "mintegral"
  },
  ...
]

Field-hygiene rules (per AppsFlyer guidance):

  • Mandatory: date, app_id, media_source, campaign_id (≤24 chars), campaign_name (≤100 chars), currency (ISO 4217 3-letter), spend.
  • Optional, paired: adset_id+adset_name, ad_id+ad_name — sent only when Mintegral surfaced offer/creative metadata via dimension_option=Campaign,Offer,Creative. Empty values are omitted from the JSON, never sent as "".
  • Optional: geo (ISO 3166 2-letter), site_id, channel (≤20 chars; populated from the AppsFlyer profile's channel_label when set — used to surface upstream source like "mintegral" as a sub-dimension under media_source in client dashboards), keywords, af_prt (agency name).
  • spend format: decimal number, up to 5 digits after the decimal point. No thousand separators. No quotation marks around the value (we send it as a JSON number-string, which AppsFlyer parses as decimal). Negative values rejected outright; zero is allowed but skipped by the worker to keep payloads compact. Example values: 1, 1.2, 1234.20, 0.05123.
23.6. Async upload cycle

InCost API is asynchronous. The POST returns immediately with a job_id:

{ "job_id": "abc-123-def" }

The data is NOT committed at this point. The worker waits job_status_poll_delay_seconds (default 60s), then polls GET /api/incost-jobstatus/v1/data/app/{app_id}/job/{job_id} up to job_status_poll_max_attempts times (default 3). Possible status values:

StatusMeaningWorker action
InProgressStill processingWait and re-poll
AppliedData committed, available in advertiser's reportsStop polling. Record matched_records_percentage in audit row.
Failed / Rejected / ErrorValidation or processing errorStop polling. Record error field in audit row.
RevertedEarlier upload was overwritten by anotherInformational; not raised as error.

matched_records_percentage below 100% indicates that some cost rows could not be matched to attribution data on the advertiser's side. Common causes: campaign/adset/ad IDs in our cost data don't match the IDs on the actual click URLs. Investigate the Mintegral click-URL templates if this drops below 90% consistently.

If polling exhausts before Applied, the worker logs a warning and moves on. The next scheduled run re-uploads the same window (retroactive_days=7), so an incomplete poll never loses data.

23.7. Multi-app dispatch

When a client's mappings span several app_id values, the worker emits one POST per app_id. Each POST is independent; failure of one does NOT roll back earlier successes (AppsFlyer commits per-app, not per-run). The audit row records every chunk in app_uploads[] with {app_id, rows, job_id, status, matched_records_percentage, applied} so partial failures are visible.

App-list pre-flight: before any upload, the worker calls GET /api/mng/apps?capabilities=cost once and skips apps that aren't in the response. Skipped apps are recorded in safe_payload.skipped_apps[] with the reason. This avoids burning rate-limit slots on apps we know will return 404.

23.8. Rate limits & data window
LimitValue
API calls per minute150 per partner-account token
API calls per day1000 per partner-account token
Body size per upload1 MB
Earliest date allowed90 days before current date
Latest date allowedCurrent date (no future dates)
Recommended frequency≥6 sends per day for freshness
Recommended retroactive windowLast 7 days every run, for completion

Default schedule (6 runs/day × ≤ ~30 apps × 1 upload + 3 polls each) = ~720 calls/day, well under the daily limit. If we grow beyond this, the worker would need explicit pacing.

23.9. Skipped rows (audited, not sent)
  • Empty campaign_id or report_date
  • Non-numeric or negative cost
  • cost == 0 (allowed by spec but skipped to compact payloads)
  • campaign_id not in appsflyer_app_id_mappings → counted in unmapped_campaigns
  • App not in AppsFlyer cost-allowlist → entire app bucket recorded in skipped_apps[]
23.10. Troubleshooting
SymptomLikely causeFix
HTTP 401 on every callAPI token revoked (March 10 2026 mass-revocation) or expiredRegenerate the V2.0 token in AppsFlyer dashboard, update the AppsFlyer profile.
HTTP 415 "media source not found"The media_source in the AppsFlyer profile is not registered to our partner accountConfirm the value with AppsFlyer's partner-development manager. For P2W it is point2web_int — same as the pid on every client's attribution link.
HTTP 200 but matched_records_percentage stays at 0media_source mismatch with attribution-link pidCheck a client's AppsFlyer attribution link (Active Integrations → Point2Web → Attribution link). The pid query parameter is the canonical media_source. Update the AppsFlyer profile and re-run.
HTTP 404 on a specific appAdvertiser hasn't enabled "Get Cost Data" for our network on that appHave the advertiser toggle Get Cost Data in their AppsFlyer dashboard → Integrated Partners → P2W → Cost tab.
HTTP 422 "incorrect JSON format"Required field missing or wrong typeCheck the audit row's response_excerpt for the specific field. Common: empty campaign_name or spend sent as a non-numeric string.
Job stays InProgress through all pollsAppsFlyer is processing under heavy loadNot a worker bug. Next scheduled run re-uploads (retroactive 7-day window) and supersedes the in-flight job.
matched_records_percentage consistently < 90%Cost data IDs don't match attribution-link IDsVerify Mintegral click-URL templates carry af_c_id, af_adset_id, af_ad_id matching the IDs reported in cost data.

References:

Important Notes

Do not use ClickFlare /api/campaigns/{id} for cost import. It edits campaign configuration and cost model settings.

Use ClickFlare /api/campaigns/manual/cost for manual cost upload. The current worker sends CSV through multipart upload.

Store IDs as strings in Firestore. This avoids precision and formatting problems.

Keep allow_internal_id_fallback false. Returning internal SmartNews IDs as tracker IDs can corrupt tracker cost mapping.

For ClickFlare, prefer all-time mode. The worker uses all_time_since → scheduled_for to generate the manual cost CSV.

For RedTrack, use scheduled window mode. RedTrack /tracks/cost accepts time-windowed cost updates.

Do not paste real tokens into documentation, commits, screenshots, or Slack. Use Secret Manager references.

If rows = 0, the tracker was not called. Debug SmartNews data and extractor first.

After frontend patches, hard refresh the admin UI. Use Cmd + Shift + R to clear cached JS/HTML.

Summary

SmartNews Cost Import API centralizes SmartNews spend collection, client-specific campaign ID extraction, Firestore-based configuration, and tracker delivery. The public import endpoint supports both explicit date ranges and all-time imports. The admin panel provides controlled access for managers and admins, while Firestore acts as the source of truth for clients, auth profiles, auto-imports, audit logs and settings.

Current tracker behavior: RedTrack uses one request per campaign cost row and works with scheduled time windows. ClickFlare uses the manual cost endpoint with CSV multipart upload and should be configured with cost_mode: all_time.

Operational rule: when adding or editing clients, always verify allowed_ad_account_ids, extractor rules, and tracker-specific auto-import config before activating scheduled runs.

Access settings
Rotate admin and manager access tokens.
No settings loaded yet.
Current session
Google identity and admin role.
No session data.
Roles
Who may use this panel and at what level. Assign a role per email address.
viewer reads clients, imports and logs only. manager adds create/edit and running imports. admin adds deletes, token rotation, auth profiles and this Roles screen. Any signed-in @point2web.com account with no explicit role below defaults to manager. Break-glass admins are set from the environment and cannot be changed here.
Current roles
Break-glass (env) rows are locked. Override rows can be changed or cleared.
EmailRoleSourceUpdated byWhen
Not loaded yet.
Audit logs
Admin and import events.
Results
0 rows.
TimeActionClientLevelStatus
Log details
Selected payload.
Select a log row to inspect it.
No log selected.