KupiProdaj API

REST API for managing listings programmatically. Upload images, create, update, and delete listings.

Base URL: https://kupiprodaj.me/api/v1Get API Key

Authentication

All API requests require a Bearer token in the Authorization header. Create API keys in your dashboard.

curl -H "Authorization: Bearer kp_live_abc123..." \
  https://kupiprodaj.me/api/v1/listings
Rate limit:100 requests per minute per API key. Headers X-RateLimit-Remaining and X-RateLimit-Reset are included in every response.
Response 401
{
  "error": "Invalid or missing API key"
}
Response 429
{
  "error": "Rate limit exceeded",
  "retryAfter": 42
}

Upload images to get URLs, then pass those URLs when creating or updating a listing. This is a two-step process: upload first, then create the listing with the returned URLs.

Request (multipart/form-data)

ParameterTypeRequiredDescription
filesFile[]requiredImage files. Max 10 per request, max 5MB each. Allowed: JPEG, PNG, WebP, GIF.
listingSlugstringoptionalOptional slug for organizing files. Defaults to 'temp'.

Example: upload with curl

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  -F "[email protected]" \
  https://kupiprodaj.me/api/v1/upload
Response 200
{
  "urls": [
    "https://images.kupiprodaj.me/listings/temp/1710000000-abc123.jpg",
    "https://images.kupiprodaj.me/listings/temp/1710000000-def456.jpg"
  ]
}

Example: upload with JavaScript

const form = new FormData();
form.append("files", photoFile1);
form.append("files", photoFile2);

const res = await fetch("https://kupiprodaj.me/api/v1/upload", {
  method: "POST",
  headers: { "Authorization": "Bearer YOUR_API_KEY" },
  body: form,  // Do NOT set Content-Type — browser sets it with boundary
});

const { urls } = await res.json();
// Use urls in the create listing request

Example: upload with Python

import requests

files = [
    ("files", ("photo1.jpg", open("photo1.jpg", "rb"), "image/jpeg")),
    ("files", ("photo2.jpg", open("photo2.jpg", "rb"), "image/jpeg")),
]

res = requests.post(
    "https://kupiprodaj.me/api/v1/upload",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    files=files,
)

urls = res.json()["urls"]

Returns a paginated list of your own listings.

Query parameters

ParameterTypeRequiredDescription
pageintegeroptionalPage number (default: 1)
limitintegeroptionalItems per page, 1-100 (default: 20)
statusstringoptionalFilter by status: DRAFT, PENDING_MODERATION, ACTIVE, REJECTED, INACTIVE, SOLD, ARCHIVED, EXPIRED

