NullSpend Docs

Margins API

Dashboard endpoints for customer margin analytics.

Margins API

Dashboard endpoints for customer margin analytics. All endpoints require session authentication.

Tier requirement: All margins endpoints (/api/margins, /api/margins/:customer, /api/margins/unmatched, /api/customer-mappings) and Stripe revenue-sync (/api/stripe/connect, /api/stripe/disconnect, /api/stripe/revenue-sync) are gated to Pro tier and above. Free-tier orgs receive 403 forbidden with an upgrade pointer.

GET /api/margins

Returns the margin table for a given period.

Query Parameters:

ParamTypeDefaultDescription
periodstringCurrent monthCalendar month in YYYY-MM format
formatstringjsonResponse format. csv returns a downloadable CSV file.

Response (JSON):

{
  "data": {
    "summary": {
      "blendedMarginPercent": 42.5,
      "totalRevenueMicrodollars": 500000000,
      "totalCostMicrodollars": 287500000,
      "criticalCount": 1,
      "atRiskCount": 2,
      "lastSyncAt": "2026-04-05T10:00:00.000Z",
      "syncStatus": "active",
      "skippedCurrencies": { "eur": 3 }
    },
    "customers": [
      {
        "stripeCustomerId": "cus_abc123",
        "customerName": "Acme Corp",
        "avatarUrl": null,
        "tagValue": "acme-corp",
        "revenueMicrodollars": 100000000,
        "costMicrodollars": 30000000,
        "marginMicrodollars": 70000000,
        "marginPercent": 70,
        "healthTier": "healthy",
        "sparkline": [
          { "period": "2026-02", "marginPercent": 65 },
          { "period": "2026-03", "marginPercent": 68 },
          { "period": "2026-04", "marginPercent": 70 },
          { "period": "2026-05", "marginPercent": 72, "projected": true }
        ],
        "projectedTierWorsening": false,
        "budgetSuggestionMicrodollars": null
      }
    ]
  }
}

Response (CSV):

Returns Content-Type: text/csv with Content-Disposition: attachment; filename="margins-2026-04.csv".

Columns: Customer, Stripe ID, Tag Value, Revenue ($), Cost ($), Margin (%), Margin ($), Health Tier.

Errors:

StatusCodeWhen
400validation_errorInvalid period format
401authentication_requiredNo session
403forbiddenInsufficient role

GET /api/margins/:customer

Returns detailed margin data for a single customer.

Path Parameters:

ParamTypeDescription
customerstringURL-encoded tag value (e.g., acme-corp)

Query Parameters:

ParamTypeDefaultDescription
periodstringCurrent monthCalendar month in YYYY-MM format

Response:

{
  "data": {
    "stripeCustomerId": "cus_abc123",
    "customerName": "Acme Corp",
    "avatarUrl": null,
    "tagValue": "acme-corp",
    "healthTier": "healthy",
    "marginPercent": 70,
    "revenueMicrodollars": 100000000,
    "costMicrodollars": 30000000,
    "revenueOverTime": [
      { "period": "2026-02", "revenue": 90000000, "cost": 25000000 },
      { "period": "2026-03", "revenue": 95000000, "cost": 28000000 },
      { "period": "2026-04", "revenue": 100000000, "cost": 30000000 }
    ],
    "modelBreakdown": [
      { "model": "gpt-4o", "cost": 20000000, "requestCount": 150 },
      { "model": "gpt-4o-mini", "cost": 10000000, "requestCount": 800 }
    ]
  }
}

Errors:

StatusCodeWhen
404not_foundCustomer mapping not found

GET /api/margins/unmatched

Returns unmatched Stripe customers, unmapped cost tags, and pending auto-matches for the mapping management UI.

Response:

{
  "data": {
    "unmatchedStripeCustomers": [
      {
        "stripeCustomerId": "cus_xyz",
        "customerName": "BetaCo",
        "customerEmail": "billing@beta.co",
        "totalRevenueMicrodollars": 50000000
      }
    ],
    "unmappedTagValues": [
      {
        "tagValue": "gamma-inc",
        "totalCostMicrodollars": 15000000,
        "requestCount": 200
      }
    ],
    "pendingAutoMatches": [
      {
        "id": "uuid",
        "stripeCustomerId": "cus_def",
        "customerName": "Delta LLC",
        "tagValue": "cus_def",
        "confidence": 0.9
      }
    ],
    "customerNames": {
      "cus_abc": "Acme Corp",
      "cus_def": "Delta LLC"
    }
  }
}

POST /api/stripe/connect

Connect a Stripe restricted key. Validates the key with a minimal Stripe API call before storing.

Request Body:

{
  "stripeKey": "rk_live_..."
}

Validation:

  • Key must start with rk_ (restricted key). sk_test_ is allowed in non-production environments.
  • Key is tested with stripe.customers.list({ limit: 1 }).

Response (201):

{
  "data": {
    "id": "uuid",
    "keyPrefix": "rk_live_abcd...wxyz",
    "status": "active",
    "createdAt": "2026-04-05T10:00:00.000Z"
  }
}

Errors:

StatusCodeWhen
400validation_errorMissing or invalid key format
400stripe_validation_failedKey doesn't authenticate with Stripe
409conflictStripe already connected (disconnect first)

DELETE /api/stripe/disconnect

Removes the Stripe connection and cascades: deletes all revenue data and customer mappings for the org.

Response:

{
  "data": { "deleted": true }
}

GET /api/stripe/revenue-sync

Triggers a revenue sync. Called by Vercel Cron (with Bearer CRON_SECRET) or manually from the dashboard (with session auth, requires member role).

Cron Response:

{
  "data": { "synced": 5, "errors": 0 }
}

Manual Response:

{
  "data": {
    "orgId": "uuid",
    "customersProcessed": 12,
    "periodsUpdated": 15,
    "autoMatchesCreated": 2,
    "invoicesFetched": 48,
    "invoicesSkipped": 3,
    "skippedCurrencies": { "eur": 3 },
    "durationMs": 4521
  }
}

Customer Mappings

GET /api/customer-mappings

Returns all customer-to-tag mappings for the org.

POST /api/customer-mappings

Create or update a mapping. Upserts on (orgId, stripeCustomerId, tagKey).

Request Body:

{
  "stripeCustomerId": "cus_abc123",
  "tagValue": "acme-corp",
  "tagKey": "customer",
  "matchType": "manual"
}
FieldRequiredDefaultDescription
stripeCustomerIdYesStripe customer ID
tagValueYesCost event tag value
tagKeyNo"customer"Tag key (almost always customer)
matchTypeNo"manual""manual" or "auto"

DELETE /api/customer-mappings?id=UUID

Delete a mapping by ID. Returns 404 if not found or not owned by the org.


On this page