Mergi la conținut

Billing API

Abonamente, scadențe și recuperarea plăților

Calendarul și încasarea sunt controlate de Monedo. Creează produse și oferte lunare sau anuale, cu preț per unitate, pe volum sau pe trepte progresive. Fiecare abonament are o singură monedă, un preț versionat și o cantitate între 1 și 10.000.

Configurații acceptate

Instalațiile `direct` încasează pentru un comerciant. Instalațiile `platform` declară explicit vânzătorul și o distribuție către unul sau mai mulți beneficiari. Același motor de comisionare deservește plățile unice și abonamentele: comerciantul suportă procesarea și tariful Monedo. Pentru plățile directe, recuperarea poate folosi rezervă și regularizare; pentru distribuții, comisionul este reținut din alocările comercianților înainte de transfer. Răspunderea pentru sold negativ nu se modifică automat. Calculul automat al taxelor, consumul contorizat, trial-urile și cupoanele nu sunt incluse.

Comision și sumă netă

Pentru RON, tariful actual este costul efectiv de procesare + 0,30 RON per plată reușită, inclusiv fiecare reînnoire. Afișează clientului comerciantul real și suma integrală, nu costurile tehnice ale încasării. În rapoartele comerciantului, folosește un singur „Comision Monedo”.

fees.status este pending până când costul real este disponibil, apoi final. fees.commission conține comisionul total final, nu rezerva. fees.collected este suma efectiv reținută după regularizare, când este disponibilă; fees.prior_recovery include diferențe din plăți anterioare, iar fees.outstanding indică diferența generată de această plată care trebuie recuperată. Acesta din urmă este un snapshot, nu soldul curent al comerciantului. Nu interpreta lipsa datelor de cost ca fiind comision zero. Regularizarea costurilor raportate ulterior poate dura câteva zile.

O plată cu rezervă de comision are sumă fixă. Modificarea sumei, autorizarea incrementală sau captura parțială sunt refuzate cu fee_amount_immutable; anulează autorizarea și creează o plată nouă pentru suma finală. Fluxurile cu distribuții și captură variabilă își păstrează propriul contract de alocare.

javascript
const price = await monedo.prices.create({
  name: 'Plan lunar', amount: 4900, currency: 'ron', interval: 'month',
}, { idempotencyKey: 'plan-monthly-v1' });
const subscription = await monedo.subscriptions.create({
  price: price.id, customer: customer.id, quantity: 1,
}, { idempotencyKey: 'customer-1042-subscription' });
const checkout = await monedo.checkoutSessions.create({
  subscription: subscription.id,
  success_url: 'https://app.example/subscription-result',
  cancel_url: 'https://app.example/plans',
}, { idempotencyKey: 'customer-1042-subscription-checkout' });

`amount` este un întreg în unități monetare minime: 4.900 înseamnă 49,00 RON. Termenii prețului sunt imuabili. Arhivează un preț și creează altul pentru oferte noi; arhivarea nu schimbă abonamentele existente. Crearea abonamentului pregătește plata, dar nu încasează automat prima rată.

Abonamente cu mai mulți beneficiari

O instalație platform trebuie să trimită distribution la crearea abonamentului. merchant_of_record este ID-ul Monedo al recipientului vânzător, inclus în alocări cu rolul merchant. Folosește ID-uri din aceeași instalație și același mediu, nu identificatori ai procesatorului. Cheia necesită payments:write și transfers:write, iar comercianții trebuie să aibă granturi active.

javascript
const subscription = await monedo.subscriptions.create({
  price: 'price_lunar', customer: 'cus_client',
  distribution: {
    merchant_of_record: 'acct_vanzator',
    allocations: [
      { recipient: 'acct_vanzator', role: 'merchant', basis_points: 8000 },
      { recipient: 'acct_dezvoltator', role: 'developer', basis_points: 2000 },
    ],
  },
}, { idempotencyKey: 'abonament-client-42' });
// Creează checkoutSessions pentru subscription.id, ca la abonamentele directe.
// Checkout-ul afișează identitatea vânzătorului, nu firma dezvoltatorului.

Alocările trebuie să însumeze 10.000 de puncte de bază, adică 100% din suma brută. Motorul păstrează fiecare ban prin rotunjire deterministă. Comisionul total este dedus proporțional doar din alocările cu rolul merchant, o singură dată pentru fiecare plată. Rolul developer este rezervat firmei care deține aplicația; partea sa nu este comisionul Monedo.

POST
/subscriptions/{subscription}/distribution
payments:write + transfers:write

Actualizează regulile pentru facturile de abonament create ulterior. Trimite distribution, expected_version și Idempotency-Key. expected_version se referă la distribution_version, nu la terms_version.

Fiecare plată inițială, reînnoire sau diferență pozitivă de preț primește un snapshot imuabil. Schimbarea distribuției nu modifică ciclurile deja începute, inclusiv cele care așteaptă autentificare bancară. Schimbarea vânzătorului sau a modelului de încasare necesită abonament nou și consimțământ nou.

