Articole PSK API v1 · documentatie publica inapoi la platforma

API v1 Articole PSK

API-ul iti permite sa plasezi comenzi de articole pe publicatii partenere, sa urmaresti statusul, sa incarci continut, sa gestionezi wallet-ul si proiectele, si sa fii notificat automat prin webhook cand articolul este publicat.

Base URL: https://articole.psk.ro/api/v1

Toate raspunsurile sunt JSON. Toate sumele monetare sunt in EUR net (fara TVA), unde nu se specifica altfel. Datele de tip moment in timp apar atat ca timestamp Unix (intreg) cat si ca string ISO 8601 UTC.

HEADER Autentificare

Toate request-urile catre API trebuie sa contina header-ul:

Authorization: Bearer psk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Cheia se genereaza din contul tau, sectiunea API. La generare se afiseaza o singura data, salveaz-o intr-un loc sigur. Daca o pierzi, poti regenera cheia (cea veche devine invalida instant). Cheia incepe mereu cu prefixul psk_.

Erori

Erorile au format consistent:

{
  "error": "Mesaj uman-friendly",
  "code": "MACHINE_READABLE_CODE"
}

Coduri HTTP

Rate limiting

60 request-uri pe minut per cheie. Cand depasesti limita primesti HTTP 429 cu header Retry-After (secunde de asteptare). Fiecare raspuns include header-ul X-Request-Id pentru tracing, il poti trimite si tu in request ca sa corelezi log-urile.

GET /me

Verificare rapida a cheii (sanity check). Returneaza datele de baza ale contului autentificat. Util ca prim apel cand integrezi.

Raspuns 200

{
  "id": 123,
  "email": "client@exemplu.ro",
  "name": "Nume client",
  "company": "Firma SRL",
  "class": "extern",
  "payment_method": "card",
  "wallet_balance_eur": 240.50,
  "webhook_url": "https://site-ul-tau.ro/webhook",
  "webhook_configured": true,
  "api_key_created_at": 1716200000,
  "api_key_last_used_at": 1716800000
}

GET /products

Lista publicatiilor partenere, grupate: o publicatie este un obiect cu un array offers[] (fiecare oferta este un tip de articol pe acea publicatie). Preturile sunt afisate pe tier-ul contului tau. Nu expunem costul de achizitie, email-ul publicatiei sau notele interne.

Parametri query

ParametruTipDescriere
pageoptionalPagina (min 1, implicit 1)
per_pageoptionalRezultate pe pagina (1 pana la 200, implicit 50)
da_min, da_maxoptionalFiltru Domain Authority (0 pana la 100)
categoryoptionalID categorie (intreg)
searchoptionalCautare dupa domeniul publicatiei
price_maxoptionalPret maxim EUR pe cea mai ieftina oferta a publicatiei
updated_sinceoptionalDoar publicatiile actualizate dupa aceasta data

Raspuns 200

{
  "data": [
    {
      "id": 88,
      "name": "exemplu.ro",
      "website": "exemplu.ro",
      "da": 42, "dr": 38, "spam_score": 2,
      "referring_domains": 1200, "traffic": 85000,
      "category_id": 5, "category_name": "Stiri",
      "is_active": true,
      "picture_format": "16:9",
      "diacritics_required": true,
      "updated_at": "2026-05-20T08:00:00Z",
      "offers": [
        {
          "id": 311,
          "article_type": "Articol cu brand",
          "article_type_id": 57,
          "price": 75.00, "currency": "EUR",
          "accepts_brand": true, "accepts_homepage": false,
          "dofollow_links": 2, "nofollow_links": 0,
          "max_images": 3, "turnaround_days": null,
          "requirements": "...", "requirements_en": "...",
          "sponsorized_label": "Articol sponsorizat",
          "is_active": true
        }
      ]
    }
  ],
  "meta": { "current_page": 1, "last_page": 12, "per_page": 50, "total": 580 }
}

GET /products/{id}

