Backend-reported audience

Meter an app, bot, or assistant

A mobile app, desktop app, browser extension, chat bot, or AI assistant has no browser for the redistribution pixel to run in. Your backend reports the audience instead: one scrambled reference per unique person per billing period, to one endpoint. Repeats are free, so retries, replays, and reporting the same person on every session never move the billable count.

Endpoint

POST /partner-meter/v1/audience

Server-to-server only. Bearer auth with your redistribution key.

Billing meter

$10 per 100 app or bot users/month

This 100-user rule applies to signed-in apps, bots, and assistants. Public feeds use declared-follower pricing.

Deduplication

Idempotent

One stored identity per reference per period. Over-calling costs nothing.

The one rule that matters

The redistribution key authenticates both the data API and this meter. It must never ship inside an app binary, a mobile bundle, a browser extension, or any client a user can unpack. Your backend fetches FXMacroData, caches what it needs, serves your own app, and reports the audience. That is a licence requirement, not a suggestion, and it is also why this endpoint deliberately sends no CORS headers: a browser cannot call it.

1. How the pieces fit

  your app / bot           your backend                        FXMacroData
  ---------------          --------------------------          -------------------
  signed-in user  ------>  1. resolve internal user id
                           2. user_ref = HMAC(secret, id)
                           3. reported this period? ------>     (optional, local)
                           4. POST /partner-meter/v1/audience   202 Accepted
                                                                one unique user

                           5. GET api.fxmacrodata.com/v1/...    calendar + data
                  <------  6. serve from your own cache

Step 3 is optional. The meter deduplicates server-side, so a backend that keeps no memory of who it has already reported is still billed correctly; it just makes more HTTP calls than it needs to. Meter calls do not consume your API request allowance, only calls to api.fxmacrodata.com do.

Your monthly usage view and your billable figure are counted over slightly different windows, and you do not have to reason about either. Reporting the same person once a day satisfies both without tracking period boundaries yourself.

2. Before your first call

  1. 1. Register the surface. Open API Management and add the app, bot, or assistant. Pick the channel type (Mobile app, iOS app, Android app, Desktop app, Browser extension, Trading platform plugin, Telegram bot, Discord bot, Slack app, Custom GPT, and so on) and give it a name. That name is your surface_id. Registering it enables access immediately, with no review queue.
  2. 2. State a declared audience. Roughly how many people use the surface each month. It is not what you are billed on, billing uses what your backend reports, but the licence requires the figure on record and it is the baseline an under-reporting review is measured against.
  3. 3. Copy the redistribution key. The same key as the data API. Store it in your server secret manager.
  4. 4. Choose a hashing secret. A long random string only you hold, used to turn internal user ids into references we cannot reverse. Do not reuse your FXMacroData key for this.

If you distribute through more than one surface, say an iOS app, an Android app, and a Telegram bot, register each one and send its own surface_id. They share a single billable total; the split exists for your reporting, not your invoice.

3. Endpoint reference

POST https://fxmacrodata.com/partner-meter/v1/audience
Authorization: Bearer <your redistribution key>
Content-Type: application/json

{
  "surface_id": "Acme Markets iOS",
  "user_ref": "9f2c4a1e8b7d6c5f0a3e2d1c9b8a7f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6d"
}

X-API-Key: <key> is accepted in place of the bearer header.

Request fields

FieldRequiredNotes
user_refYesYour scrambled reference for one person. 1 to 80 characters, must start with a letter or digit, then letters, digits, _ . : -. A hex HMAC-SHA256 digest is 64 characters and fits exactly. Standard base64 does not: + / = are rejected. anonymous_visitor_id is accepted as an alias.
surface_idConditionalThe surface name exactly as registered, matched case-insensitively. Required when the account has more than one non-web surface, optional when there is exactly one. surface is accepted as an alias.
report_typeNouser (default) or roster. See roster reports.
occurred_atNoISO-8601 timestamp of the activity. Defaults to receipt time. timestamp is accepted as an alias. Decides which month the report lands in.
surface_typeNoFree-form label kept on the event for your own reporting, such as ios_app. The authoritative type comes from the registered surface.
meter_versionNoYour integration version string. Useful when you roll out a change and want to see which build reported.

The request body is capped at 16 KB. There is no batch form: one person per call. Send nothing else. No names, no email addresses, no device identifiers, no IP addresses. The fields above are the entire surface area.

Responses

