All pages

HTTP API

Four routes your application calls, plus the one RevenueCat calls. Everything below is generated from the schemas the server validates production traffic against, so this page cannot describe an endpoint we do not serve or a field we do not accept. The same document is served live at api.marginfuse.com/openapi.json.

Authentication

A project API key as a bearer token: Authorization: Bearer mf_live_.... Keys are secrets and this is a server-side API, so never ship one anywhere a user can read it. Only the hash is stored, so a lost key is rotated, never recovered.

Every failure is safe

The official SDKs treat any non-2xx, timeout or transport error as fail-open: the application proceeds unprotected rather than breaking. A rate limit, an outage and a rejected payload all degrade measurement, never availability. Anything you build directly against this API should hold the same contract.

Errors share one envelope. error is a stable machine-readable code, while detail and issues are human-facing and may change, so branch on error alone.

Routes

post/v1/events

Report usage for AI calls that already happened

Accepts up to 500 events per request. Each event is processed independently: the response carries one result per event, in request order, and a rejected event does not fail the batch.

Idempotent on eventId. Re-sending an event you already sent returns status duplicate and changes nothing, so a retry after an ambiguous network failure is always safe. To correct a figure you already reported, send a new eventId and set correctsEventId to the original.

Send costUsd when your provider tells you the real charge (gateways such as OpenRouter do). Otherwise omit it and MarginFuse prices the call from token counts, labeling the result as estimated rather than observed.

Request body

EventsRequest fields
FieldTypeRequired
eventIdstring, 1..256 charsyes
customerIdstring, 0..256 chars
planstring, 0..64 chars
featurestring, 0..128 chars
providerstring, 1..64 charsyes
modelstring, 1..128 charsyes
requestedModelstring, 0..128 chars
usageobject
costUsdstring
occurredAtstring, date-time
outcome"success" | "provider_error" | "app_cancelled" | "timeout"
decisionIdstring, 0..128 chars
retryOfEventIdstring, 0..256 chars
correctsEventIdstring, 0..256 chars

200 response

EventsResponse fields
FieldTypeRequired
resultsobject[]yes

Status codes

  • 200Batch processed. Inspect each result for its individual status.
  • 401Missing, malformed, or unrecognized API key.
  • 422The body did not match the schema. issues lists up to 10 problems.
  • 429Rate limit exceeded for this API key. These events are neither measured nor protected, but the SDK fails open so the caller keeps working.
post/v1/decisions

Ask whether the next AI call should run

Evaluates the project's policies against this customer's current margin and returns an action to take before you call the provider:

  • allow proceed with the model you asked for
  • downgrade proceed, but use the model in the response instead
  • topup_required do not call the provider; present the top-up path to the customer
  • block do not call the provider

Policies run in dry-run before they run live, so this endpoint records what protection would have done against real traffic without acting on it. A dry-run policy always answers allow.

Report back with /v1/decisions/{id}/ack once you know what your application did. Savings can only be credited as realized on that confirmation.

Request body

DecideRequest fields
FieldTypeRequired
customerIdstring, 1..256 charsyes
planstring, 0..64 chars
featurestring, 0..128 chars
providerstring, 1..64 charsyes
modelstring, 1..128 charsyes
expectedUsageobject

200 response

Decision fields
FieldTypeRequired
idstringyes
action"allow" | "downgrade" | "topup_required" | "block"yes
modelstringyes
providerstringyes
topupContextstring
degradedbooleanyes
degradedReasonstring

Status codes

  • 200A verdict. Enforce on action alone.
  • 401Missing, malformed, or unrecognized API key.
  • 422The body did not match the schema. issues lists up to 10 problems.
  • 429Rate limit exceeded for this API key. Treat as unavailable and fail open: allow the call rather than failing the customer's request.
post/v1/identify

Tell MarginFuse who a customer is and what they pay

Sets the plan a customer is on, and optionally a display name, an email and short metadata labels that segment policies can match on.

