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 · POST | Your amount. sourceType: 4 + amount. No Enopsy plan required. |
|---|---|
| 02 · GET URL | redirectUrl. Hosted checkout. Open it, email it, or put it on a button. |
| 03 · PAID | One-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.
| 1 | Your app POSTs the amount to Enopsy. |
|---|---|
| 2 | Enopsy encrypts and saves the link. |
| 3 | You send the customer the returned redirectUrl. |
| 4 | The customer opens the Payment App and pays. Stripe charges your account. |
| 5 | After 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
sourceType: 3 needs an Enopsy plan slug. Custom amount is sourceType: 4 — you send the rupees/dollars yourself. No plan form, no slug.
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.
?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: productOrigin | Mongo 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
| Field | One-time | Recurring |
|---|---|---|
sourceType | 4 | 4 |
billingType | 1 (or omit — treated as one-time) | 2 |
amount | What Stripe charges now | What Stripe charges now (first invoice) |
recurringDetails | Not used | Required: interval + recurringAmount |
| After pay | Paid 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=…"
}
}
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
| Field | Meaning |
|---|---|
billingInterval | Months between charges: 1, 3, 6, 12, 24, 36 (monthly → 3-year). |
recurringAmount | What later cycles charge. If omitted, Enopsy uses the first amount. |
isOnTrial | true if the first checkout should be a trial. |
trialPeriod.durationInDays | Trial length. First renewal is after this many days, then every billingInterval months. |
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).
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> |
|---|---|
| Redirect | window.location.href = json.data.redirectUrl; |
| Share | Paste into email, WhatsApp, SMS, or a QR that encodes the same URL. |
What the customer sees
- Opens
https://<your>.payment.enopsy.xyz/pay?payload=… - Page decrypts the payload: amount, currency, your logo, Stripe publishable key, one-time vs recurring.
- Enters card (Stripe Elements — card number never hits Enopsy).
- If the bank wants 3-D Secure, Stripe shows the challenge, then Enopsy confirms.
- 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" })
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) | |
|---|---|---|
| Stripe | One PaymentIntent on your account | First PaymentIntent now; later off-session charges on the saved card |
| Invoice | Created, marked Paid | First invoice Paid; later invoices on each renewal |
| Transaction | Yes — PaymentIntent / Charge ids | Yes, per charge |
| Order | No | Yes — Active, with nextBillingDate |
| Subscription doc | No | No (custom path). Schedule lives on the Order |
| Your bank | Stripe 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.
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.
redirectUrl → customer pays → records appear. Amount is yours. Plan catalog is not involved.
7. Reference — Field List, curl, and Failures
| Field | Req | Notes |
|---|---|---|
paymentSourceDetails | yes | Object: { "sourceType": 4 } — not an array. |
productOrigin | yes | Project ObjectId. |
amount + currency | yes | Charge now. Currency code + symbol. |
billingType | recurring | 1 = one-time, 2 = recurring. |
recurringDetails | if 2 | billingInterval 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
| Symptom | Cause |
|---|---|
| Origin not found / Unauthorized Tenant | Missing or unregistered Origin header (do not put origin in the JSON body). |
sourceType is required / 400 | paymentSourceDetails 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 found | Id is not a project in this tenant DB. |
| Link expired | Past expiresAt (default 60 min). POST again. |