Detaliile unei singure publicatii cu toate ofertele ei. {id} este ID-ul publicatiei (campul id din lista de mai sus). Raspunde 404 daca publicatia nu exista sau nu are oferte active. Structura obiectului este identica cu un element din data[] la GET /products.

Tipuri de articol

Campul offers[].article_type contine numele tipului, exact cum apare in platforma. Tipurile curente:

Lista completa si actuala o ai mereu in raspunsul de la GET /products: numele vine direct din platforma, nu dintr-o lista statica.

POST /orders

Plaseaza o comanda de articol pe o oferta. Suporta 4 surse pentru articol:

Oferte de tip Inserare link / Brand mention: nu exista articol de livrat, deci article_source NU se trimite. In schimb trimiti insertion_url (URL-ul articolului EXISTENT al publicatiei in care se face inserarea) si, pentru inserare link, keywords cu ancora si linkul tau. Reguli de validare, aplicate la plasare: articolul tinta trebuie sa fie pe domeniul publicatiei comandate; articolele care sunt ele insele advertoriale sau publicari platite ale altor clienti sunt refuzate (422, INSERTION_TARGET_BLOCKED); daca o ancora nu apare in textul articolului tinta, comanda se creeaza si primesti insertion_warnings in raspuns: publicatia va adauga o fraza noua care contine ancora.

Parametri body (JSON sau multipart/form-data)

ParametruTipDescriere
product_idobligatoriuID-ul ofertei (campul offers[].id). Alias acceptat: offer_id
article_sourceobligatoriu*Una din: docx_upload, drive_link, redactare_750, redactare_1000. *NU se trimite la ofertele Inserare link / Brand mention
insertion_urlconditionatDoar la ofertele Inserare link / Brand mention: URL-ul articolului existent al publicatiei in care se face inserarea. Alias acceptat: existing_article_url
cover_sourceconditionatURL-ul sursei imaginii de cover. Obligatoriu cand publicatia cere imagini (max_images > 0); altfel primesti 422 COVER_SOURCE_REQUIRED. Alias acceptat: image_source
project_idoptionalProiectul caruia ii apartine comanda (trebuie sa fie al tau)
drive_urlconditionatObligatoriu daca article_source = drive_link. Alias acceptat: drive_link
article_fileoptionalFisier .doc/.docx, max 10 MB (doar pe multipart)
imagesoptionalArray de imagini, max 10, jpg/jpeg/png/webp/gif, max 5 MB fiecare (doar pe multipart)
homepage, brand, dofollowoptionalBoolean, optiuni pe articol (daca oferta le accepta). Addon-urile cu pret ale publicatiilor nu sunt disponibile prin API; se comanda din platforma
promoted_keywordoptionalCuvant cheie promovat (max 200)
promoted_urloptionalURL promovat. Alias acceptat: target_url
keywordsoptionalArray (max 10) de { "anchor": "...", "url": "..." }
notesoptionalInstructiuni pentru redactor (max 2000). Devine primul mesaj pe comanda
external_referenceoptionalReferinta ta interna (max 128). Folosita si pentru idempotenta
webhook_urloptionalURL webhook specific acestei comenzi (altfel se foloseste cel din cont)
idempotency_keyoptionalCheie unica (max 64). Acelasi key in 24h returneaza comanda existenta

Idempotenta

Daca trimiti din nou un idempotency_key folosit in ultimele 24 de ore, sau un external_reference deja existent, nu se creeaza o comanda noua: primesti comanda existenta cu "idempotent_replay": true.

Raspuns 201

{
  "order": { "id": 9001, "status": "processing", "price": 75.00, "currency": "EUR", ... },
  "next_steps": {
    "type": "upload_required",
    "message": "Incarca articolul (.docx) si imaginile prin POST /api/v1/orders/9001/upload ..."
  }
}

Campul next_steps.type poate fi: upload_required (sursa docx_upload), drive_pending (sursa drive_link, descarcam noi documentul), sau in_progress (redactare de catre echipa noastra).