StatusBodyMeaning
202{"ok": true, "event_id": "..."}Recorded. A first sighting this period counts one unique user; a repeat is stored and changes nothing.
400user_ref is requiredMissing reference.
400user_ref contains unsupported charactersAlmost always base64 padding or a raw email address. Use hex.
400register a mobile app, channel, newsletter, or community surface in API Management before reporting an audienceThe account has no non-web surface registered yet.
400surface_id is required when more than one non-web surface is registeredName the surface.
400surface_id is not a registered non-web surfaceThe name does not match a registered surface. Check for a typo or a trailing space.
400payload must be JSON, payload is too largeMalformed or oversized body.
401a redistribution API key is requiredNo bearer token and no X-API-Key header.
401the API key is not an active redistribution keyWrong key, revoked key, or a plain API key with no redistribution licence.
500report could not be storedTransient. Retry with backoff; retries are free.

Treat the meter as best-effort telemetry in your request path: never let a failed report block a user. Log it, retry later, and remember which direction the failure runs. A dropped report makes your audience look smaller than it is, and under-reporting is the thing the licence audit exists to catch.

4. Building the user reference

Take your internal user id, run HMAC-SHA256 over it with a secret only you hold, and send the hex digest. We never see the id, cannot reverse the digest, and cannot correlate your users with anyone else. You get a stable reference that is identical on every call for the same person, which is exactly what deduplication needs.

Keep the secret stable. Rotating it mid-period re-randomises every reference and every user is counted a second time, which inflates your own invoice. If you must rotate, do it on the first day of a billing period.

Node.js

import { createHmac } from "node:crypto";

const userRef = (userId) =>
  createHmac("sha256", process.env.FXMD_METER_SECRET)
    .update(String(userId))
    .digest("hex");

Python

import hmac, hashlib, os

def user_ref(user_id: str) -> str:
    return hmac.new(
        os.environ["FXMD_METER_SECRET"].encode(),
        str(user_id).encode(),
        hashlib.sha256,
    ).hexdigest()

Go

func userRef(userID string) string {
    mac := hmac.New(sha256.New, []byte(os.Getenv("FXMD_METER_SECRET")))
    mac.Write([]byte(userID))
    return hex.EncodeToString(mac.Sum(nil))
}

PHP

function user_ref(string $userId): string {
    return hash_hmac('sha256', $userId, getenv('FXMD_METER_SECRET'));
}

Ruby

require "openssl"

def user_ref(user_id)
  OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("FXMD_METER_SECRET"), user_id.to_s)
end

Java / Kotlin

fun userRef(userId: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(System.getenv("FXMD_METER_SECRET").toByteArray(), "HmacSHA256"))
    return mac.doFinal(userId.toByteArray()).joinToString("") { "%02x".format(it) }
}

C# / .NET

static string UserRef(string userId)
{
    var key = Encoding.UTF8.GetBytes(
        Environment.GetEnvironmentVariable("FXMD_METER_SECRET"));
    using var mac = new HMACSHA256(key);
    var hash = mac.ComputeHash(Encoding.UTF8.GetBytes(userId));
    return Convert.ToHexString(hash).ToLowerInvariant();
}

Elixir

def user_ref(user_id) do
  :crypto.mac(:hmac, :sha256, System.fetch_env!("FXMD_METER_SECRET"), to_string(user_id))
  |> Base.encode16(case: :lower)
end

Three ways to get this wrong

  • Base64 output. digest("base64") produces +, /, and =, all rejected. Use hex, or base64url with no padding.
  • A per-session or per-device reference. A new value each launch counts one person many times and inflates your bill. Hash the account, not the session, not the device, not the install id.
  • An anonymous app with no accounts. Hash a long-lived install identifier instead, and say so in your privacy policy. Expect it to over-count reinstalls; that is the honest direction to be wrong in.

5. When to call

Report a person when they actually see FXMacroData-derived content: the calendar screen opened, the release alert delivered, the indicator card rendered. Not on app launch, not on sign-in, unless the data is on the first screen. Pick whichever of these fits your architecture.

