Developer
enopsy.com
DocumentationDeveloperCustom Payment Links

Take Any Amount With a Payment Link

How anyone can POST a custom amount to /public-api/payment-links — one-time or recurring — then send the customer the URL that comes back. They pay on Enopsy's hosted Payment App. Money lands in your Stripe.

Integrator guide, document version 1.0.

01 · POSTYour amount. sourceType: 4 + amount. No Enopsy plan required.
02 · GET URLredirectUrl. Hosted checkout. Open it, email it, or put it on a button.
03 · PAIDOne-time or renew. billingType 1 = once. 2 = Enopsy renews on the saved card.

1. How It Works — One Endpoint, a URL, Then They Pay

You do not create an order first. You ask Enopsy for a checkout link with the amount you want. The customer pays on that URL. Enopsy writes the records after Stripe succeeds.

1Your app POSTs the amount to Enopsy.
2Enopsy encrypts and saves the link.
3You send the customer the returned redirectUrl.
4The customer opens the Payment App and pays. Stripe charges your account.
5After success, Enopsy writes an Invoice + Transaction (plus an Order if recurring).

Endpoint: POST https://backend-billing.enopsy.com/api/v1/public-api/payment-links

sourceType: 4 = custom amount · billingType: 1 = one-time · billingType: 2 = recurring

This is not a plan purchase. sourceType: 3 needs an Enopsy plan slug. Custom amount is sourceType: 4 — you send the rupees/dollars yourself. No plan form, no slug.
This is not "create order." The POST only returns a URL. The order/invoice appears after the customer pays. To create a pending order without charging, that is a different API (POST /webhook/orders).

What you do with the URL

Copy redirectUrl → put it on a button, email, WhatsApp, or QR → customer pays → money lands in your Stripe.

Do not build the ?payload= yourself. Always use the URL Enopsy returns.

2. Before You Call — Two Things the Request Must Carry

The public API has no bearer token. Enopsy finds your tenant from the browser origin, then finds which storefront from productOrigin.

Header: Origin (or Host)Your registered storefront domain, e.g. https://app.yourproduct.com. If this does not match, you get Unauthorized Tenant. Server-to-server: send the same header yourself.
Body: productOriginMongo ObjectId of the project in Enopsy. Required for custom links. Get it once with GET /api/v1/public-api/tenant/fetch-tenant?origin=https://app.yourproduct.com — keep data._id, that is productOrigin.

Also required on the tenant

  • Stripe gateway (secret + publishable keys) attached to that project — Enopsy decrypts the publishable key into the checkout page.
  • Payment App base URL on the project (redirect.billing_URL, e.g. yourbrand.payment.enopsy.xyz).

Minimal headers