Plată încasată nu înseamnă distribuție finalizată

Acordă acces pe baza stării abonamentului și a perioadei plătite. Urmărește separat allocation_group, distribution_version și settlement_status din subscriptions.invoices. Dacă un transfer eșuează, motorul reia numai operațiile nefinalizate; nu creezi altă plată sau alte transferuri. Folosește operațiunile și evenimentele de alocare existente pentru retry și diagnostic.

Pentru rambursare folosește refunds.create cu plata ciclului. Motorul generic oprește distribuțiile neexecutate și inversează transferurile necesare înainte de rambursare. Nu iniția și inversări manuale pentru aceeași rambursare. Costurile nerecuperabile ale procesării rămân în responsabilitatea comercianților conform politicii de comisionare; comisionul nu este restituit automat. Anularea abonamentului oprește viitoarele încasări, fără refund implicit și fără anularea distribuțiilor pentru perioade deja plătite.

Produse și prețuri dinamice

Catalogul Billing aparține instalației și mediului autentificat; nu este catalogul comercial din API-ul Commerce. Un produs grupează oferte, iar un preț păstrează condițiile financiare. Redenumirea sau arhivarea produsului nu schimbă acordurile deja active. Arhivarea blochează vânzări noi; reînnoirile contractate continuă până la anulare.

POST
/products
payments:write

Creează un produs cu name și description opțional. GET /products și GET /products/{product} permit listarea paginată și citirea.

POST
/products/{product}/update
payments:write

Actualizează name, description sau active. Pentru arhivare folosește /products/{product}/archive cu body gol. Toate operațiile POST cer Idempotency-Key.

CaracteristicăSandboxLive
per_unitCantitate × preț unitarTrimite amount; nu trimite tiers.
volumeToată cantitatea folosește treapta în care se încadreazăPreț unitar × cantitate + flat_amount al acelei trepte.
graduatedFiecare segment folosește propriul tarifSuma segmentelor utilizate și a flat_amount pentru fiecare segment utilizat.
javascript
const product = await monedo.products.create({ name: 'Workspace' },
  { idempotencyKey: 'workspace-product' });
const priceData = {
  product: product.id, name: 'Workspace lunar', currency: 'ron', interval: 'month',
  pricing_model: 'graduated',
  tiers: [
    { up_to: 10, unit_amount: 1000, flat_amount: 0 },
    { up_to: null, unit_amount: 800, flat_amount: 0 },
  ],
};
const price = await monedo.prices.create(priceData, { idempotencyKey: 'workspace-v1' });
const quote = await monedo.prices.quote(price.id, { quantity: 12 },
  { idempotencyKey: 'workspace-quote-12' });
// quote.amount === 11600: 10 × 1000 + 2 × 800.
const subscription = await monedo.subscriptions.create({
  customer: customer.id, price_data: priceData, quantity: 12,
}, { idempotencyKey: 'workspace-subscription-customer-42' });
// Folosește price SAU price_data, niciodată ambele.

Limitele up_to sunt crescătoare, ultima trebuie să fie null. Sunt permise cel mult 50 de trepte; sumele sunt întregi nenegativi, iar totalul final trebuie să fie între 1 și 99.999.999 unități monetare minime și să acopere comisionul. flat_amount omis înseamnă zero. interval_count acceptă 1–12. Prețul calculat prin /prices/{price}/quote este informativ și nu debitează clientul.

Schimbări de plan cu acordul clientului

Citește terms_version înainte de modificare. Creează o propunere cu expected_version, prețul dorit, cantitatea și effective. O singură propunere poate fi deschisă per abonament. Propunerea nu schimbă planul și nu autorizează încasarea; expiră în cel mult 15 minute dacă nu este acceptată.

javascript
const current = await monedo.subscriptions.retrieve(subscription.id);
const change = await monedo.subscriptions.changes.create(current.id, {
  price: newPrice.id, quantity: 20, expected_version: current.terms_version,
  effective: 'immediate', // 'next_cycle' pentru reducere sau schimbare lunar/anual
}, { idempotencyKey: 'upgrade-request-1042' });
// amount: noul total recurent; amount_due: diferența de achitat acum.
const consent = await monedo.checkoutSessions.create({
  subscription: current.id, change: change.id,
  success_url: 'https://app.example/subscription-result',
  cancel_url: 'https://app.example/plans',
}, { idempotencyKey: 'upgrade-consent-1042' });
// Trimite clientul la consent.checkout_url. Nu confirma plata din backend.
// Urmărește subscription.change.applied și recitește abonamentul.
await monedo.subscriptions.changes.list(current.id, { limit: 20 });