plan is the key of a plan declared in Settings, not a Stripe price. MarginFuse derives revenue from that plan's price for every period, which is what makes margin per customer, margin policies and plan budgets work with no revenue source connected. Every figure built that way is labeled as a declared price, because nobody confirmed collection: the margin is an upper bound.

Safe to call on every sign-in. Sending the plan the customer is already on changes nothing. Sending a different one ends the current cycle at that moment, prorates the revenue accrued so far, and starts the new plan from there. Pass periodStart when the customer has been paying since an earlier date, and clearPlan to take them off plans entirely.

Once a revenue source bills this customer, that subscription is the truth and this call is refused with provider_subscription_present - the application can keep calling it unchanged.

There is no field for anything but identity here: no prompts, no content, no arbitrary payload.

Request body

IdentifyRequest fields
FieldTypeRequired
customerIdstring, 1..256 charsyes
planstring, 0..64 chars
clearPlanboolean
periodStartstring, date-time
namestring, 0..200 chars
emailstring, 0..320 chars
metadataobject

200 response

Identify fields
FieldTypeRequired
customerIdstringyes
planstring,nullyes
periodStartstring
periodEndstring

Status codes

  • 200The customer's plan after the call.
  • 401Missing, malformed, or unrecognized API key.
  • 404No declared plan in this project has that key.
  • 409The customer could not be described: its id is waiting in the mapping queue, or a revenue-source subscription already bills it.
  • 422The body did not match the schema. issues lists up to 10 problems.
  • 429Rate limit exceeded for this API key.
post/v1/decisions/{id}/ack

Report what your application did with a decision

Closes the loop on a decision. This is evidence, not bookkeeping: MarginFuse will not count an avoided provider call as realized savings without the application confirming the call did not occur.

presented_topup deliberately does not earn realized savings on its own. The customer may have paid and proceeded, so the call may well have happened.

Request body

AckRequest fields
FieldTypeRequired
acknowledgment"proceeded_as_requested" | "used_downgrade_model" | "presented_topup" | "blocked_before_provider_call" | "failed_to_apply"yes

200 response

AckResponse fields
FieldTypeRequired
okbooleanyes

Status codes

  • 200Recorded.
  • 401Missing, malformed, or unrecognized API key.
  • 404No decision with this id belongs to the authenticated project.
  • 422The body did not match the schema.
  • 429Rate limit exceeded for this API key.
post/webhooks/revenuecat/{projectId}no auth

Receive a RevenueCat event for one project

Where RevenueCat delivers purchases, renewals, cancellations and refunds. Not for application traffic: the URL and its secret are generated in Settings → Integrations and pasted into RevenueCat's webhook configuration.

Authenticated by the shared secret MarginFuse minted for this project, sent in the Authorization header exactly as RevenueCat was configured to send it. MarginFuse stores only its hash and compares in constant time. A project that does not exist and a wrong secret are both answered 401, so this endpoint cannot be used to discover project ids.

Answers 200 for events it deliberately ignores as well as for those it records, so RevenueCat does not retry a subscriber alias notification forever. A purchase becomes revenue for the period it paid for, counted as the store's proceeds; the periodic read then replaces that row with the store's own arithmetic, keyed by the same store transaction id so it is never counted twice.

Status codes

  • 200Recorded, or deliberately ignored. Either way, do not retry.
  • 401Missing or wrong webhook secret, or no such project.
  • 422The body carried no readable event.
get/openapi.jsonno auth

This document

The machine-readable contract, served by the same deployment that enforces it. Unauthenticated, so an SDK author or an agent can read the API before being given a key.

Status codes

  • 200An OpenAPI 3.1 document.
get/healthno auth

Liveness, including a database round trip

Unauthenticated. Returns 503 when the database is unreachable, so a rotated credential or a dead pooler shows up here and not only as failing traffic.

200 response

Health fields
FieldTypeRequired
okbooleanyes
dbbooleanyes

Status codes

  • 200Serving traffic.
  • 503The database could not be reached.
NextWhat it sendsEvery field on the wire, and why prompts cannot be among them.