Skip to content

Frontend Developer Reference

Everything a Nuxt frontend developer needs to know about the Partizap backend API.

Base URL

All API requests go through the /api prefix. Nginx strips it before passing to PHP.

EnvironmentBase URL
Productionhttps://partizap.ru/api
Developmenthttps://dev.partizap.ru/api
Local devhttp://localhost:8000 (no /api prefix needed)

In nuxt.config.ts:

ts
runtimeConfig: {
  public: {
    apiBase: '/api',  // same-origin, nginx handles proxy
  },
},

Response Format

Success

json
{
  "data": { ... },
  "meta": { "has_more": true, "next_cursor": "..." }
}

meta only present on paginated list endpoints.

Error

json
{
  "error": {
    "code": "validation_error",
    "message": "Human-readable message",
    "details": { "email": ["Email is required"] }
  }
}

details only present on validation errors (422).

Error Codes

HTTPCodeWhen
401authentication_errorNot logged in or invalid credentials
403authorization_errorLogged in but not allowed
404not_found_errorResource doesn't exist
409conflict_errorDuplicate (e.g. email already registered)
422validation_errorInvalid input, details has per-field errors
429rate_limit_errorToo many requests, check Retry-After header

Authentication

Session-based with HTTP-only cookies (not JWT). The browser handles cookies automatically.

CSRF Protection

Every mutating request (POST/PUT/PATCH/DELETE) must include CSRF token:

  1. Read CSRF_TOKEN cookie (readable, not HTTP-only)
  2. Send as X-CSRF-TOKEN header
ts
// composables/useApi.ts
const csrfToken = useCookie('CSRF_TOKEN');

const response = await $fetch('/api/auth/login', {
  method: 'POST',
  body: { email, password },
  credentials: 'include',
  headers: {
    'X-CSRF-TOKEN': csrfToken.value ?? '',
  },
});

For SSR requests (server-side), forward browser cookies to the API:

ts
// In server routes or SSR composables
const headers = useRequestHeaders(['cookie']);
const data = await $fetch('/api/auth/me', {
  headers,
  credentials: 'include',
});
  • Name: PARTIZAP_SESSION
  • HTTP-only (JS cannot read it)
  • Secure (HTTPS only in production)
  • SameSite: Lax
  • Managed automatically by the browser

Rate Limiting

Global: 100 requests/minute per IP.

Response headers on every request:

  • X-RateLimit-Limit: 100
  • X-RateLimit-Remaining: N
  • X-RateLimit-Reset: <unix timestamp>

Auth-specific:

EndpointLimitWindowPer
POST /auth/login515 minemail
POST /auth/register31 hourIP
POST /auth/forgot-password31 houremail
POST /auth/verify-email515 minuser

On 429: Retry-After header contains seconds to wait.


Cursor Pagination

All list endpoints use cursor-based pagination (no offset/page numbers).

Query params:

  • limit (default: 20, max: 100)
  • cursor (opaque string, pass from previous response)

Usage:

ts
// First page
const { data, meta } = await $fetch('/api/store/products?limit=20');

// Next page
if (meta.has_more) {
  const page2 = await $fetch(`/api/store/products?limit=20&cursor=${meta.next_cursor}`);
}

Auth Endpoints

POST /api/auth/register

json
// Request
{ "email": "user@example.com", "password": "SecureP@ss1", "display_name": "John Doe" }

// Response 201 — session cookie set automatically
{ "data": { "id": 1, "email": "...", "display_name": "...", "email_verified": false, ... } }

Side effects: sends 6-digit email verification code (expires in 15 min).

POST /api/auth/login

json
// Request
{ "email": "user@example.com", "password": "SecureP@ss1" }

// Response 200
{ "data": { "id": 1, "email": "...", "display_name": "...", ... } }

Errors:

  • 401 "Invalid email or password" — wrong credentials (intentionally vague)
  • 401 "Account temporarily locked. Try again later." — 10+ failed attempts, 30-min lockout
  • 401 "Account is deactivated"is_active = false

Progressive delays: 2s after 4 attempts, 5s after 6, 10s after 8.

GET /api/auth/me

Returns current user if session is valid, 401 otherwise.

json
// Response 200
{ "data": { "id": 1, "email": "...", "display_name": "...", ... } }

POST /api/auth/verify-email (authenticated)

json
// Request
{ "code": "123456" }

// Response 200
{ "data": { "verified": true } }

POST /api/auth/resend-verification (authenticated)

json
// Response 200
{ "data": { "sent": true } }

Errors: 422 if email already verified or last code sent <60s ago.

POST /api/auth/forgot-password

json
// Request
{ "email": "user@example.com" }

// Response 200 — always succeeds (prevents email enumeration)
{ "data": { "sent": true } }

POST /api/auth/reset-password