immediate păstrează frecvența și perioada plătită. Diferența este rotunjită în sus la unitatea monetară minimă: (noul total − totalul curent) × timpul rămas / durata perioadei. Nu se calculează asupra perioadelor rambursate sau disputate. Termenii se aplică numai după acord și confirmarea integrală a diferenței. O diferență prea mică pentru comision se refuză; programează schimbarea la scadență.

next_cycle păstrează accesul curent și nu încasează imediat. Reducerile, schimbările de cantitate și trecerea lunar/anual intră în vigoare la reînnoire după acord. Un checkout fără sumă de plată afișează confirmarea schimbării, nu o confirmare falsă de încasare. Moneda nu se schimbă implicit și nu se generează credite sau refunduri automate.

POST
/subscriptions/{subscription}/changes/{change}/cancel
payments:write

Retrage o schimbare neaplicată cu body gol. O plată în procesare trebuie clarificată înainte de anulare; o diferență deja plătită trebuie reconciliată. Nu este un endpoint de refund.

subscription_version_conflict cere o nouă citire și o nouă propunere, nu retry orb. subscription_change_pending cere finalizarea sau retragerea propunerii anterioare. change_expired cere o propunere nouă. După acceptare, next_cycle rămâne programat chiar dacă URL-ul checkout expiră. Retragerea abonamentului anulează și schimbările încă neaplicate.

Checkout-ul afișează suma și frecvența și cere acord explicit pentru salvarea metodei și reînnoire. Fără acest acord nu există debitări recurente. Calendarul folosește UTC și păstrează ziua de ancorare: 31 ianuarie devine ultima zi din februarie, apoi 31 martie.

CaracteristicăSandboxLive
incompletePrima plată nu este confirmatăNu acorda acces pe baza creării abonamentului.
activePerioadă plătităFolosește entitled și access_until din răspunsul API.
past_dueRata curentă necesită intervențieCreează checkout pentru documentul de încasare restant.
canceledNu vor mai exista reînnoiriPerioada deja plătită rămâne în access_until.
review_requiredO verificare de siguranță a oprit automatizareaRezolvă configurația sau cere reconciliere; nu crea o plată duplicată.
GET
/prices
payments:read

Catalog cu `limit` între 1 și 100 și `cursor`. Aceeași paginare se aplică abonamentelor și documentelor de încasare.

POST
/prices/{price}/archive
payments:write

Arhivează oferta fără a modifica sumele sau perioadele deja contractate.

GET
/subscriptions/{subscription}/invoices
payments:read

Returnează documentele de încasare `subscription_invoice`, identificatorul plății și starea. Aceste înregistrări NU sunt facturi fiscale numerotate; folosește API-ul Fiscal pentru documentele legale.

POST
/subscriptions/{subscription}/sync
payments:write

Solicită verificarea durabilă a stării, inclusiv după corectarea unei configurații. Întoarce `202` și un obiect `operation`; urmărește-l cu `waitForOperation`. Nu ocolește limitele sau verificările de securitate.

javascript
const invoices = await monedo.subscriptions.invoices(subscription.id, { limit: 20 });
const failed = invoices.data.find(item => item.status === 'failed' && item.billing_kind === 'cycle');
if (failed) {
  const recovery = await monedo.checkoutSessions.create({
    subscription: subscription.id, invoice: failed.id,
    success_url: 'https://app.example/subscription-result',
    cancel_url: 'https://app.example/plans',
  }, { idempotencyKey: 'recover-' + failed.id });
  // Prezintă recovery.checkout_url clientului; nu crea alt PaymentIntent.
}
await monedo.subscriptions.cancel(subscription.id,
  { at_period_end: true }, { idempotencyKey: 'cancel-' + subscription.id });
// Înainte de scadență poți retrage anularea programată:
await monedo.subscriptions.resume(subscription.id,
  { idempotencyKey: 'resume-' + subscription.id });

O rată refuzată nu este debitată repetat în fundal. Recuperarea folosește aceeași plată în checkout, inclusiv pentru autentificarea suplimentară cerută de bancă. O întrerupere prelungită nu declanșează automat încasarea retroactivă a perioadelor expirate. Anularea nu rambursează; rambursarea se face separat prin motorul protejat de refunds.

Abonează webhook-ul la `subscription.updated`, `subscription.invoice.paid` și `subscription.invoice.failed`. Persistă rapid evenimentul, deduplică după ID, apoi citește abonamentul. Evenimentele pot fi reordonate; `access_until` din citirea curentă este sursa pentru acces, nu un redirect sau un mesaj vechi.

Pentru schimbări poți selecta subscription.change.accepted, subscription.change.awaiting_payment, subscription.change.applied, subscription.change.canceled și subscription.change.expired. accepted confirmă acordul, nu aplicarea planului; applied confirmă termenii noi. subscription.updated include terms_version. billing_kind distinge încasarea unui ciclu de diferența de upgrade; subscription_invoice rămâne o înregistrare de încasare, nu o factură fiscală.