POST /api/v1/public-api/payment-links
Origin: https://app.yourproduct.com
Content-Type: application/json
x-tenant-id is not used here. This public route is origin-routed. Tenant-id auth is for /webhook/* and admin. If you call from Postman, set Origin (or Host) to a domain that is registered on the tenant.

Switch: one-time vs recurring

FieldOne-timeRecurring
sourceType44
billingType1 (or omit — treated as one-time)2
amountWhat Stripe charges nowWhat Stripe charges now (first invoice)
recurringDetailsNot usedRequired: interval + recurringAmount
After payPaid invoice + transaction. No order.Active order + paid invoice + transaction. Later renewals on saved card.

3. One-Time — Charge a Custom Amount Once

Use this for deposits, invoices you priced yourself, "pay ₹X now", event tickets — anything that should not renew.

POST /api/v1/public-api/payment-links
{
  "paymentSourceDetails": { "sourceType": 4 },
  "productOrigin": "6512f0aae91…",
  "amount": 2500,
  "currency": { "code": "INR", "symbol": "₹" },
  "billingType": 1,
  "description": "Website setup — Acme Pvt Ltd",
  "additionalInfo": {
    "additionalDescription": "Acme Pvt Ltd · SO-1042"
  }
}

Optional line items

If you send items[], Enopsy uses their totals. If amount is also sent and > 0, amount wins as the charge. If items is empty, one line is synthesised from description + amount.

{
  "items": [
    { "name": "Design", "unitPrice": 1500, "quantity": 1, "totalAmount": 1500 },
    { "name": "Hosting (year)", "unitPrice": 1000, "quantity": 1, "totalAmount": 1000 }
  ],
  "amount": 2500
}

What comes back — copy this URL

{
  "success": true,
  "data": {
    "slug": "paylink-…",
    "status": 1,
    "statusLabel": "active",
    "amount": { "value": 2500, "currency": { "code": "INR" } },
    "totalAmount": 2500,
    "expiresAt": "2026-09-07T12:00:00.000Z",
    "redirectUrl": "https://yourbrand.payment.enopsy.xyz/pay?payload=…"
  }
}
Give the customer data.redirectUrl. That page is the Payment App: card fields (Stripe Elements), 3-D Secure, then a success screen. Default link life is 60 minutes (expiresInMinutes on the body, or env PAYMENT_LINK_TTL_MINUTES).

4. Recurring — Charge a Custom Amount, Then Renew It

Same endpoint. Set billingType: 2 and send the interval. There is still no Enopsy plan — you own the amount. After the first successful pay, Enopsy keeps an Order and re-charges the saved card on the schedule.

POST /api/v1/public-api/payment-links
{
  "paymentSourceDetails": { "sourceType": 4 },
  "productOrigin": "6512f0aae91…",
  "amount": 99,
  "currency": { "code": "USD", "symbol": "$" },
  "billingType": 2,
  "description": "Acme Pvt Ltd — monthly retainer",
  "recurringDetails": {
    "billingInterval": 1,
    "recurringAmount": 99,
    "isOnTrial": false,
    "trialPeriod": { "durationInDays": 0 }
  },
  "additionalInfo": {
    "additionalDescription": "Acme Pvt Ltd"
  }
}

recurringDetails

FieldMeaning
billingIntervalMonths between charges: 1, 3, 6, 12, 24, 36 (monthly → 3-year).
recurringAmountWhat later cycles charge. If omitted, Enopsy uses the first amount.
isOnTrialtrue if the first checkout should be a trial.
trialPeriod.durationInDaysTrial length. First renewal is after this many days, then every billingInterval months.
First charge vs renewals. amount is what Stripe takes now on the Payment App. recurringAmount is what Enopsy's daily job charges later on the saved card. They can differ (e.g. setup now, lower monthly after).
No Stripe Subscription. Enopsy stores an Order with recurringDetails and a next billing date. A platform job calls Stripe off_session on that date. You will not see a Subscription object in Stripe.

Custom recurring links do not create an Enopsy Subscription document (that is plan-only). Renewals still run off the Order.

5. The URL — What To Do With redirectUrl

The Payment App is already built. Your job is to get the customer onto that URL before it expires.

Button<a href="{redirectUrl}">Pay now</a>
Redirectwindow.location.href = json.data.redirectUrl;
SharePaste into email, WhatsApp, SMS, or a QR that encodes the same URL.

What the customer sees

  1. Opens https://<your>.payment.enopsy.xyz/pay?payload=…
  2. Page decrypts the payload: amount, currency, your logo, Stripe publishable key, one-time vs recurring.
  3. Enters card (Stripe Elements — card number never hits Enopsy).
  4. If the bank wants 3-D Secure, Stripe shows the challenge, then Enopsy confirms.
  5. Success page: /payment-success?summaryRef=…

Tiny integration (browser on your domain)

async function chargeCustom({ amount, billingType, interval, description }) {
  const res = await fetch(BILLING + "/public-api/payment-links", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      paymentSourceDetails: { sourceType: 4 },
      productOrigin: PRODUCT_ORIGIN_ID,
      amount,
      currency: { code: "USD", symbol: "$" },
      billingType,                           // 1 or 2
      description,
      ...(billingType === 2 ? {
        recurringDetails: { billingInterval: interval, recurringAmount: amount }
      } : {}),
    }),
  });
  const json = await res.json();
  window.location.href = json.data.redirectUrl;
}

// one-time  → chargeCustom({ amount: 2500, billingType: 1, description: "Setup" })
// monthly   → chargeCustom({ amount: 99, billingType: 2, interval: 1, description: "Retainer" })
Idempotency (optional). Send idempotencyKey (your own unique string). A retry with the same key returns the existing link instead of minting another.

6. After They Pay — What Enopsy Writes, and Where the Money Is

One-time (billingType 1)Recurring (billingType 2)
StripeOne PaymentIntent on your accountFirst PaymentIntent now; later off-session charges on the saved card
InvoiceCreated, marked PaidFirst invoice Paid; later invoices on each renewal
TransactionYes — PaymentIntent / Charge idsYes, per charge
OrderNoYes — Active, with nextBillingDate
Subscription docNoNo (custom path). Schedule lives on the Order
Your bankStripe payout on Stripe's schedule (often 2–7 days). Enopsy never holds the funds.

Where to look

  • Enopsy → Invoices — branded PDF, amount, customer.
  • Enopsy → Transactions — Stripe ids, success/fail.
  • Enopsy → Orders — only for recurring custom links.
  • Stripe Dashboard — the actual money and payouts.
Abandoned 3-D Secure. If the customer starts the card challenge and leaves, no invoice/order is created. Generate a new link and send it again — the old one may still be valid until expiresAt.

Optional: tell your own backend

If a webhook URL is configured on the project, Enopsy POSTs events such as payment.succeeded after the charge. Delivery is fire-and-forget — a failure on your side does not undo Stripe.

Remember: POST link → open redirectUrl → customer pays → records appear. Amount is yours. Plan catalog is not involved.

7. Reference — Field List, curl, and Failures

FieldReqNotes
paymentSourceDetailsyesObject: { "sourceType": 4 } — not an array.
productOriginyesProject ObjectId.
amount + currencyyesCharge now. Currency code + symbol.
billingTyperecurring1 = one-time, 2 = recurring.
recurringDetailsif 2billingInterval in months (1, 3, 6, 12, 24, 36).

Copy-paste curl (Origin is a header, not JSON)

paymentSourceDetails must be an object { }, never an array [ ]. Open data.redirectUrl from the JSON response.

# one-time — billingType 1
curl -sS -X POST "https://backend-billing.enopsy.com/api/v1/public-api/payment-links" \
  -H "Origin: https://marketxy.com" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentSourceDetails": { "sourceType": 4 },
    "productOrigin": "6a180fcc94ba01d45a468f39",
    "amount": 10,
    "currency": { "code": "USD", "symbol": "$" },
    "billingType": 1,
    "description": "One-time custom test"
  }'

# recurring monthly — billingType 2
curl -sS -X POST "https://backend-billing.enopsy.com/api/v1/public-api/payment-links" \
  -H "Origin: https://marketxy.com" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentSourceDetails": { "sourceType": 4 },
    "productOrigin": "6a180fcc94ba01d45a468f39",
    "amount": 10,
    "currency": { "code": "USD", "symbol": "$" },
    "billingType": 2,
    "description": "Monthly custom test",
    "recurringDetails": { "billingInterval": 1, "recurringAmount": 10 }
  }'

If it fails

SymptomCause
Origin not found / Unauthorized TenantMissing or unregistered Origin header (do not put origin in the JSON body).
sourceType is required / 400paymentSourceDetails sent as an array. Use an object.
HTML <title>Error</title>Request never reached the API as JSON — extra Postman Host / cookie headers. Prefer curl.
Product origin not foundId is not a project in this tenant DB.
Link expiredPast expiresAt (default 60 min). POST again.