PatternCall volumeGood for
Fire and forget. Report on every request that serves the data.HighestSmall audiences, or a first integration you want working today. Correct, just chatty.
Local day cache. Report, then remember the user for 24 hours in Redis or memcached.One call per user per dayThe default recommendation. Survives period boundaries with no bookkeeping.
Period ledger. A metered_at column on your users table, cleared each month.One call per user per monthBackends that already track monthly activity. Must handle the period boundary yourself.
Nightly batch. A cron job that walks users active since the last run.One batch per nightBots and newsletters where activity is already logged, and anything with a strict request path budget.
// Local day cache, Node + Redis
async function meterUser(userId) {
  const ref = userRef(userId);
  const key = `fxmd:metered:${ref}`;
  if (await redis.set(key, "1", { NX: true, EX: 86400 }) === null) return;
  await reportAudience(ref);          // failures are logged, never thrown
}
# Nightly batch, Python
since = datetime.now(timezone.utc) - timedelta(days=1)
for user in users.active_since(since):        # anyone who saw the calendar
    report_audience(user_ref(user.id))        # repeats are free

6. Backend recipes

Every one of these does the same three things: build the reference, POST it, and never let a meter failure reach the user. Set a short timeout. The call is telemetry, not a dependency.

curl (verify your key first)

curl -sS -X POST https://fxmacrodata.com/partner-meter/v1/audience \
  -H "Authorization: Bearer $FXMD_KEY" \
  -H "Content-Type: application/json" \
  -d '{"surface_id":"Acme Markets iOS","user_ref":"testuser0000000000000000000000000000000000000000000000000000abcd"}'

# {"ok":true,"event_id":"accepted-audience_ping-..."}

Node.js / Express

import { createHmac } from "node:crypto";
import { logger } from "./your-server-logger.js";   // pino, winston, whatever you use

const SURFACE = "Acme Markets iOS";

async function reportAudience(userId) {
  const userRef = createHmac("sha256", process.env.FXMD_METER_SECRET)
    .update(String(userId)).digest("hex");
  try {
    const res = await fetch("https://fxmacrodata.com/partner-meter/v1/audience", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FXMD_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ surface_id: SURFACE, user_ref: userRef }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) logger.warn("fxmd meter", res.status, await res.text());
  } catch (err) {
    logger.warn("fxmd meter failed", err.message);    // never rethrow
  }
}

app.get("/api/calendar", async (req, res) => {
  void reportAudience(req.user.id);                   // do not await
  res.json(await calendarCache.get());
});

Next.js route handler

// app/api/calendar/route.ts  -  runs on the server, key stays server-side
import { after } from "next/server";

export async function GET() {
  const session = await auth();
  after(() => reportAudience(session.user.id));   // after the response is sent
  return Response.json(await getCalendar());
}

Python / FastAPI

import hashlib, hmac, os, httpx
from fastapi import BackgroundTasks, Depends

METER_URL = "https://fxmacrodata.com/partner-meter/v1/audience"
SURFACE = "Acme Markets iOS"

def user_ref(user_id: str) -> str:
    return hmac.new(os.environ["FXMD_METER_SECRET"].encode(),
                    str(user_id).encode(), hashlib.sha256).hexdigest()

async def report_audience(user_id: str) -> None:
    try:
        async with httpx.AsyncClient(timeout=3.0) as client:
            await client.post(
                METER_URL,
                headers={"Authorization": f"Bearer {os.environ['FXMD_API_KEY']}"},
                json={"surface_id": SURFACE, "user_ref": user_ref(user_id)},
            )
    except Exception as exc:
        logger.warning("fxmd meter failed: %s", exc)

@app.get("/api/calendar")
async def calendar(tasks: BackgroundTasks, user=Depends(current_user)):
    tasks.add_task(report_audience, user.id)
    return await calendar_cache.get()

Django

# Celery keeps the meter off the request path entirely.
@shared_task(ignore_result=True)
def report_audience(user_id):
    requests.post(
        "https://fxmacrodata.com/partner-meter/v1/audience",
        headers={"Authorization": f"Bearer {settings.FXMD_API_KEY}"},
        json={"surface_id": settings.FXMD_SURFACE, "user_ref": user_ref(user_id)},
        timeout=3,
    )

class CalendarView(APIView):
    def get(self, request):
        report_audience.delay(request.user.pk)
        return Response(calendar_cache.get())

Go

func ReportAudience(ctx context.Context, userID string) {
    body, _ := json.Marshal(map[string]string{
        "surface_id": surface,
        "user_ref":   userRef(userID),
    })
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
        "https://fxmacrodata.com/partner-meter/v1/audience", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("FXMD_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Printf("fxmd meter: %v", err)
        return
    }
    defer resp.Body.Close()
}

PHP / Laravel

