DevMargin Push API

DevMargin pulls revenue from your billing rails on its own. But some numbers no rail can know:

The Push API is how your product reports those. Everything you push is stored as latest state and merged into the next daily snapshot, so history stays in one place and every figure remains traceable.


1. Get your ids and a token

Channel or project id — open Projects. Every project heading and channel row carries a copyable id chip. Click it.

Which channel?

Token — Settings → Push API → create one. It starts with dmp_ and is shown once. Push tokens can only write; they cannot read your portfolio.


2. The endpoint

> Machine-readable: the whole HTTP surface — this endpoint and the signed-in > read API — is described in OpenAPI 3.1 at > `/api/openapi`. Point Swagger UI, Postman > or a client generator at it rather than transcribing the tables below.

POST https://devmargin.app/api/push
Authorization: Bearer dmp_...
Content-Type: application/json

Shape A — metrics for a channel

{ "channel": "<channel-id>", "metrics": { "users": 1234 } }

All metric fields are optional. Send only what you actually know:

FieldTypeNotes
usersintegerThe only field a rail-backed channel accepts
activeSubs, newSubs, churnedSubsinteger
mrr, arr, oneOffRevenuenumberPush-rail channels only
currencystringISO-4217, e.g. "EUR". Converted to USD at snapshot FX

Shape B — a month that has already been earned

{
  "channel": "<channel-id>",
  "earned": { "month": "2026-07", "amount": 48200, "currency": "CZK", "basis": "proceeds" }
}

Shape A carries latest state — what your product is earning now. This carries a dated fact: what July earned. They answer different questions and the dashboard uses them in different places, so a seasonal product needs both.

FieldTypeNotes
monthstring"YYYY-MM". The current month is accepted and marked partial; a future one is refused
amountnumberIn currency, >= 0
currencystringISO-4217
basisstringRequired. "gross" before any platform fee, "proceeds" after it

basis has no default deliberately. "48200 CZK" is a different fact depending on whether a marketplace already took its cut, and a portfolio total that silently mixed the two would be wrong in a way nobody could see.

Push-rail channels only: a channel backed by Stripe or the App Store gets its months from that rail. Upsert keyed by month, so re-pushing a corrected figure replaces it rather than adding to it. The USD figure is filled in at the next pull, from that run's rates — the same single FX source every other figure uses.

Don't push the same money through both shapes. If a month's takings go through earned, leave oneOffRevenue out of your metrics push, or the same money is described twice in two places.

Shape C — a running cost for a project

{ "project": "<project-id>", "cost": { "label": "OpenAI", "monthly": 42.10, "currency": "USD" } }

Upsert keyed by label, so re-pushing the same label every day updates one row instead of piling up duplicates. Amounts are monthly — divide yearly bills by twelve. Costs apply to net margin immediately; no pull needed.

Responses

StatusMeaning
200Stored
400Bad or unknown field, or an explicit null
401Missing or invalid token
403Wrong token kind (a read/MCP token cannot write)
404That channel or project isn't in your account
422Money pushed to a rail-backed channel
429Over 60 pushes per hour for this token

3. Two rules worth understanding

Omit what you don't know. Never send 0 to mean "unknown", and never send null — it's rejected. A missing field shows as in the dashboard and stays honestly unknown. A fabricated zero silently corrupts your net-margin maths, which defeats the point of the product.

One source per channel. A channel's money comes from exactly one place. If a channel draws from Stripe, RevenueCat, the App Store or Google Play, then pushing mrr there would double-count what the rail already reports — so it's refused with 422. Only users is accepted, because no rail reports it. For revenue no rail sees, use a push-rail channel.


4. Security

The token is a write credential. If it leaks, someone can write junk numbers into your dashboard — they cannot read it. Still, treat it as a secret:

Pushing is idempotent (it's an upsert of latest state), so retries are safe, and nothing can be deleted through the API. The worst outcome of a bad push is a wrong latest value, corrected by the next one.


5. How often?

Whenever your numbers change — but once a day is plenty, because the snapshot that reads them runs daily at 05:00 UTC. The rate limit is 60 pushes per hour per token. Values older than seven days still count, but the snapshot notes how stale they are rather than quietly presenting them as current.


6. Minimal example

curl -X POST https://devmargin.app/api/push \
  -H "Authorization: Bearer $DEVMARGIN_PUSH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channel":"<channel-id>","metrics":{"users":1234}}'