json
// Request
{ "token": "<from email link>", "password": "NewSecureP@ss1" }

// Response 200
{ "data": { "reset": true } }

Side effect: invalidates ALL user sessions.

POST /api/auth/logout (authenticated)

Destroys current session only.

POST /api/auth/logout-all (authenticated)

Destroys all sessions for the user.


Password Validation Rules

The backend enforces these rules — mirror them in the frontend for UX:

  1. 8-128 characters
  2. At least 1 uppercase letter (A-Z)
  3. At least 1 lowercase letter (a-z)
  4. At least 1 digit (0-9)
  5. At least 1 special character (!@#$%^&* etc.)
  6. No 3+ consecutive identical characters (e.g. aaa)
  7. No 3+ sequential characters (e.g. abc, 321)
  8. Not a common password
  9. Doesn't contain the email local part

User Object

Private (returned to the user themselves):

json
{
  "id": 1,
  "email": "user@example.com",
  "display_name": "John Doe",
  "phone": "+79991234567",
  "account_type": "personal",
  "email_verified": false,
  "avatar_url": "https://s3.selectel.ru/...",
  "city_id": 1,
  "district_id": 5,
  "metro_station_id": 3,
  "rating": "0.00",
  "reviews_count": 0,
  "products_count": 0,
  "is_active": true,
  "is_admin": false,
  "created_at": "2026-02-07T12:30:45+00:00"
}

Public (returned when viewing another user/seller):

json
{
  "id": 5,
  "display_name": "BMW Parts Store",
  "avatar_url": "https://s3.selectel.ru/...",
  "city_id": 1,
  "rating": "4.85",
  "reviews_count": 127,
  "products_count": 342,
  "created_at": "2025-06-15T08:00:00+00:00"
}

Store Endpoints (Public, No Auth)

Products

MethodEndpointDescription
GET/api/store/productsList active products (filtered, sorted, paginated)
GET/api/store/products/search?q=...Full-text search
GET/api/store/products/{id}Product detail with images, compatibility, seller

GET /api/store/products query params:

  • sort: date_desc (default), price_asc, price_desc
  • make_id, model_id, generation_id, modification_id — YMMM filter
  • category_id, primary_category_id
  • city_id, region_id, district_id, metro_station_id — geo filter
  • steering: left, right, both, universal
  • price_min, price_max
  • limit, cursor

Product Detail Response

Returned by GET /api/store/products/{id}, GET /api/vendor/products/{id}, GET /api/admin/products/{id}.

json
{
  "data": {
    "id": 1,
    "seller_id": 5,
    "title": "Бампер передний BMW 3 Series",
    "description": "Оригинальный бампер BMW",
    "price": 2500.00,
    "steering": "left",
    "oem_number": "51117140396",
    "manufacturer": "BMW",
    "city_id": 1,
    "region_id": 47,
    "primary_category_id": 50,
    "district_id": 5,
    "metro_station_id": 3,
    "address": "Невский проспект, 40",
    "is_available": true,
    "status": "active",
    "views_count": 42,
    "favorites_count": 3,
    "published_at": "2026-02-01T10:30:00+00:00",
    "created_at": "2026-02-01T10:00:00+00:00",
    "images": [
      {
        "id": 1,
        "thumbnail_webp": "https://s3...",
        "thumbnail_jpeg": "https://s3...",
        "medium_webp": "https://s3...",
        "medium_jpeg": "https://s3...",
        "large_webp": "https://s3...",
        "large_jpeg": "https://s3...",
        "status": "ready",
        "is_primary": true,
        "sort_order": 0
      }
    ],
    "categories": [
      { "id": 1, "product_id": 1, "category_id": 50, "is_primary": true },
      { "id": 2, "product_id": 1, "category_id": 96, "is_primary": false },
      { "id": 3, "product_id": 1, "category_id": 99, "is_primary": false }
    ],
    "compatibility": [
      { "id": 1, "product_id": 1, "make_id": 2, "model_id": 10, "generation_id": 45, "modification_id": 501, "note": null }
    ],
    "oem_numbers": [
      { "id": 1, "product_id": 1, "oem_number": "51117140396", "oem_display": "51117140396" }
    ],
    "seller": {
      "id": 5,
      "display_name": "BMW Parts Store",
      "avatar_url": "...",
      "rating": "4.85",
      "reviews_count": 127,
      "products_count": 342
    }
  }
}

categories array: M:N from product_categories table. Each entry has category_id referencing a category (which has category_type: part, condition, or attribute) and is_primary flag. The primary_category_id on the product itself is a denormalized shortcut to the primary part category.

Sellers

MethodEndpointDescription
GET/api/store/sellers/{id}Seller profile + business profile
GET/api/store/sellers/{id}/productsSeller's active products

Cars (YMMM Cascade)

MethodEndpointDescriptionCache
GET/api/store/cars/makesAll makes (?popular=true for popular only)1h
GET/api/store/cars/makes/{id}/modelsModels for a make1h
GET/api/store/cars/models/{id}/generationsGenerations for a model1h
GET/api/store/cars/generations/{id}/modificationsModifications for a generation1h

Generation fields: id, model_id, name, code, year_from, year_to, steering.

Modification fields: id, generation_id, name, engine_volume, fuel_type, power, transmission, drivetrain.

Geography (4-Level Cascade)

MethodEndpointDescriptionCache
GET/api/store/geo/regionsAll regions1h
GET/api/store/geo/regions/{id}/citiesCities in region1h
GET/api/store/geo/cities/{id}/districtsDistricts in city1h
GET/api/store/geo/cities/{id}/metroMetro stations in city1h

Metro fields include line, line_color, lat, lon.

Categories

MethodEndpointDescriptionCache
GET/api/store/categoriesAll active categories1h

Category types: part, condition, attribute.


Vendor Endpoints (Authenticated Sellers)

All require session cookie. Email must be verified for product creation.

Profile

MethodEndpointDescription
GET/api/vendor/meSeller profile + business profile
PUT/api/vendor/meUpdate profile (display_name, phone, account_type, city_id, etc.)
POST/api/vendor/me/avatarUpload avatar (multipart, max 5MB, JPEG/PNG/WebP)

Products

MethodEndpointDescription
GET/api/vendor/productsSeller's products (all statuses, ?status= filter)
POST/api/vendor/productsCreate product (draft)
GET/api/vendor/products/{id}Product detail (with categories, compatibility, images, oem_numbers)
PUT/api/vendor/products/{id}Update (draft/rejected only)
POST/api/vendor/products/{id}/publishSubmit for approval (draft -> pending)
DELETE/api/vendor/products/{id}Delete (draft only)

POST /api/vendor/products — Create product:

json
{
  "title": "Амортизатор передний BMW E39",
  "price": 4500.00,
  "description": "Оригинальный амортизатор в хорошем состоянии",
  "oem_numbers": ["31311096855", "31316785589"],
  "manufacturer": "Sachs",
  "steering": "left",
  "city_id": 1,
  "district_id": 3,
  "metro_station_id": 12,
  "address": "ул. Ленина, 15",
  "primary_category_id": 22,
  "category_ids": [22, 96, 99],
  "compatibility": [
    { "make_id": 1, "model_id": 10, "generation_id": 25, "modification_id": 42 }
  ]
}

Required: title, price. All other fields optional.

category_ids — array of category IDs from all types:

  • part categories (e.g. 22 = Амортизаторы) — at least one, set same ID as primary_category_id
  • condition category (e.g. 96 = Б/У) — exactly one
  • attribute category (e.g. 99 = Оригинал) — zero or one

compatibility — array of car fitment entries. Each requires make_id; model_id, generation_id, modification_id are optional.

oem_numbers — array of OEM number strings (e.g. ["04465-33471", "04465-06090"]). Populates the M:N oem_numbers/product_oem tables. The first value is synced to the denormalized oem_number field on the product. If oem_numbers is not provided, a single oem_number string field is still accepted as fallback.

PUT /api/vendor/products/{id} accepts the same fields (all optional — only sent fields are updated). category_ids, compatibility, and oem_numbers use replace-all strategy.

Product status flow:

draft -> (publish) -> pending -> (admin) -> active
                                         -> rejected
active -> sold / archived

Product Images

MethodEndpointDescription
POST/api/vendor/products/{id}/imagesUpload (multipart, max 10MB)
PUT/api/vendor/products/{id}/images/orderReorder: { "image_ids": [5,3,1] }
DELETE/api/vendor/products/{id}/images/{imgId}Delete image

Images are processed asynchronously. status goes from processing to ready.

Favorites

MethodEndpointDescription
GET/api/vendor/favoritesUser's favorites (with product data)
POST/api/vendor/favoritesAdd: { "product_id": 1 }
DELETE/api/vendor/favorites/{product_id}Remove

Sessions

MethodEndpointDescription
GET/api/vendor/sessionsList active sessions
DELETE/api/vendor/sessions/{id}Kill specific session

Enums

EnumValues
ProductStatusdraft, pending, active, sold, archived, rejected
Steeringleft, right, both, universal
AccountTypepersonal, business
CategoryTypepart, condition, attribute

CORS

Allowed origins: partizap.ru, dev.partizap.ru, localhost:3000
Allowed headers: Content-Type, Authorization, X-CSRF-TOKEN, X-Request-ID
Credentials: true (cookies sent cross-origin)

Content Type

All requests and responses use application/json, enforced by middleware. Exception: image uploads use multipart/form-data.