Example

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://kupiprodaj.me/api/v1/listings?page=1&limit=10&status=ACTIVE"
Response 200
{
  "listings": [
    {
      "id": "clx...",
      "title": "iPhone 15 Pro 256GB",
      "slug": "iphone-15-pro-256gb-a1b2c3",
      "description": "Kao nov, korišćen 3 mjeseca...",
      "price": 899,
      "currency": "EUR",
      "status": "ACTIVE",
      "externalRef": null,
      "category": { "id": "clx...", "name": "Mobilni telefoni", "slug": "elektronika/telefoni/mobilni-telefoni" },
      "city": { "id": "clx...", "name": "Podgorica", "slug": "podgorica" },
      "country": { "id": "clx...", "name": "Crna Gora" },
      "images": [
        { "id": "clx...", "url": "https://images.kupiprodaj.me/listings/...", "alt": "iPhone 15 Pro", "order": 0 }
      ],
      "attributes": { "brand": "Apple", "model": "iPhone 15 Pro", "storage": "256GB" },
      "featured": false,
      "views": 42,
      "createdAt": "2026-03-01T12:00:00.000Z",
      "updatedAt": "2026-03-01T12:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10,
  "pages": 1
}

Create a new listing. Upload images first via /api/v1/upload, then pass the returned URLs here.

Request body (JSON)

ParameterTypeRequiredDescription
titlestringrequiredListing title (min 3 characters)
categoryIdstringrequiredCategory UUID. See reference table below.
descriptionstringoptionalListing description
pricenumberoptionalPrice (null for 'negotiable')
currencystringoptionalCurrency code (default: EUR)
cityIdstringoptionalCity UUID. Country auto-resolved from city.
countryIdstringoptionalCountry UUID (optional if cityId provided)
attributesobjectoptionalCategory-specific attributes (brand, model, year, etc.)
imageUrlsstring[]optionalArray of image URLs from /api/v1/upload
externalRefstringoptionalYour external reference ID for tracking

Full example: upload + create

# Step 1: Upload images
IMAGE_URLS=$(curl -s -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  -F "[email protected]" \
  https://kupiprodaj.me/api/v1/upload | jq -r '.urls')

# Step 2: Create listing with image URLs
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "iPhone 15 Pro 256GB",
    "description": "Kao nov, korišćen 3 mjeseca. Komplet oprema.",
    "price": 899,
    "currency": "EUR",
    "categoryId": "CATEGORY_UUID_HERE",
    "cityId": "CITY_UUID_HERE",
    "imageUrls": '"$IMAGE_URLS"',
    "attributes": {
      "brand": "Apple",
      "model": "iPhone 15 Pro",
      "storage": "256GB",
      "color": "Crna",
      "condition": "Kao novo"
    },
    "externalRef": "my-shop-sku-12345"
  }' \
  https://kupiprodaj.me/api/v1/listings
Response 201
{
  "id": "clx...",
  "slug": "iphone-15-pro-256gb-a1b2c3",
  "title": "iPhone 15 Pro 256GB",
  "status": "ACTIVE",
  "category": { "id": "clx...", "name": "Mobilni telefoni", "slug": "elektronika/telefoni/mobilni-telefoni" },
  "images": [
    { "id": "clx...", "url": "https://images.kupiprodaj.me/listings/...", "alt": "iPhone 15 Pro 256GB", "order": 0 },
    { "id": "clx...", "url": "https://images.kupiprodaj.me/listings/...", "alt": "iPhone 15 Pro 256GB", "order": 1 }
  ],
  "createdAt": "2026-03-10T12:00:00.000Z"
}
Response 400
{
  "error": "Title is required (min 3 chars)"
}

Get full details of one of your listings by its ID.

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://kupiprodaj.me/api/v1/listings/LISTING_ID
Response 200
{
  "id": "clx...",
  "title": "iPhone 15 Pro 256GB",
  "slug": "iphone-15-pro-256gb-a1b2c3",
  "description": "Kao nov, korišćen 3 mjeseca...",
  "price": 899,
  "currency": "EUR",
  "isPriceNegotiable": false,
  "status": "ACTIVE",
  "externalRef": "my-shop-sku-12345",
  "category": { "id": "clx...", "name": "Mobilni telefoni", "slug": "elektronika/telefoni/mobilni-telefoni" },
  "city": { "id": "clx...", "name": "Podgorica", "slug": "podgorica" },
  "country": { "id": "clx...", "name": "Crna Gora" },
  "images": [ ... ],
  "attributes": { "brand": "Apple", "model": "iPhone 15 Pro", "storage": "256GB" },
  "featured": false,
  "views": 42,
  "createdAt": "2026-03-01T12:00:00.000Z",
  "updatedAt": "2026-03-01T12:00:00.000Z"
}

Update any fields of your listing. Only send the fields you want to change. To replace images, upload new ones first, then pass the new imageUrls array.

Request body (JSON) — all fields optional

ParameterTypeRequiredDescription
titlestringoptionalNew title
descriptionstringoptionalNew description
pricenumberoptionalNew price
currencystringoptionalNew currency
isPriceNegotiablebooleanoptionalPrice negotiable flag
statusstringoptionalACTIVE, INACTIVE, SOLD, or ARCHIVED
categoryIdstringoptionalMove to different category
cityIdstringoptionalChange city
countryIdstringoptionalChange country
attributesobjectoptionalReplace all attributes
imageUrlsstring[]optionalReplace all images with new URLs

Example: update price and status

curl -X PUT \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "price": 799, "status": "ACTIVE" }' \
  https://kupiprodaj.me/api/v1/listings/LISTING_ID

Example: replace images

# Upload new photos
NEW_URLS=$(curl -s -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "files=@new_photo.jpg" \
  https://kupiprodaj.me/api/v1/upload | jq '.urls')

# Update listing images (replaces all existing)
curl -X PUT \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "imageUrls": '"$NEW_URLS"' }' \
  https://kupiprodaj.me/api/v1/listings/LISTING_ID
Response 200
{
  "id": "clx...",
  "title": "iPhone 15 Pro 256GB",
  "slug": "iphone-15-pro-256gb-a1b2c3",
  "status": "ACTIVE",
  "images": [ ... ],
  "updatedAt": "2026-03-10T14:30:00.000Z"
}

Permanently deletes a listing and all associated images from storage (R2). This action cannot be undone.

curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  https://kupiprodaj.me/api/v1/listings/LISTING_ID
Response 200
{
  "success": true,
  "id": "clx..."
}
Response 404
{
  "error": "Not found"
}

Returns the effective attribute schema for a category — its own attributes plus any inherited from ancestor categories (nearest wins). This is the exact set that attributes is validated against when you create or update a listing: unknown keys, wrong types, or values outside options are rejected with a 422.

Path parameters

ParameterTypeRequiredDescription
idstringrequiredCategory id (see Categories Reference below)

Example

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://kupiprodaj.me/api/v1/categories/CATEGORY_ID/attributes
Response 200
{
  "categoryId": "clx...",
  "categorySlug": "vozila/automobili",
  "categoryName": "Automobili",
  "attributes": [
    {
      "key": "brand",
      "label": "Marka",
      "labelEn": "Brand",
      "labelRu": "Марка",
      "type": "SELECT",
      "options": ["Audi", "BMW", "Volkswagen", "..."],
      "unit": null,
      "isRequired": true,
      "isFilterable": true
    },
    {
      "key": "year",
      "label": "Godište",
      "labelEn": "Year",
      "labelRu": "Год",
      "type": "NUMBER",
      "options": null,
      "unit": null,
      "isRequired": false,
      "isFilterable": true
    }
  ]
}
Response 404
{
  "error": "Category not found"
}

Complete Workflow Example

Here is the typical flow for creating a listing with images using JavaScript/TypeScript:

const API_KEY = "kp_live_...";
const BASE_URL = "https://kupiprodaj.me/api/v1";

async function createListingWithImages(data, imageFiles) {
  // Step 1: Upload images
  const form = new FormData();
  for (const file of imageFiles) {
    form.append("files", file);
  }

  const uploadRes = await fetch(`${BASE_URL}/upload`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_KEY}` },
    body: form,
  });
  const { urls } = await uploadRes.json();

  // Step 2: Create listing with image URLs
  const listingRes = await fetch(`${BASE_URL}/listings`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      ...data,
      imageUrls: urls,
    }),
  });

  return await listingRes.json();
}

// Usage:
const listing = await createListingWithImages(
  {
    title: "Samsung Galaxy S24 Ultra",
    description: "Potpuno nov, zapakovan.",
    price: 1199,
    currency: "EUR",
    categoryId: "CATEGORY_UUID",
    cityId: "CITY_UUID",
    attributes: {
      brand: "Samsung",
      model: "Galaxy S24 Ultra",
      storage: "512GB",
      color: "Titanium Black",
    },
    externalRef: "shop-sku-001",
  },
  [file1, file2, file3]
);

console.log("Created:", listing.id, listing.slug);

Error Codes

StatusDescription
400Bad request — missing/invalid parameters
401Unauthorized — invalid or missing API key
404Not found — listing doesn't exist or doesn't belong to you
429Rate limit exceeded — wait and retry
500Server error — contact support