Un API dummy, compatibil cu documentația oficială eMAG & FD Marketplace API v4.5.2 (https://marketplace-api.emag.ro/api-doc), pentru a-ți testa integrările fără a lovi producția.
- Toate cele 41 de endpoint-uri din specificația oficială (produse/oferte, comenzi, AWB, retururi, categorii, campanii, facturi).
- Validare automată pe schemele OpenAPI oficiale (
spec/openapi.jsone copia exactă a specificației publice), plus regulile de business descrise în documentație (tranziții de status, câmpuri obligatorii, limite). - UI web cu jurnalul live al request-urilor trimise prin el (request/response, note de validare, „Copy as cURL”) și generatoare de date dummy pe fiecare secțiune.
- Totul e persistent în SQLite (
data/dummy-emag.sqlite): colecțiile, setările și fiecare request cu body-ul complet (ultimele 20 000, body-uri până la 1 MB — configurabil, vezi Expunere publică). - UI protejat prin cont (primul cont se poate înregistra, apoi înregistrarea se închide) și import read-only din API-ul real eMAG, cu credențiale folosite doar în memorie și niciodată salvate.
- Fără dependențe externe: doar Node.js ≥ 22.16 (folosește
node:sqlite, inclus în Node).
npm start # http://127.0.0.1:3030
PORT=4000 npm start # alt port
HOST=0.0.0.0 npm start # expune și în rețea (UI-ul cere cont; API-ul dummy acceptă orice Basic auth)
npm test # 60 de teste (API, SQLite, autentificare, import, securitate)Prima pornire: deschide UI-ul și creezi contul (username ≥ 3 caractere, parolă ≥ 8). Este singurul cont care se va putea înregistra vreodată; după el, register răspunde 403. Parola e stocată cu scrypt, sesiunea e un cookie HttpOnly; SameSite=Strict (în baza de date se păstrează doar SHA-256 al token-ului), loginul eșuat e limitat (5 încercări / minut). Dacă îți uiți parola, șterge rândul din tabelul users (sqlite3 data/dummy-emag.sqlite "DELETE FROM users") și te înregistrezi din nou.
În integrarea ta setează:
| Base URL | http://127.0.0.1:3030/api-3 (la fel ca https://marketplace-api.emag.ro/api-3) |
| Autentificare | HTTP Basic. Implicit orice credențiale sunt acceptate; din Settings poți trece pe mod strict (doar utilizatorii definiți, altfel 401) |
| UI | http://127.0.0.1:3030/ |
curl -u user:parola -X POST http://127.0.0.1:3030/api-3/order/read \
-H 'Content-Type: application/json' -d '{"status":1,"currentPage":1}'Căile din spec care apar deja prefixate cu /api-3 (facturi, smart-deals) funcționează atât ca /api-3/invoice/read, cât și ca /api-3/api-3/invoice/read.
- Deschide UI-ul → secțiunea dorită (ex. Orders) → Generate dummy data → „Orders” (alegi număr, status, plată…).
- Pornește integrarea ta împotriva Base URL-ului de mai sus: citește comenzi, confirmă (
/order/acknowledge), emite AWB etc. - În Requests vezi live fiecare apel: metodă, status, durată, body-ul trimis, răspunsul și notele de validare (ex. „filtrele nu trebuie împachetate în
data”, „string numeric convertit”). - Acțiuni pe rând (tabelele din secțiuni): aprobă/respinge documentația unei oferte, clientul anulează comanda, avansează statusul AWB-ului, pornește/oprește o campanie etc. — multe declanșează și callback-uri.
| Secțiune | Endpoint-uri | Generatoare | Acțiuni pe rând |
|---|---|---|---|
| Products & Offers | product_offer/read·save·count, offer/save, offer_stock/{id} (PATCH), documentation/find_by_eans, measurements/save |
oferte proprii, produse din catalogul eMAG (pt. atașare după EAN/PNK) | aprobă / respinge / blochează documentația, stoc 0 / +50, preț în afara intervalului |
| Orders | order/read·save·count, order/acknowledge/{id}, order/{id}/unlock-courier, order/attachments/read·save |
comenzi (status, tip 2/3, plată) | generează AWB dummy (+ PDF/ZPL), client anulează, marchează plătită, resetează la New, finalizează |
| Shipping | awb/read·save, awb/read_pdf, awb/read_zpl, locality/read·count, courier_accounts/read, addresses/read, awb/package/read·save, order/volumetry/read |
AWB-uri pentru comenzi, conturi curier, adrese | avansează statusul AWB (declanșează awb.status), descarcă eticheta PDF/ZPL |
| Returns | rma/read·save·count |
retururi din comenzi finalizate | confirmat / primit / anulat de client |
| Categories | category/read·count, vat/read, handling_time/read |
reset date de referință | — |
| Campaigns | campaigns/read, campaign_categories/read, campaigns/{id}/eligible-offers, campaign_proposals/save, smart-deals-price-check, offers/{id}/campaign_product_badge_timeline |
campanii (live/trecute/viitoare) cu oferte eligibile, unele cu susținere eMAG și altele fără, oferte eligibile pentru o campanie existentă (și importată), oferte în campanii | pornește / oprește campania; pe tab-ul Eligible offers: dă / scoate susținerea eMAG |
| Invoices | invoice/categories·read, customer-invoice/read |
facturi eMAG, facturi client (+ storno) | — |
| Callbacks | (eMAG → serverul tău) | — | retrimite un callback |
- Convenții de request:
save⇒ payload îndata(max. 50 elemente);read/count⇒ filtre top-level (datae ignorat și apare ca notă în jurnal); max. 4000 de variabile; chei case-sensitive. - Envelope:
{isError, messages, results}; lacount,results = {noOfItems, noOfPages, itemsPerPage}. Erori de validare ⇒ HTTP 200 +isError: true(elementul invalid nu se salvează, restul da); eroare de documentație la salvarea unui produs ⇒isError: truedar oferta se salvează (status 8 +doc_errors), exact ca în documentație. - Produse: draft (documentație incompletă), atașare la produs existent prin
part_number_key(status 9) sau EAN (status 3), caracteristici obligatorii/valori permise per categorie,offer_validation_status1/2 dinmin/max_sale_price,start_dateprogramat (între mâine și +60 zile),part_numbercurățat de spații/,/;. Documentația nou trimisă trece 1 → 9 automat dupăautoApproveSeconds(implicit 20 s;0= manual din UI) și apoipart_number_keyapare în catalog. - Comenzi:
acknowledgee singura cale New → In progress; tranziții permise laorder/save(2→3→4, anulare cureason_cancellation, reactivare < 48 h), comenzi cu plată online card nu permit eliminarea/reducerea produselor, adăugare produs, comenzi finalizate (4) doar storno (total 4→5 sau parțial cuis_storno), comenziletype 2doar atașamente, atașamente doar.pdf. - AWB: validează comanda (tip 3; ca în eMAG real, fără restricții de status și fără limită de AWB-uri per comandă — se poate emite oricând, chiar dacă există deja unul, cu eMAG courier sau cu curierii contractuali), localități, cont de curier,
weight= suma pachetelor,envelope_number/parcel_number, reguli pentru AWB de retur; returneazăreservation_id+emag_id. Emiterea unui AWB pentru o comandă în status 1/2/3 o trece automat în status 4 (Finalizată) (comutator în Settings → Issuing an AWB finalizes the order; AWB-urile de retur nu schimbă comanda). - Etichete AWB dummy:
awb/read_pdf(A4/A5/A6) șiawb/read_zplîntorc o etichetă generată pe loc — curier, număr AWB, cod de bare Code 128 real, destinatar/expeditor, ramburs, colete/greutate, comandă. Din UI (Orders → deschizi o comandă): „Generate dummy AWB” emite un AWB fără să construiești payload-ul (orice stare a comenzii seller) și linkurile AWB PDF (A6/A4) / ZPL îl descarcă — generându-l la cerere dacă nu există încă (o singură dată per comandă). Aceleași linkuri există și pe rândurile din Shipping → AWBs. - Retururi: matricea de tranziții din documentație (ex. New → Finalized e respins).
- Rate limiting: 12 req/s la comenzi, 3 req/s restul (cumulat), 5 req/s
find_by_eans, și limitele pe minut; depășirea ⇒ HTTP 429{"message":"API rate limit exceeded"}+ headereleX-RateLimit-*. Cererile invalide se numără. Se poate opri din Settings. - Callback-uri (
order.new,order.cancel,rma.new,rma.status,awb.status,documentation.approved): configurezi URL-ul în Callbacks; dummy-ul facePOSTcu entitatea JSON ca body (+ headerX-Dummy-Emag-Event) și păstrează un jurnal de livrare. - Chaos / fault injection (Settings): latență, procent de erori 5xx aleatoare și reguli „pe endpoint” (ex.
/order/*→ 429 de 3 ori,/awb/save→ 500, 401…). Settings → strictRules: dezactivează regulile de business (rămâne doar validarea de schemă) dacă vrei să treci peste ele la un test.
Import din sidebar → alegi platforma (eMAG RO/BG/HU, Fashion Days RO/BG), introduci username/parola contului de API și ce vrei să aduci. Dummy-ul apelează API-ul real doar pe endpoint-uri de citire (allow-list în src/importer/client.js; orice altceva e refuzat în cod) și scrie datele în aceleași colecții pe care le servește apoi API-ul dummy, deci id-urile sunt cele reale.
| Sursă | Ce aduce | De ce ajută |
|---|---|---|
| VAT, handling time | vat_id-urile și valorile reale |
obligatorii la fiecare product_offer/save |
| Categorii | arborele real + opțional caracteristici, valori permise, family types (implicit doar pentru categoriile folosite de ofertele tale; valorile se aduc paginat) | validarea documentației ca în producție |
| Produse & oferte | ofertele tale cu documentația trimisă (+ completează catalogul dummy: atașare după EAN/PNK cu coduri reale) | teste cu date reale |
| Conturi curier, adrese, pachete AWB, localități | id-urile reale | awb/save cu valori valide |
| Campanii | campaniile active și regulile lor | testarea propunerilor |
| Comenzi, retururi (opt-in) | ultimele N zile | teste de sincronizare pe forme reale |
- Mod
merge(implicit în UI): upsert după id, restul rămâne. Modreplace: datele locale ale surselor alese sunt înlocuite de cele reale (ex. categoriile reale înlocuiesc pe cele dummy). - Respectă limitele eMAG (≈2,5 req/s, backoff la 429), progres live, anulare, istoric al importurilor. Contul tău trebuie să aibă IP-ul whitelisted, altfel eMAG răspunde 403 și vezi un mesaj explicit.
- Date personale: comenzile și retururile conțin date de clienți. Sunt oprite implicit, cer bifare explicită și vin anonimizate (nume, telefon, adrese, email, IBAN… înlocuite cu date fake; același client real ⇒ același client fake; orașul/județul/localitatea rămân pentru AWB-uri valide). Poți dezactiva anonimizarea doar în mod explicit.
- Credențialele nu se salvează NICIODATĂ: ajung o singură dată la serverul local, trăiesc doar într-un closure din memorie pe durata importului și sunt șterse la final (
dispose()); nu intră în baza de date, în jurnal, în starea sau istoricul job-ului, în mesaje de eroare, nici în browser (nu ajung în storage/URL/console, câmpurile se golesc imediat după trimitere). Rutele UI (unde ar trece parola) nu sunt înregistrate în jurnalul de request-uri. Un test automat rulează un import complet împotriva unui „eMAG” fals și caută parola, username-ul și base64-ul lor în baza de date, WAL, istoricul importurilor și jurnal. - Adresa de bază e fixată pe hosturile oficiale; un
baseUrlcustom e acceptat doar pentru loopback (teste).
Dummy-ul e gândit să stea permanent pe internet. Ce îl protejează și ce trebuie să faci tu la deploy:
Ce e deja în cod
- UI-ul cere cont (primul se înregistrează, apoi
register⇒ 403). Parole scrypt, sesiune în cookieHttpOnly; SameSite=Strict(+Securepe HTTPS), token hash-uit în DB, login limitat (5 încercări / minut / IP), verificareOriginpe orice request care modifică ceva. Headere de hardening pe UI:Content-Security-Policy(fără scripturi inline, doarself),X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,Cache-Control: no-storepe API-ul intern. - API-ul dummy (
/api-3) rămâne deschis prin design (orice Basic auth), de aceea tot ce poate face un necunoscut e plafonat:- protecție anti-flood per IP, care nu se poate opri din UI (
ABUSE_LIMIT_PER_SEC, implicit 100;ABUSE_LIMIT_PER_MIN, implicit 3000;0= oprit) ⇒ HTTP 429, fără să fie scrisă în jurnal; - limitele eMAG (3/12 req/s) se aplică pe IP + username, deci schimbarea username-ului nu resetează fereastra;
- body maxim 2 MB (
API_MAX_BODY_BYTES) ⇒ 413; - fiecare colecție acceptă maxim 20 000 de documente (
MAX_DOCS_PER_COLLECTION,0= nelimitat) ⇒ HTTP 507Storage limit reached; ștergi date din UI sau ridici limita; - jurnalul păstrează ultimele 20 000 de request-uri (
LOG_MAX_REQUESTS,0= nelimitat) și trunchiază body-urile peste 1 MB (LOG_BODY_MAX_BYTES), ca să nu umple discul.
- protecție anti-flood per IP, care nu se poate opri din UI (
- Callback-uri (anti-SSRF): când serverul e expus (
HOSTdiferit de loopback), URL-urile de callback pot ținti doar hosturi publicehttp(s); loopback, rețele private, link-local și metadata cloud (169.254.169.254) sunt refuzate, redirecturile nu sunt urmate. PeHOST=127.0.0.1callback-urile către localhost rămân permise. Forțezi cuCALLBACKS_ALLOW_PRIVATE=1/=0. - Importul din API-ul real apelează doar hosturile oficiale eMAG/FD, doar endpoint-uri de citire, fără a salva credențialele (vezi mai sus).
- Setările și snapshot-urile sunt protejate la prototype pollution; fișierele statice sunt servite doar din
public/(fără path traversal); timeouts pe cereri lente (30 s headere / 120 s request).
Ce trebuie să faci la deploy
- Pune-l în spatele unui reverse proxy cu TLS (Caddy / nginx / Cloudflare) — aplicația vorbește doar HTTP. Lasă
HOST=127.0.0.1dacă proxy-ul e pe aceeași mașină, altfelHOST=0.0.0.0+ firewall. - Setează
TRUST_PROXY=1doar dacă proxy-ul rescrieX-Forwarded-For/X-Forwarded-Proto; fără el, toate limitele per IP ar vedea IP-ul proxy-ului (și un atacator ar putea bloca login-ul tuturor), iar cookie-ul nu ar primiSecure. Poți forțaCOOKIE_SECURE=1. - Înregistrează contul imediat după prima pornire (până atunci oricine poate crea primul cont) și folosește o parolă lungă.
- Rulează sub un utilizator fără privilegii, cu
data/scris doar de acel utilizator; fă backup ladata/dummy-emag.sqlitedacă îți pasă de date. - Ține minte că tot ce intră în API-ul public e vizibil în UI: nu trimite date reale de clienți; folosește generatoarele sau importul anonimizat.
Variabile de mediu: PORT, HOST, DATA_DIR, TRUST_PROXY, COOKIE_SECURE, ABUSE_LIMIT_PER_SEC, ABUSE_LIMIT_PER_MIN, API_MAX_BODY_BYTES, MAX_DOCS_PER_COLLECTION, LOG_MAX_REQUESTS, LOG_BODY_MAX_BYTES, CALLBACKS_ALLOW_PRIVATE.
data/dummy-emag.sqlite (WAL): docs (documentele tuturor colecțiilor, JSON), meta (secvențe, setări), requests (fiecare apel API cu request/response, cu retenția din Expunere publică), users, sessions, import_runs. Datele stau și în memorie pentru viteză; orice modificare este scrisă în tranzacție (diff pe conținut) înainte ca răspunsul HTTP să plece, deci un apel confirmat este deja pe disc. „Clear log” din UI golește jurnalul; peste LOG_MAX_REQUESTS cele mai vechi intrări se șterg automat. Dacă ai folosit versiunea cu JSON, db.json și requests.json se migrează automat la prima pornire (originalele rămân ca *.migrated). Interogări directe: sqlite3 data/dummy-emag.sqlite "select method,path,status from requests order by id desc limit 20".
Specificația oficială publică nu descrie forma exactă a unor răspunsuri. Am ales forme rezonabile; unde integrarea ta e sensibilă, confirmă cu un apel real și ajustează modulul respectiv:
awb/save⇒results: [{order_id, reservation_id, emag_id}];awb/read_zpl⇒results: ["<base64>"]; statusurile curierului (1–7) sunt inventate.invoice/readșicustomer-invoice/read⇒resultse obiect{total_results, invoices[]}(conform descrierii);invoice/categories⇒ listă (doarFCe din exemplele oficiale, restul sunt inventate).vat/read⇒{vat_id, vat_rate:"0.2100", status};handling_time/read⇒{id, value}; id-urile de categorii/caracteristici/TVA/localități nu sunt cele din producție.order/read: pe lângă cheile din spec returnează și câmpuri întâlnite în API-ul real (payment_mode,shipping_*,part_number_key,vatca string…).campaign_categories/read: schema oficială nu menționează filtrul, dar API-ul real cerecampaignId(camelCase, obligatoriu; altfel HTTP 400 cumessages: {campaignId: "campaignId cannot be empty"}, observat la un import real) — dummy-ul se comportă la fel.eligible-offersadaugă cheia top-levelnext(URL pagina următoare). Ofertele fără susținere eMAG auemag_support: []; cele cu susținere au 1–3 praguri. Tab-ul Campaigns → Eligible offers e calculat live (read-only), exact ce răspunde endpoint-ul, pluskeyșihas_emag_support.- Textele mesajelor de eroare sunt formulate liber (în engleză), nu cele din producție. Nu se simulează filtrarea pe IP.
- Callback-urile trimit direct entitatea (ca la
*/read), fără alt „plic”.
server.js pornire HTTP
src/app.js asamblare: store + spec + setări + jurnal + module
src/core/ http pipeline, router, validator OpenAPI, SQLite (db.js, store.js), rate-limit, faults, request log
src/ui/ api.js (API intern /__ui/api/*), auth.js (conturi, sesiuni)
src/importer/ client read-only către API-ul real, surse de import, anonimizare, manager de job-uri
src/modules/<secțiune>/ index.js (rute + generatoare + acțiuni), service.js (logică), seed.js (date de referință)
public/ UI (HTML/CSS/JS vanilla, fără build)
spec/openapi.json specificația oficială v4.5.2
test/ teste de integrare (API, SQLite, autentificare, import + scanare de credențiale, securitate)
Adaugi o secțiune/endpoint nou creând src/modules/<nume>/index.js și înregistrându-l în src/modules/index.js. Un modul declară collections, routes (cu body/items/query = nume de schemă din spec sau schemă inline — validarea e automată), generators și rowActions; UI-ul le afișează automat din /__ui/api/meta.
Când eMAG publică o versiune nouă: înlocuiește spec/openapi.json (se extrage din pagina /api-doc, constanta spec din HTML) și rulează npm test.
Settings → Data: export/import de snapshot (colecții + setări, fără conturi/sesiuni), „Reset all data” (șterge colecțiile și reface datele de referință; conturile și jurnalul rămân). Pentru o pornire complet curată oprește serverul și șterge data/.