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
400request malformat (ex. niciun fisier la upload)401cheia API lipseste, are format gresit sau a fost revocata402sold insuficient in wallet403nu ai permisiunea pentru aceasta resursa404resursa nu exista422validare esuata (ex. parametru lipsa, brand interzis pe site)429prea multe request-uri (rate limit)500eroare server (ne contactezi pe articole@psk.ro)
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
| Parametru | Tip | Descriere |
|---|---|---|
page | optional | Pagina (min 1, implicit 1) |
per_page | optional | Rezultate pe pagina (1 pana la 200, implicit 50) |
da_min, da_max | optional | Filtru Domain Authority (0 pana la 100) |
category | optional | ID categorie (intreg) |
search | optional | Cautare dupa domeniul publicatiei |
price_max | optional | Pret maxim EUR pe cea mai ieftina oferta a publicatiei |
updated_since | optional | Doar 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:
Articol fara brandcontinut editorial cu link, fara numele brandului (profil natural de link-uri)Articol cu brandnumele si link-urile tale in articolArticol cu promovarepublicare plus vizibilitate in homepage-ul publicatieiInserare linkinserarea unui link intr-un articol EXISTENT al publicatiei (veziinsertion_urlla POST /orders)Brand mentionmentionarea brandului intr-un articol existent al publicatiei- Nise speciale, acceptate doar de publicatiile listate cu ele:
Articol Bet/Casino,Crypto/Forex/IFN/Banci,Articol Videochat,Articol Sex Shop,Bauturi alcoolice
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:
docx_uploadincarci un fisier .docx (acum sau ulterior, prin endpoint-ul de upload)drive_linklink Google Drive cu acces public la documentul .docxredactare_750articol redactat de noi (750 de cuvinte)redactare_1000articol redactat de noi (1000 de cuvinte)
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)
| Parametru | Tip | Descriere |
|---|---|---|
product_id | obligatoriu | ID-ul ofertei (campul offers[].id). Alias acceptat: offer_id |
article_source | obligatoriu* | Una din: docx_upload, drive_link, redactare_750, redactare_1000. *NU se trimite la ofertele Inserare link / Brand mention |
insertion_url | conditionat | Doar la ofertele Inserare link / Brand mention: URL-ul articolului existent al publicatiei in care se face inserarea. Alias acceptat: existing_article_url |
cover_source | conditionat | URL-ul sursei imaginii de cover. Obligatoriu cand publicatia cere imagini (max_images > 0); altfel primesti 422 COVER_SOURCE_REQUIRED. Alias acceptat: image_source |
project_id | optional | Proiectul caruia ii apartine comanda (trebuie sa fie al tau) |
drive_url | conditionat | Obligatoriu daca article_source = drive_link. Alias acceptat: drive_link |
article_file | optional | Fisier .doc/.docx, max 10 MB (doar pe multipart) |
images | optional | Array de imagini, max 10, jpg/jpeg/png/webp/gif, max 5 MB fiecare (doar pe multipart) |
homepage, brand, dofollow | optional | Boolean, optiuni pe articol (daca oferta le accepta). Addon-urile cu pret ale publicatiilor nu sunt disponibile prin API; se comanda din platforma |
promoted_keyword | optional | Cuvant cheie promovat (max 200) |
promoted_url | optional | URL promovat. Alias acceptat: target_url |
keywords | optional | Array (max 10) de { "anchor": "...", "url": "..." } |
notes | optional | Instructiuni pentru redactor (max 2000). Devine primul mesaj pe comanda |
external_reference | optional | Referinta ta interna (max 128). Folosita si pentru idempotenta |
webhook_url | optional | URL webhook specific acestei comenzi (altfel se foloseste cel din cont) |
idempotency_key | optional | Cheie 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
| Parametru | Tip | Descriere |
|---|---|---|
page | optional | Pagina (min 1, implicit 1) |
per_page | optional | 1 pana la 100, implicit 25 |
status | optional | Una din: pending, processing, published, failed, rejected |
project_id | optional | Filtru pe proiect |
updated_since | optional | Doar 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
| Camp | Tip | Descriere |
|---|---|---|
files[] | obligatoriu | Unul 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
| Parametru | Tip | Descriere |
|---|---|---|
message | obligatoriu | Text, 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
| Parametru | Tip | Descriere |
|---|---|---|
page | optional | Pagina (min 1, implicit 1) |
per_page | optional | 1 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
| Parametru | Tip | Descriere |
|---|---|---|
amount_net_eur | obligatoriu | Suma 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
| Parametru | Tip | Descriere |
|---|---|---|
page | optional | Pagina (min 1, implicit 1) |
per_page | optional | 1 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
| Parametru | Tip | Descriere |
|---|---|---|
name | obligatoriu | Numele proiectului (max 255) |
url | obligatoriu | Domeniul 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
order.createdcomanda a fost creataorder.status_changedstatusul public s-a schimbat (includeold_statussinew_status)order.changes_requesteds-au cerut modificari la comanda; includemessage(textul cu ce trebuie modificat). Statusul public ramaneprocessing. Raspunzi prinPOST /orders/{id}/messagessi/sau re-incarci articolul cuPOST /orders/{id}/upload(campfiles[]).
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
| Parametru | Tip | Descriere |
|---|---|---|
page | optional | Pagina (min 1, implicit 1) |
per_page | optional | 1 pana la 100, implicit 25 |
status | optional | Una 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