Elixpo Pay

Elixpo Docs

Dashboard

Catalog sync

Products and pricing tiers are managed from your code, not the dashboard. You keep a catalog file in your repo and push it with your secret key — so pricing is versioned, reviewable, and the same across environments. The dashboard shows the result read-only.

1. Declare your catalog

Commit a payouts.catalog.json describing each product (by tier) and its regional prices. unit_amount is in minor units (paise/cents). The optional app block sets your homepage and pricing links (shown on your product page).

json
{
  "app": {
    "homepage_url": "https://blogs.elixpo.com",
    "pricing_url":  "https://blogs.elixpo.com/pricing"
  },
  "products": [
    {
      "tier": "member",
      "name": "Blogs Member",
      "description": "Member-only reads, higher limits…",
      "prices": [
        // One-time payment — buyer manually re-purchases each cycle.
        { "nickname": "India",  "currency": "INR", "unit_amount": 19900, "interval": "month", "region": "IN", "type": "one_time" },

        // Autopay (recurring) — Razorpay charges automatically each cycle.
        // The buyer is redirected to Razorpay's hosted mandate page on
        // first checkout; subsequent renewals are silent.
        { "nickname": "India · Autopay",  "currency": "INR", "unit_amount": 19900, "interval": "month", "region": "IN", "type": "recurring" },

        { "nickname": "Global", "currency": "USD", "unit_amount":   600, "interval": "month", "type": "one_time" }
      ]
    }
  ]
}

2. Push it with your secret key

http
POST https://payouts.elixpo.com/v1/sync
Authorization: Bearer <ELIXPO_PAY_API_KEY>
Content-Type: application/json

<the contents of payouts.catalog.json>

A tiny script makes it a one-liner you can run in CI or by hand:

javascript
// scripts/sync-catalog.mjs
import { readFile } from "node:fs/promises";

const catalog = JSON.parse(await readFile("payouts.catalog.json", "utf8"));
const res = await fetch("https://payouts.elixpo.com/v1/sync", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.ELIXPO_PAY_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ products: catalog.products }),
});

// /v1/sync returns HTTP 200 even when a product is rejected — you MUST
// inspect the body. Trusting the status code alone silently no-ops the sync.
const out = await res.json();
if (!res.ok || out.ok === false || (out.errors?.length ?? 0) > 0) {
  console.error("catalog sync failed:", JSON.stringify(out.errors ?? out));
  process.exit(1);
}
console.log(`synced ${out.synced?.length ?? 0} products`);

How sync reconciles

  • Each product upserts by (app, tier) — same tier updates in place.
  • Prices reconcile by (currency, region, interval) — matching prices update, new ones are added.
  • An active price that's no longer in the file is deactivated (never hard-deleted, so history stays intact).
  • Changing a recurring price's unit_amount or interval re-mints the Razorpay plan automatically (provider plans are immutable). New checkouts use the new amount; subscriptions already active keep billing their original amount until they renew or cancel.
  • Send a single product as the bare object, or many under products.

Check the response

/v1/sync returns HTTP 200 even when a product fails validation — always inspect the body, and treat ok: false or a non-empty errors array as a failure. Trusting the status code alone silently no-ops the sync.

json
// Success
{ "ok": true, "app": "blogs", "synced": [ { "product": { "tier": "member" }, "prices": [ … ] } ], "errors": [] }

// Rejected — still HTTP 200, but ok:false with per-product errors
{ "ok": false, "synced": [], "errors": [
  { "tier": null, "error": "invalid_tier", "error_description": "product.tier required (a-z 0-9 _)" }
] }

Read it back

Render your pricing page from the live catalog — public, no secret needed:

http
GET https://payouts.elixpo.com/v1/catalog?app=<your-client-id>

QuickstartCheckout sessions