// app/Jobs/ReportAudience.php
public function handle(): void
{
    Http::withToken(config('services.fxmd.key'))
        ->timeout(3)
        ->post('https://fxmacrodata.com/partner-meter/v1/audience', [
            'surface_id' => config('services.fxmd.surface'),
            'user_ref'   => hash_hmac('sha256', $this->userId, config('services.fxmd.secret')),
        ]);
}

// Controller
ReportAudience::dispatch($request->user()->id)->afterResponse();

Ruby on Rails

class ReportAudienceJob < ApplicationJob
  def perform(user_id)
    ref = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("FXMD_METER_SECRET"), user_id.to_s)
    Faraday.post("https://fxmacrodata.com/partner-meter/v1/audience") do |req|
      req.headers["Authorization"] = "Bearer #{ENV.fetch('FXMD_API_KEY')}"
      req.headers["Content-Type"] = "application/json"
      req.options.timeout = 3
      req.body = { surface_id: ENV.fetch("FXMD_SURFACE"), user_ref: ref }.to_json
    end
  rescue => e
    Rails.logger.warn("fxmd meter failed: #{e.message}")
  end
end

Java / Spring Boot

@Async
public void reportAudience(String userId) {
    var body = Map.of("surface_id", surface, "user_ref", userRef(userId));
    try {
        restClient.post()
            .uri("https://fxmacrodata.com/partner-meter/v1/audience")
            .header("Authorization", "Bearer " + apiKey)
            .contentType(MediaType.APPLICATION_JSON)
            .body(body)
            .retrieve()
            .toBodilessEntity();
    } catch (RestClientException ex) {
        log.warn("fxmd meter failed: {}", ex.getMessage());
    }
}

ASP.NET Core

public async Task ReportAudienceAsync(string userId, CancellationToken ct)
{
    var payload = new { surface_id = _surface, user_ref = UserRef(userId) };
    var request = new HttpRequestMessage(HttpMethod.Post,
        "https://fxmacrodata.com/partner-meter/v1/audience")
    {
        Content = JsonContent.Create(payload)
    };
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
    try { await _http.SendAsync(request, ct); }
    catch (Exception ex) { _logger.LogWarning(ex, "fxmd meter failed"); }
}

Firebase Cloud Functions

exports.calendar = functions
  .runWith({ secrets: ["FXMD_API_KEY", "FXMD_METER_SECRET"] })
  .https.onCall(async (data, context) => {
    if (!context.auth) throw new functions.https.HttpsError("unauthenticated", "sign in");
    await reportAudience(context.auth.uid);      // uid is already stable and internal
    return calendarCache.get();
  });

Supabase edge function

Deno.serve(async (req) => {
  const { data: { user } } = await supabase.auth.getUser(req.headers.get("Authorization"));
  const key = new TextEncoder().encode(Deno.env.get("FXMD_METER_SECRET"));
  const mac = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" },
                                            false, ["sign"]);
  const sig = await crypto.subtle.sign("HMAC", mac, new TextEncoder().encode(user.id));
  const userRef = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");

  await fetch("https://fxmacrodata.com/partner-meter/v1/audience", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Deno.env.get("FXMD_API_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ surface_id: "Acme Markets iOS", user_ref: userRef }),
  });
  return Response.json(await getCalendar());
});

Cloudflare Worker

export default {
  async fetch(request, env, ctx) {
    const userId = await authenticate(request, env);
    ctx.waitUntil(reportAudience(userId, env));   // runs after the response
    return Response.json(await calendar(env));
  },
};

A Worker is a server, so the key is safe there. A Worker that simply proxies our API to your app is not: that is API mirroring and the licence prohibits it. Cache and reshape, do not relay.

Any scheduler (cron, Cloud Scheduler, Sidekiq, Hangfire)

#!/usr/bin/env bash
# Nightly: report everyone who opened the calendar screen since yesterday.
psql -At -c "select id from users where calendar_seen_at > now() - interval '1 day'" \
| while read -r uid; do
    ref=$(printf '%s' "$uid" | openssl dgst -sha256 -hmac "$FXMD_METER_SECRET" -r | cut -d' ' -f1)
    curl -sS -o /dev/null -X POST https://fxmacrodata.com/partner-meter/v1/audience \
      -H "Authorization: Bearer $FXMD_KEY" -H "Content-Type: application/json" \
      -d "{\"surface_id\":\"Acme Markets iOS\",\"user_ref\":\"$ref\"}"
  done

7. App recipes