GET /orders

Lista comenzilor tale, paginata, cele mai noi primele.

Parametri query

ParametruTipDescriere
pageoptionalPagina (min 1, implicit 1)
per_pageoptional1 pana la 100, implicit 25
statusoptionalUna din: pending, processing, published, failed, rejected
project_idoptionalFiltru pe proiect
updated_sinceoptionalDoar comenzile cu activitate dupa aceasta data

Raspuns 200

{
  "data": [
    {
      "id": 9001, "master_id": "...", "project_id": 4,
      "offer_id": 311, "publication": "exemplu.ro", "website": "exemplu.ro",
      "status": "published", "status_internal": "complete",
      "price": 75.00, "currency": "EUR", "total_eur": 75.00,
      "external_reference": "ref-123",
      "order_date": 1716200000, "created_at": "2026-05-20T08:00:00Z",
      "updated_at": "2026-05-22T10:00:00Z",
      "published_url": "https://exemplu.ro/articol", "publish_url": "https://exemplu.ro/articol",
      "published_at": "2026-05-22T10:00:00Z", "publish_date": 1716372000
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 40, "last_page": 2 }
}

GET /orders/{id}

Detaliile complete ale unei comenzi. Pe langa campurile din lista, raspunsul include si cover_source, image_source, article_type, keywords, target_url, insertion_url, insertion_warnings, notes si webhook_url. Raspunde 404 daca comanda nu exista sau nu iti apartine.

POST /orders/{id}/upload

Incarca fisierele unei comenzi (articol .docx si/sau imagini). Folosit mai ales pentru comenzile cu sursa docx_upload. Cand articolul .docx ajunge, comanda avanseaza automat la review.

Request: multipart/form-data

CampTipDescriere
files[]obligatoriuUnul sau mai multe fisiere. Acceptate: .doc, .docx, .jpg, .jpeg, .png. Max 10 MB fiecare, max 5 fisiere per request

Raspuns 200

{
  "uploaded": [
    { "id": 5001, "media_type": "document", "filename": "articol.docx", "url": "https://articole.psk.ro/storage/orders/9001/..." }
  ],
  "order_status": "processing"
}

GET /orders/{id}/messages

Mesajele de pe comanda (conversatia cu echipa). Mesajele interne nu sunt expuse, la fel ca in interfata clientului.

Raspuns 200

{
  "data": [
    { "from": "Nume client", "message": "Va rog sa folositi tonul formal.", "date": 1716200100 }
  ]
}

POST /orders/{id}/messages

Adauga un mesaj pe comanda. Echipa noastra este notificata pe email.

Parametri body

ParametruTipDescriere
messageobligatoriuText, intre 1 si 5000 de caractere

Raspuns 201

{ "status": "sent", "date": 1716200200 }

GET /wallet

Soldul portofelului. TVA-ul se aplica in functie de profilul tau fiscal.

Raspuns 200

{
  "balance_net_eur": 240.50,
  "balance_gross_eur": 291.01,
  "currency": "EUR",
  "apply_vat": true
}

GET /wallet/transactions

Istoricul tranzactiilor din wallet, paginat.

Parametri query

ParametruTipDescriere
pageoptionalPagina (min 1, implicit 1)
per_pageoptional1 pana la 100, implicit 25

Raspuns 200

{
  "data": [
    { "id": 12, "type": "credit", "amount_net_eur": 100.00, "description": "Alimentare card", "order_id": null, "created_at": 1716200000 }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 12, "last_page": 1 }
}

POST /wallet/topup

Initiaza o alimentare de wallet prin card. Raspunsul contine datele de plata: faci un HTTP POST catre action_url cu campurile din transfer_data (form-urlencoded). Dupa plata, banca confirma prin IPN si wallet-ul se crediteaza automat.

Parametri body

ParametruTipDescriere
amount_net_eurobligatoriuSuma neta in EUR, intre 20 si 5000

Raspuns 200

{
  "master_order_id": 7700,
  "amount_net_eur": 100.00,
  "amount_gross": 121.00,
  "currency": "RON",
  "payment": {
    "action_url": "https://...netopia-payments...",
    "transfer_data": { "...": "..." },
    "method": "POST",
    "note": "Trimite POST catre action_url cu campurile din transfer_data (form-urlencoded)."
  }
}

GET /projects

Lista proiectelor tale, paginata.

Parametri query

ParametruTipDescriere
pageoptionalPagina (min 1, implicit 1)
per_pageoptional1 pana la 100, implicit 50

Raspuns 200

{
  "data": [
    { "id": 4, "name": "Campanie primavara", "url": "exemplu.ro", "description": null }
  ],
  "meta": { "page": 1, "per_page": 50, "total": 4, "last_page": 1 }
}

POST /projects

Creeaza un proiect nou. Proiectele grupeaza comenzile (le poti referi prin project_id la POST /orders).

Parametri body

ParametruTipDescriere
nameobligatoriuNumele proiectului (max 255)
urlobligatoriuDomeniul promovat, fara protocol (ex. exemplu.ro)

Raspuns 201

{ "id": 5, "name": "Campanie vara", "url": "exemplu.ro", "description": null }

Datele de facturare per proiect (billing override) se configureaza din interfata platformei, nu prin API.

GET /projects/{id}

Detaliile unui proiect. Raspunde 404 daca proiectul nu exista sau nu iti apartine.

Webhook-uri

Cand statusul unei comenzi se schimba, iti trimitem un POST catre URL-ul tau, ca sa nu fie nevoie de polling. Configurezi webhook_url si webhook_secret in contul tau (sectiunea API), sau trimiti un webhook_url per comanda la POST /orders.

Evenimente

Livrare si retry

Webhook-urile sunt trimise de un proces care ruleaza in fiecare minut. Pe raspuns 5xx sau timeout (10 secunde) reincercam cu backoff: dupa 1 minut, 5 minute, apoi 30 de minute. Dupa 3 incercari esuate, livrarea e marcata failed. Fiecare request poarta header-ele X-PSK-Signature, X-PSK-Event si X-PSK-Delivery-Id.

Exemplu payload

{
  "event": "order.status_changed",
  "order_id": 9001,
  "master_id": "...",
  "status": "published",
  "status_internal": "complete",
  "old_status": "processing",
  "new_status": "published",
  "project_id": 4,
  "publication": "exemplu.ro",
  "website": "exemplu.ro",
  "price": 75.00, "currency": "EUR", "total_eur": 75.00,
  "external_reference": "ref-123",
  "published_url": "https://exemplu.ro/articol",
  "publish_url": "https://exemplu.ro/articol",
  "published_at": "2026-05-22T10:00:00Z",
  "timestamp": "2026-05-22T10:00:01Z"
}

GET /webhooks/deliveries

Istoricul livrarilor de webhook, util pentru debugging (vezi de ce a esuat un webhook anume).

Parametri query

ParametruTipDescriere
pageoptionalPagina (min 1, implicit 1)
per_pageoptional1 pana la 100, implicit 25
statusoptionalUna din: pending, success, failed

Raspuns 200

{
  "data": [
    {
      "id": 41, "event": "order.status_changed", "order_id": 9001,
      "webhook_url": "https://site-ul-tau.ro/webhook",
      "status": "success", "http_status": 200, "attempts": 1,
      "response_body": "ok",
      "created_at": 1716372001, "delivered_at": 1716372002, "next_attempt_at": 1716372001,
      "payload": { "...": "..." }
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 41, "last_page": 2 }
}

Verificare semnatura webhook

Fiecare cerere webhook trimisa de noi include header-ul X-PSK-Signature cu valoarea sha256=<hex>, calculata ca HMAC-SHA256 al body-ului folosind webhook_secret din contul tau.

Exemplu PHP:

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PSK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $WEBHOOK_SECRET);
if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}
// ... payload-ul este de incredere