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
Comision și sumă netă
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.
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.
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.
/subscriptions/{subscription}/distributionActualizează 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ă
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.
/productsCreează un produs cu name și description opțional. GET /products și GET /products/{product} permit listarea paginată și citirea.
/products/{product}/updateActualizează name, description sau active. Pentru arhivare folosește /products/{product}/archive cu body gol. Toate operațiile POST cer Idempotency-Key.
| Caracteristică | Sandbox | Live |
|---|---|---|
| per_unit | Cantitate × preț unitar | Trimite amount; nu trimite tiers. |
| volume | Toată cantitatea folosește treapta în care se încadrează | Preț unitar × cantitate + flat_amount al acelei trepte. |
| graduated | Fiecare segment folosește propriul tarif | Suma segmentelor utilizate și a flat_amount pentru fiecare segment utilizat. |
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ă.
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.
/subscriptions/{subscription}/changes/{change}/cancelRetrage 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ă | Sandbox | Live |
|---|---|---|
| incomplete | Prima plată nu este confirmată | Nu acorda acces pe baza creării abonamentului. |
| active | Perioadă plătită | Folosește entitled și access_until din răspunsul API. |
| past_due | Rata curentă necesită intervenție | Creează checkout pentru documentul de încasare restant. |
| canceled | Nu vor mai exista reînnoiri | Perioada deja plătită rămâne în access_until. |
| review_required | O verificare de siguranță a oprit automatizarea | Rezolvă configurația sau cere reconciliere; nu crea o plată duplicată. |
/pricesCatalog cu `limit` între 1 și 100 și `cursor`. Aceeași paginare se aplică abonamentelor și documentelor de încasare.
/prices/{price}/archiveArhivează oferta fără a modifica sumele sau perioadele deja contractate.
/subscriptions/{subscription}/invoicesReturnează 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.
/subscriptions/{subscription}/syncSolicită 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.
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ă.