The app never calls FXMacroData. It calls your backend, which already knows who the user is from their session token, and your backend does the metering. Below is the client half for each framework: a single authenticated call to your own endpoint when the FXMacroData-powered screen appears.

If your app already fetches the calendar from your backend, you are done. Meter inside that handler and skip this section entirely. These snippets are for apps where the data screen is served from a cache and no request reaches your server.

Swift, SwiftUI

struct CalendarView: View {
    var body: some View {
        CalendarList()
            .task { await MeterClient.shared.reportCalendarView() }
    }
}

enum MeterClient {
    static let shared = MeterClient.self
    static func reportCalendarView() async {
        var req = URLRequest(url: URL(string: "https://api.example.com/v1/meter/calendar")!)
        req.httpMethod = "POST"
        req.setValue("Bearer \(Session.token)", forHTTPHeaderField: "Authorization")
        _ = try? await URLSession.shared.data(for: req)
    }
}

Swift, UIKit

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    Task { await MeterClient.reportCalendarView() }
}

Use viewDidAppear, not viewDidLoad: a preloaded tab that the user never opens is not an audience.

Kotlin, Android

class CalendarFragment : Fragment() {
    override fun onResume() {
        super.onResume()
        viewLifecycleOwner.lifecycleScope.launch {
            runCatching { api.meterCalendarView() }   // Retrofit, suspend fun
        }
    }
}

// Retrofit service on your own backend
interface AppBackendApi {
    @POST("v1/meter/calendar")
    suspend fun meterCalendarView(): Response<Unit>
}

Kotlin, offline-tolerant

// WorkManager retries when the device regains connectivity, so a user who
// opened the calendar on a plane is still reported.
val work = OneTimeWorkRequestBuilder<MeterWorker>()
    .setConstraints(Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED).build())
    .build()
WorkManager.getInstance(context).enqueue(work)

Java, Android

@Override
protected void onResume() {
    super.onResume();
    executor.execute(() -> {
        try { api.meterCalendarView().execute(); }
        catch (IOException e) { Log.w("meter", e); }
    });
}

React Native / Expo

import { useFocusEffect } from "@react-navigation/native";

useFocusEffect(
  useCallback(() => {
    fetch("https://api.example.com/v1/meter/calendar", {
      method: "POST",
      headers: { Authorization: `Bearer ${session.token}` },
    }).catch(() => {});
  }, [session.token])
);

Flutter / Dart

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    try {
      await http.post(
        Uri.parse('https://api.example.com/v1/meter/calendar'),
        headers: {'Authorization': 'Bearer ${session.token}'},
      );
    } catch (_) {}
  });
}

.NET MAUI

protected override async void OnAppearing()
{
    base.OnAppearing();
    try { await _api.MeterCalendarViewAsync(); }
    catch (Exception ex) { Debug.WriteLine(ex); }
}

Ionic / Capacitor / Cordova

// Angular, Vue, or React inside Capacitor - the WebView is still your app,
// so call your own backend, never the meter, and never embed the key.
ionViewDidEnter() {
  this.http.post('/v1/meter/calendar', {}).subscribe({ error: () => {} });
}

A Capacitor WebView loading a page on your own registered domain can use the web pixel instead. Pick one, not both, or the same person is metered twice under two identities.

Unity / C#

IEnumerator ReportCalendarView() {
    var req = UnityWebRequest.Post(BackendUrl + "/v1/meter/calendar", "");
    req.SetRequestHeader("Authorization", "Bearer " + Session.Token);
    yield return req.SendWebRequest();
}

Electron / Tauri desktop

// Main process only. The renderer is untrusted for this purpose: anything
// it can read, a user with devtools can read too.
ipcMain.handle("calendar:opened", async (_e) => {
  await reportAudience(currentUserId);       // your backend, or here if you own it
});

Browser extension

// background service worker -> your backend -> meter
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === "calendar-opened") {
    fetch("https://api.example.com/v1/meter/calendar", {
      method: "POST",
      headers: { Authorization: `Bearer ${await getToken()}` },
    }).catch(() => {});
  }
});

An extension bundle is readable by anyone who installs it. The redistribution key cannot live there under any packing, obfuscation, or build step.

8. Bots and assistants

A bot already has a stable per-person identifier in every update it receives. Hash it and report when the bot answers with FXMacroData content. A broadcast to a channel is different: that is a roster, covered below.

Telegram, python-telegram-bot

async def calendar(update: Update, ctx) -> None:
    await report_audience(user_ref(update.effective_user.id))
    await update.message.reply_text(render_calendar())

