Skip to content
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.

A website, web app, portal, or customer dashboard does not use this endpoint, even when your server knows exactly who signed in. Those surfaces have a browser, so they are measured by the pixel, and a report made against a website surface is rejected. The two meters feed one combined total, so there is nothing to gain by choosing between them.

Endpoint

POST /partner-meter/v1/audience

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

Billing meter

$10/month from 0 app or bot users

Billing starts at one block whatever your audience size. A block covers up to 100 signed-in app, bot, or assistant users, and public feeds use declared-follower pricing.

Deduplication

Idempotent

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

The native-app licence is self-service

You do not need to email FXMacroData, request manual approval, or sign a separate agreement for standard customer-facing display. Buy Commercial Redistribution online, register the app in API Management, keep the API key on your backend, and use this meter for the app audience.

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, serves your own app, and reports the audience. Keeping the key server-side is a licence requirement. This endpoint sends no CORS headers because audience reports come from your backend.

Caching and persistent storage are recommended to reduce API requests. You may keep raw records, historical databases, private warehouses, backups, and multi-year alert or compliance logs without an FXMacroData retention limit. When cancellation, termination, or expiry of the add-on takes effect, stop customer-facing display, push notifications, and other deliveries containing the data or derived output, including from previously stored copies. Remove or disable external access; retained copies are for internal use only. An active API subscription alone does not extend redistribution rights. Resale of the underlying data remains excluded, and publisher conditions continue to apply. See storage and retention terms.

Start with your own channel

This page is the full reference for apps, bots, assistants, newsletters, and communities, covering every platform at once. If you only want the steps for one surface, open its install guide instead: it names the meter that applies to you, and the meter that does not, before any instructions. Find your channel.

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. A website or web app does not count as one: register it as a website and measure it with the pixel.
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));
  },
};

Keep the key in the Worker's secret storage. Your backend may supply the data needed for your app's permitted human-readable display. It must not expose a reusable FXMacroData dataset or feed to third parties. Cache or persist data to reduce API requests.

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.