7. Copy-paste prompt for an AI coding agent

Paste this into Claude Code (or a similar agent) inside the repository of the app that should report its numbers:

Add a nightly push of this project's metrics to DevMargin (https://devmargin.app),
a portfolio dashboard showing revenue and net margin across billing rails. It can't see
things only this app knows — user counts, revenue collected outside a billing rail — so
we push those. Full reference: https://devmargin.app/docs/push-api

ENDPOINT CONTRACT
POST https://devmargin.app/api/push
Headers: Authorization: Bearer <DEVMARGIN_PUSH_TOKEN>, Content-Type: application/json
Three body shapes (one per request):
  1) { "channel": "<uuid>", "metrics": { ... } }
     Latest state. Optional fields — send only what you actually know:
       users (int), activeSubs (int), newSubs (int), churnedSubs (int),
       mrr (number), arr (number), oneOffRevenue (number), currency ("EUR", ISO-4217)
     IMPORTANT: if the channel is backed by a billing rail (Stripe, RevenueCat, App
     Store, Google Play), ONLY "users" is accepted — money is rejected with HTTP 422
     because it would double-count what the rail already reports.
  2) { "channel": "<uuid>", "earned": { "month": "2026-07", "amount": 48200,
       "currency": "CZK", "basis": "proceeds" } }
     A month that already happened, for the Earned history and lifetime totals.
     Push-rail channels only. basis is REQUIRED: "gross" (before any platform fee)
     or "proceeds" (what reached you) — there is no default, because the same number
     means different things and mixing them corrupts a portfolio total invisibly.
     Upsert keyed by month, so a corrected figure replaces the old one. Do not also
     report that money as oneOffRevenue in shape 1.
  3) { "project": "<uuid>", "cost": { "label": "OpenAI", "monthly": 42.10, "currency": "USD" } }
     Upsert keyed by label, so re-pushing daily is idempotent. Monthly amounts —
     divide yearly bills by 12.
Responses: 200 ok · 400 bad field · 401 bad token · 403 wrong token kind · 404 unknown id
· 422 money on a rail-backed channel · 429 rate limit (60/hour/token).

HONESTY RULE (non-negotiable — it is the point of the product)
Omit any field you cannot compute. NEVER send 0 to mean "unknown", and never send null
(it is rejected). A missing field stays honestly unknown; a fabricated 0 silently
corrupts net-margin maths.

SECURITY REQUIREMENTS
- The token is a WRITE credential. It lives only in a server-side environment variable
  named DEVMARGIN_PUSH_TOKEN. Never in a mobile app binary, never in client-side JS,
  never committed, never logged, never printed in error messages or CI output.
- If this project is a mobile app with no backend, do NOT push from the device. Push from
  whatever server-side source of truth actually holds the numbers (database, analytics
  job, CI). If there is none, say so and stop rather than shipping a token to devices.
- Add DEVMARGIN_PUSH_TOKEN to .env.example (empty) and confirm .env is git-ignored.

TASK
1. Inspect this repo and tell me FIRST: what the project is, where its user count
   genuinely comes from (a DB query? an auth provider API? analytics?), and which
   server-side mechanism fits a once-a-day push — an existing cron route, a Vercel cron,
   a Supabase Edge Function with pg_cron, a GitHub Action, or a script I run. Recommend
   one and wait for my OK before writing code.
2. Then implement it:
   - Compute the metrics from the real source of truth. If a number isn't reliably
     computable, omit the field and tell me which and why — do not invent it.
   - POST with a short timeout, and make the whole thing non-fatal: a DevMargin outage
     must never break this app or fail a deployment.
   - Log one line (status + which fields were sent), with the token redacted.
   - No new dependencies for the HTTP call — use the platform's built-in fetch or curl.
3. Verify: run it once against production, show me the HTTP status and response body, and
   confirm the token appears nowhere in the repo (grep) or in the log output.

I WILL PROVIDE (ask me — don't guess):
- DEVMARGIN_PUSH_TOKEN (starts with dmp_) — from DevMargin Settings → Push API
- The channel id — copy the "channel id" chip on https://devmargin.app/projects
- The project id, if we're pushing costs — copy the "project id" chip on the same page