# CreatorAPI × Instructify Ads: click and revenue tracking

Updated: 8 October 2026.
Status: implemented for OnlyFans. The three /api/smart-links endpoints are first-party
stored reports, not native proxies; the separate legacy campaign reports read native sources.
[Human-readable reference and examples](/docs/reference/onlyfans#instructify-ad-tracking).
Specification: [Instructify requirements, 8 October 2026](https://docs.google.com/document/d/1NxGhNA-uoOxDrz0EGgzGkqIjJu3qNlwXYnvxnF4KjBc/edit).
The public API is additive; existing /v1/{account}/smart-links response shapes remain available.

## Authentication and paths

Base URL: https://creator-api.com (https://api.creator-api.com also supported).
Use X-API-Key: YOUR_KEY, or Authorization: Bearer YOUR_KEY.
A readonly key is sufficient. The key must have access to the active creator and its platform.
Never send the key in a click URL.

- GET /api/smart-links?account_ids=on_YOUR_ACCOUNT&limit=100&offset=0
- GET /api/smart-links/{id}/clicks?date_start=2026-10-01T00:00:00Z&date_end=2026-10-08T23:59:59Z&limit=100&offset=0
- GET /api/smart-links/{id}/conversions?conversion_type=new_transaction&limit=100&offset=0

All three return a data envelope. The list has data:[...]; reports have
data:{summary:{...},rows:[...],filters:{...}}.
If the integration appends /smart-links to a configurable base URL, use
https://api.creator-api.com/api for these reports, not /v1/{account}/native.
Use the existing root base URL for /v1 account, creation and native-history operations.
The legacy /v1/{account}/smart-links reports remain distinct; do not assume their
envelopes or date-filter types are interchangeable with the three /api reports.
Limits: 1–100; offset >=0. Reports accept ISO 8601 timestamps with a timezone, inclusive boundaries.
Rows sort newest first, then by stable ID. Equal timestamps do not produce unstable pagination.
HTTP 401/403 protect identity, account scope, platform entitlement and inactive creators.
HTTP 422 means invalid filters. HTTP 429 includes Retry-After in seconds.
Marketing pages keep their existing canonical-host redirects.

## Create and advertise a link

Creating a link requires a full key, not the read-only reporting key:

```bash
curl -X POST "https://api.creator-api.com/v1/{account}/smart-links" \
  -H "X-API-Key: $CREATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Instructify | Meta Ads","link_type":"tracking_link"}'
```

Replace {account} with the connected CreatorAPI account ID, not a username.

Creation provisions real OnlyFans tracking-link inventory. It is a provider write.
Use the returned traffic_redirect_url (also exposed as url by the new list endpoint).
For link-preview redirects, disable optional bot cloaking:
```http
PATCH /v1/{account}/smart-links/{id}
X-API-Key: YOUR_FULL_KEY
Content-Type: application/json

{"cloaking_enabled":false}
```

Ad destination example:

```text
https://creator-api.com/go/{id}?utm_source=meta&utm_medium=paid&utm_campaign={{campaign.id}}&utm_content={{ad.id}}&utm_term={{adset.id}}&ecid={{ad.id}}
```

Meta supplies fbclid. utm_* and click-ID values are preserved after ordinary URL decoding;
they are not silently truncated to 500 characters. Recognized extras include gclid and ttclid.
Unknown parameters are ignored, and an empty query still redirects and records a visit.
The redirect performs no synchronous provider request. Inventory replenishment stays in
the existing background path. Database availability and the public network still affect latency.
Creation is a real mutation. If its outcome is unclear, reconcile the returned ID or exact
unique name before considering another creation request; never blindly repeat it.
Use the returned /go/ URL, not the native campaign URL or the unrelated /l/ short-link service.

## Reporting examples

These GETs use a readonly key. Replace {account} and {id}; the key stays server-side.

```bash
curl --get "https://api.creator-api.com/api/smart-links" \
  -H "X-API-Key: $CREATOR_API_KEY" \
  --data-urlencode "account_ids={account}" \
  --data-urlencode "limit=100" --data-urlencode "offset=0"

curl --get "https://api.creator-api.com/api/smart-links/{id}/clicks" \
  -H "X-API-Key: $CREATOR_API_KEY" \
  --data-urlencode "date_start=2026-10-01T00:00:00Z" \
  --data-urlencode "date_end=2026-10-08T23:59:59Z" \
  --data-urlencode "limit=100" --data-urlencode "offset=0"

curl --get "https://api.creator-api.com/api/smart-links/{id}/conversions" \
  -H "X-API-Key: $CREATOR_API_KEY" \
  --data-urlencode "conversion_type=new_transaction" \
  --data-urlencode "date_start=2026-10-01T00:00:00Z" \
  --data-urlencode "date_end=2026-10-08T23:59:59Z" \
  --data-urlencode "limit=100" --data-urlencode "offset=0"
```

Advance offset by the number of returned rows until the page is shorter than limit.
Summary counts describe the full filter, not only the page. Separate HTTP pages are not
a frozen first-party snapshot while new events arrive; overlap future polls and upsert IDs.
For initial subscriptions use conversion_type=new_subscriber, or omit it for both types.
Do not add initial-payment and subscription rows from different legacy report formats.

Each click object has all these fields:

```json
{
  "id": "clk_example",
  "created_at": "2026-10-02T18:04:11Z",
  "utm_source": "meta",
  "utm_medium": "paid",
  "utm_campaign": "example_campaign",
  "utm_content": "example_ad",
  "utm_term": "example_adset",
  "external_click_id": "example_ad",
  "fbclid": "example_fbclid",
  "gclid": null,
  "ttclid": null,
  "is_bot": false,
  "is_duplicate": false,
  "country_code": "US",
  "browser_device_type": "mobile",
  "referrer": "https://l.facebook.com/"
}
```

Illustrative values only, not a live test record. The same complete object is returned in
a matched conversion's click field. Missing URL parameters remain null.

## Clicks and attribution

Each request has its own stable click ID and UTC created_at, including repeats.
Rows include referrer, lowercase mobile/desktop/tablet, country_code, is_bot, is_duplicate,
external_click_id (ecid), fbclid, gclid, ttclid and the five utm fields.
Bot detection is heuristic. Duplicate detection uses the first-party visitor cookie,
falling back to IP plus user agent, within 30 minutes. Shared IP/browser combinations
can produce false positives; these are excluded rather than advertised as certain matches.

A subscription claims at most one unused, non-bot, non-duplicate click from the same
native offer, no later than the subscription and no more than seven days earlier.
A click is never assigned to two subscribers. Matching is last-eligible-click within the
offer, including when inventory is shared under load. A pooled native URL can be shared
or revisited; this is attribution, not cryptographic proof of a visitor's identity.
Native claimer previews can omit subscription data. The poller resolves those through
GET /users/{fan_id}, verifies the returned identity, and uses subscribedOnData
(the fan-to-creator subscription). It never treats the fan's public subscribePrice
or the reverse subscribedByData as acquisition evidence. Profile reads share the
bounded durable catch-up budget. A price from a different subscription timestamp
is not substituted. Missing timestamp evidence stops acknowledgement for retry.
Unmatched subscriptions with known amounts remain visible with click:null.
Unknown amounts remain pending as described below.
After acquisition, subsequent payments retain that acquisition click with no lifetime cutoff.

## Payments, revenue and polling

conversion_type is new_subscriber or new_transaction. The latter covers subsequent
subscription/renewal, message/PPV, tip, post, stream and signed reversal records.
IDs remain stable across repeated reads. Upsert by ID rather than appending blindly.
The existing poller normally runs every five minutes; failures or catch-up can delay data.
Payments seen before fan discovery are retained in a seven-day durable retry inbox.

Initial paid subscriptions are combined with one unambiguous matching subscription payment
(same fan/link, observed gross price, within 300 seconds after the subscription).
Only a fully collected, validated native payment batch may establish that binding.
The confirmed binding is persisted atomically in existing conversion columns; a later payment
does not undo or replace it. The original payment ledger is not edited. This is a bounded
same-fan/price/time match, not a claimed native cross-reference.

A paid acquisition uses the payment's stable conversion ID. The same payment therefore keeps
one public ID before/after financial enrichment and is never returned twice in the same report.
Free acquisitions retain their subscription conversion IDs. Upsert globally by conversion ID.
Revenue totals use confirmed ledger evidence only, including negative chargebacks.
Historical baseline imports financial evidence without historical pixel/postback delivery.
Native chargebacks are paginated and use stable negative conversion IDs.

Every PUBLISHED row has numeric USD amount_gross and amount_net, never null or a guessed fee:
- Known free subscription/trial: both 0.
- Confirmed payment/paid acquisition: exact numeric gross and net from the native ledger.
- Paid/unknown subscription without an unambiguous payment: retained internally, not published
  as a monetary row until known. summary.pending_subscribers_total (and the list's
  pending_subscribers_count) exposes this delay. Missing historical source evidence can leave
  it pending; it is not silently labeled free. The legacy /v1 raw report retains pending data.

Published summary counts correspond to the published rows, independent of page size.
Counts and rows use one read snapshot. Ties sort by the public ID, newest first.
Poll with overlapping dates (at least seven days), upsert existing IDs, and periodically
reconcile totals; native events and payment confirmation can arrive after conversion_at.

## Existing native tracking links and purchases

No new purchase is needed to verify the financial read path. Existing native campaigns,
their actual claimers, and existing paid transactions can be joined directly:

- GET /v1/{account}/tracking-links/{campaign_id}/transactions?date_start=2026-09-01T00:00:00Z&date_end=2026-10-01T00:00:00Z
- GET /v1/{account}/tracking-links/{campaign_id}/spenders?date_start=2026-09-01T00:00:00Z&date_end=2026-10-01T00:00:00Z

These are explicit read-only analyses, not aliases for the claimer list. A readonly key
suffices, within the existing active-account, platform and email gates. Each successful
completed report costs one native-read credit; errors refund that request's reservation.
A cold historical scan returns HTTP 202 with data.status=collecting, job_id, and
Retry-After:2. Repeat the SAME GET until HTTP 200; 202 status polls are free.
The native scan runs once in a bounded background reader, not once per status poll.
Transactions and spender views of the same key/account/link/date/fan selection share one
completed snapshot for five minutes, including across limit/offset pages. Snapshots are
key-scoped and every GET still rechecks account access; they are not shared across keys.
No data is persisted outside the project or to shared tables. A service restart discards
these read caches; the next GET safely recollects native data. At most two scans run at once,
with 32 cache entries, 8 MiB/report and a 64 MiB total result budget.
Both accept limit 1–100, offset >=0 and optional fan_id. UTC date boundaries are inclusive;
the default is the last 30 days. No subscription, purchase, message, campaign or native
profile is modified. No import, pixel/postback delivery or shared database migration occurs.

Responses use data:{summary,rows,filters,source}. Transaction rows include stable id,
conversion_type:new_transaction, amount_gross, amount_net, currency:USD, conversion_at,
fan_onlyfans_id, native transaction ID/type/status, native_campaign_id, subscription_at
and click:null. Spender rows group the exact same financial rows by fan, including
negative balances from reversals; they do not substitute the profile's lifetime spend.

The service fully collects campaign membership and the requested transaction window,
uses identity-checked subscribedOnData timestamps to exclude payments before the observed
subscription, and separately collects dated chargebacks. A negative ledger entry and its
matching chargeback are counted once. Exact native gross and net are summed with decimal
arithmetic. Missing identities, contradictory amounts, unsupported currency, repeated
cursors or incomplete pages fail the request rather than publishing partial totals.
Reads are bounded (240 requests / 300 seconds between provider calls, 200 pages per stream).
Large histories may require a narrower recent period or an existing-fan filter; this is not
a claim of constant-time reporting or a strict wall-clock cutoff on an in-flight native call.
Counts and monetary summaries describe the full requested window, not just the returned page.

An old native link does NOT retrospectively contain CreatorAPI click IDs or UTM values.
Historical rows therefore have click:null, never invented TEST123 data. Native membership
can overlap across campaigns; these legacy-link reports are not a cross-campaign
deduplicated acquisition model. The source metadata states this explicitly.
For ad-level UTM attribution going forward, advertise the CreatorAPI /go/{id} redirect.
The existing /api/smart-links reports keep their first-party stored click/conversion contract.

## Verification and limits

Automated checks exercise exact document fields and nested click objects, mobile/desktop
HTTP user agents, free/paid acquisition, delayed payments, client upserts, refunds, concurrency,
missing parameters, scoping, pagination and HTTP errors against private databases and synthetic
native responses. Real loopback HTTP exercises the request/storage/report path; it is NOT a
real phone/browser-to-native-subscription-to-purchase acceptance test.

Earlier separate live tests proved native campaign creation/deletion, origin click capture and
free subscribe/unsubscribe, with cleanup. They were separate tests, not one attributed chain.
Existing-purchase validation is now the user-requested alternative to making a new payment.
It tests native membership, real financial amounts and report output without spending again.
It does not retroactively prove a browser click/UTM chain that was never recorded.
No provider change is called verified solely because an isolated fixture test passed.

Polling is the delivery mechanism for this integration. Requirement 5 explicitly permits
operation without the recommended webhooks. Existing CreatorAPI webhook APIs keep their
existing envelope/event names; do not advertise them as the document's optional exact dialect.
No new external webhook or notification is configured.
After the linked acceptance, the document assigns API switch-over and the following week's
comparison to Instructify. These external actions are not silently performed by this release.
No key is entered into a third-party application without the necessary explicit authorization.
The integrating colleague owns the Instructify instance. Handoff: configure the reporting
base URL and existing key, choose an active connected creator, create a Smart Link, place
its /go/ URL in the ad, and confirm real clicks, acquisition and later payments in the reports.
Existing purchases verify financial reads without another test purchase. They cannot prove
a new ad-click chain retroactively. Compare seven actual days after integration; do not mark
that observation period complete using repeated fixture runs.

Response shape references:
[OnlyFansAPI list](https://docs.onlyfansapi.com/api-reference/smart-links/list-smart-links),
[clicks](https://docs.onlyfansapi.com/api-reference/smart-links/list-smart-link-clicks),
[conversions](https://docs.onlyfansapi.com/api-reference/smart-links/list-smart-link-conversions).
The Instructify request intentionally uses a seven-day window and maximum page size 100.

## Deleting a test link

DELETE /v1/{account}/smart-links/{id} requires a full key.
A native deletion failure returns HTTP 502, not success. The redirect is disabled;
link/click/conversion evidence and native offer IDs remain available for recovery.
Already acknowledged offer deletions are checkpointed and skipped by a later
explicit DELETE; an exact native 404 also means that offer is already absent.
No automatic native retry is performed. A timeout can mean a remote offer was
already removed; partial deletion is not an undoable all-or-nothing operation.
New offer refills and settings changes are blocked while cleanup is pending.
Pending delivery/action jobs are stopped; already in-flight external effects
cannot be recalled. A completed cleanup removes the local link and reports.
For live acceptance, separately verify the native inventory and subscription
state after cleanup; HTTP success alone is not proof of full live rollback.