Telegram, Telegraf

bot.command("calendar", async (ctx) => {
  void reportAudience(ctx.from.id);
  await ctx.reply(renderCalendar());
});

Discord, discord.js

client.on(Events.InteractionCreate, async (i) => {
  if (i.commandName !== "calendar") return;
  void reportAudience(i.user.id);
  await i.reply({ embeds: [calendarEmbed()] });
});

Slack, Bolt

app.command("/calendar", async ({ command, ack, respond }) => {
  await ack();
  void reportAudience(command.user_id);
  await respond(calendarBlocks());
});

WhatsApp Business webhook

# The wa_id is stable per person; hash it like any other user id.
wa_id = payload["entry"][0]["changes"][0]["value"]["messages"][0]["from"]
await report_audience(user_ref(wa_id))

Custom GPT or AI assistant

# Your action endpoint is the metering point. If the platform gives you a
# stable end-user id, hash it. If it does not, meter per conversation id and
# say so when you register the surface - it over-counts, which is safe.
user_key = headers.get("openai-ephemeral-user-id") or conversation_id
await report_audience(user_ref(user_key))

9. Roster reports

Newsletters, Telegram channels, Discord and Slack communities, SMS and push lists, podcasts, and printed reports have no per-person interaction to meter. Nobody can count who read a broadcast. These report a subscriber count instead, on the same endpoint.

POST https://fxmacrodata.com/partner-meter/v1/audience
Authorization: Bearer <your redistribution key>
Content-Type: application/json

{
  "report_type": "roster",
  "subscriber_count": 4820
}

A roster report is account-level: it carries the combined audience across every roster channel you run, so it takes no surface_id. Send it from a scheduled job that reads the current count from your email or chat platform. That keeps the figure inside the current billing period, which a number typed into a form quietly stops doing.

You can also enter it by hand in API Management. Social feeds are different again: they are priced on declared follower reach at 1,000 followers per block, because a post reaches a small single-digit percentage of a following.

10. Attribution inside an app

The licence requires visible FXMacroData attribution wherever the data appears. On the web the pixel checks for it automatically; in an app nothing can check, so it is on you. What satisfies it:

  • A visible line on or adjacent to the data screen: Data powered by FXMacroData, tappable, opening https://fxmacrodata.com.
  • Or a credit in the screen footer, the About screen, and the data-source list, if the calendar is one panel inside a wider dashboard.
  • Not acceptable: attribution only in a licence file, a settings sub-page nobody reaches, or a web page outside the app.

Add ?utm_source= your surface name to the link if you want the referral traffic attributed back to you.

11. Verify and troubleshoot

Send one report with a throwaway reference, then open the redistribution panel in API Management. It shows every registered surface, the combined measured users, the backend-reported audience, and the estimated next invoice. A report lands within a few seconds.

SymptomCauseFix
202 responses, but the panel still shows zeroYou are reporting the same user_ref every time, so there is exactly one unique userVary the reference. One test call per test identity.
surface_id is not a registered non-web surfaceThe string does not match the registered nameCopy the name from API Management. Matching ignores case but not spelling or trailing spaces.
the API key is not an active redistribution keyA standard API key, a revoked key, or a licence that is not activeUse the key shown in the redistribution panel. Check the subscription is current.
user_ref contains unsupported charactersBase64 output, a raw email, or a UUID with bracesSend the hex digest.
Count is far higher than your real audienceA per-session or per-device reference, or a rotated hashing secretHash the account id and keep the secret stable.
Count is far lower than your declared audienceMetering the wrong screen, an exception swallowed in the request path, or a batch job that stoppedLog every non-202 response. A reported audience far below the reach you declared is flagged for review.
Nothing at all, no errorThe call is being made from client code and blocked, or the response is never readConfirm the call runs server-side and log the status code.

Related

AI Answer-Ready

Key Facts

Page
Redistribution App Meter
Section
Documentation
Canonical URL
https://fxmacrodata.com/documentation/redistribution-app-meter
Source
FXMacroData editorial and official publisher references
Last Updated
See page metadata

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

What is this page about? This page explains Redistribution App Meter with directly usable context for trading, research, and API workflows.

What source should be cited? Use the canonical URL and the listed source field; cite official publisher references when available.

How fresh is this content? The last updated value above reflects the page metadata or latest available data timestamp.

Can this be used in AI assistants? Yes. This section is intentionally structured for retrieval and citation in chat assistants.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.