Fyndare API

Integrate your business systems with Fyndare. Create, manage, and sync listings programmatically.

v1.0 — StableREST APIOpenAPI Spec (JSON)

Quick Start

  1. Create a business account at fyndare.se/register (select "Företag")
  2. Go to Dashboard → API and request API access
  3. Once approved, create an API key (the key is shown only once — save it!)
  4. Make your first request:
The 'API key created' dialog in the Fyndare dashboard: the fresh key is shown only once, with a Copy button and amber 'save this now' warning
The dialog shown immediately after Create key: the full key is displayed just once — copy it before closing or you'll need to regenerate.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://fyndare.se/api/v1/me
bash

Base URL: https://fyndare.se/api/v1

The API Keys page with one active key in the list, the API documentation link, and the 'Pre-built solutions' card with Download plugin, Install guide, and Open integration flow buttons
Dashboard → API after your first key is created. If you'd rather skip building against the API directly, the Pre-built solutions card at the bottom has an Open integration flow button that jumps straight into the Fyndare Connector plugin.

Authentication

All API requests require a Bearer token in the Authorization header.

Authorization: Bearer fyn_live_abc123...
http

Live Keys

fyn_live_*

Creates real, publicly visible listings.

Test Keys

fyn_test_*

Creates draft listings. Not visible publicly. Safe for testing.

API keys are hashed with SHA-256 and never stored in plaintext. A key is shown only once at creation. If lost, revoke the old key and create a new one.

Response Format

All responses use a consistent JSON envelope:

Success
{
  "success": true,
  "data": { ... },
  "meta": {
    "requestId": "req_a1b2c3d4e5f6",
    "timestamp": "2026-02-15T14:30:00.000Z"
  }
}
json
Paginated
{
  "success": true,
  "data": [ ... ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "pages": 8,
    "hasMore": true
  },
  "meta": { ... }
}
json
Error
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Title must be at least 5 characters",
    "details": [
      { "field": "title", "message": "Title must be at least 5 characters" }
    ]
  },
  "meta": { ... }
}
json

Rate Limits

MethodLimitWindow
GET300 requestsper minute
POST / PATCH / PUT / DELETE30 requestsper minute

When rate limited, you receive a 429 response with a RATE_LIMITED error code. Wait and retry with exponential backoff.

Reference Data

Use these endpoints to discover valid category slugs and location slugs for creating listings. Cache them locally — they rarely change.

GET/categories

List all categories with children and their attributes.

GET/locations

List all 21 Swedish län with their kommuner.

Listings

GET/listings

List your own listings. Supports pagination, status filter, and sorting.

Query params: page, limit (max 100), status, sort (newest|oldest|price_asc|price_desc), externalId
POST/listings

Create a new listing.

Request body
{
  "title": "Volvo V60 2019",
  "description": "Välskött familjebil med...",
  "price": 259000,
  "condition": "GOOD",
  "categorySlug": "fordon-bilar",
  "locationLan": "vastra-gotalands-lan",
  "locationKommun": "goteborg",
  "externalId": "INV-001",
  "phone": "+46701234567",
  "images": [
    { "url": "https://example.com/car1.jpg", "order": 0 },
    { "url": "https://example.com/car2.jpg", "order": 1 }
  ]
}
json
Tip: Use the Idempotency-Key header to prevent duplicate listings on network retries.
GET/listings/:id

Get a single listing by ID.

PATCH/listings/:id

Partial update. Only send the fields you want to change.

DELETE/listings/:id

Soft-delete a listing (status set to REMOVED).

POST/listings/:id/sold

Mark a listing as sold. Only works on ACTIVE listings.

POST/listings/:id/renew

Renew a listing (+30 days). Works on ACTIVE or EXPIRED listings.

Images

Images can be uploaded in three ways: URL fetch, multipart upload, or base64 inline. Max 30 images per listing.

POST/listings/:id/images

Add an image to a listing. Supports URL, multipart, or base64.

DELETE/listings/:id/images/:imageId

Remove an image from a listing.

PATCH/listings/:id/images/reorder

Reorder images by passing an array of image IDs in desired order.

External ID / Upsert

The upsert pattern is the recommended way to sync your inventory. Use your own product ID as the externalId — Fyndare creates or updates automatically.

PUT/listings/by-external-id/:externalId

If a listing with this externalId exists for your account, update it. Otherwise, create a new one.

Example: Sync inventory item
curl -X PUT https://fyndare.se/api/v1/listings/by-external-id/INV-001 \
  -H "Authorization: Bearer fyn_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Volvo V60 2019",
    "description": "Updated description...",
    "price": 249000,
    "condition": "GOOD",
    "categorySlug": "fordon-bilar",
    "locationLan": "vastra-gotalands-lan",
    "locationKommun": "goteborg"
  }'
bash

Bulk Operations

Process up to 50 items per request. Partial success is supported — each item is processed independently.

POST/listings/bulk

Bulk create listings (max 50).

PATCH/listings/bulk

Bulk update listings by ID or externalId (max 50).

DELETE/listings/bulk

Bulk delete listings (max 50).

POST/listings/bulk/sold

Bulk mark listings as sold (max 50).

Bulk response format
{
  "success": true,
  "data": {
    "total": 3,
    "created": 2,
    "failed": 1,
    "results": [
      { "index": 0, "success": true, "data": { "id": "clxx..." } },
      { "index": 1, "success": true, "data": { "id": "clxy..." } },
      { "index": 2, "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Price is required" } }
    ]
  }
}
json

Webhooks

Receive HTTP notifications when events happen. Max 5 webhook endpoints per account.

Supported Events

EventTrigger
listing.createdListing created
listing.updatedListing updated
listing.soldMarked as sold
listing.expiredListing expired
listing.removedAdmin removed listing
message.receivedNew message on your listing
listing.favoritedSomeone favorited your listing
POST/webhooks

Create a webhook endpoint. The secret is returned only once.

GET/webhooks

List your webhook endpoints.

PATCH/webhooks/:id

Update URL, events, or active status.

DELETE/webhooks/:id

Delete a webhook endpoint.

POST/webhooks/:id/test

Send a test webhook delivery.

GET/webhooks/:id/deliveries

View delivery history.

Webhook Payload

{
  "event": "listing.sold",
  "deliveryId": "del_abc123",
  "timestamp": "2026-02-15T14:30:00.000Z",
  "data": {
    "id": "clxx123",
    "externalId": "INV-001",
    "soldAt": "2026-02-15T14:30:00.000Z"
  }
}
json

Headers included with every delivery:

X-Fyndare-Event: listing.sold
X-Fyndare-Delivery: del_abc123
X-Fyndare-Signature: sha256=abc123...
X-Fyndare-Timestamp: 1708012345
http

Verifying Signatures

Verify the X-Fyndare-Signature header using HMAC-SHA256 with your webhook secret:

JavaScript
const crypto = require("crypto");

function verifyWebhook(secret, timestamp, body, signature) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(timestamp + "." + body)
    .digest("hex");
  return signature === "sha256=" + expected;
}

// In your handler:
const sig = req.headers["x-fyndare-signature"];
const ts = req.headers["x-fyndare-timestamp"];
const isValid = verifyWebhook(WEBHOOK_SECRET, ts, JSON.stringify(req.body), sig);
javascript
Python
import hmac, hashlib

def verify_webhook(secret, timestamp, body, signature):
    message = f"{timestamp}.{body}"
    expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
    return signature == f"sha256={expected}"
python

Retry Policy

  • 1st attempt: immediate
  • Retry 1: after 5 minutes
  • Retry 2: after 30 minutes
  • Retry 3: after 2 hours
  • After 10 consecutive failures: endpoint auto-disabled

Re-enable a disabled endpoint by updating it with isActive: true.

Error Reference

CodeHTTPDescription
UNAUTHORIZED401Missing or invalid API key
FORBIDDEN403Valid key but insufficient scopes
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid input (check details array)
RATE_LIMITED429Rate limit exceeded — wait and retry
SERVER_ERROR500Internal error — retry or contact support

Code Examples

cURL — Create a listing

curl -X POST https://fyndare.se/api/v1/listings \
  -H "Authorization: Bearer fyn_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-key-123" \
  -d '{
    "title": "Volvo V60 2019",
    "description": "Välskött familjebil med full servicehistorik...",
    "price": 259000,
    "condition": "GOOD",
    "categorySlug": "fordon-bilar",
    "locationLan": "vastra-gotalands-lan",
    "locationKommun": "goteborg"
  }'
bash

JavaScript / Node.js

const API_KEY = "fyn_live_...";
const BASE = "https://fyndare.se/api/v1";

async function createListing(data) {
  const res = await fetch(`${BASE}/listings`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });
  return res.json();
}

// Create
const { data: listing } = await createListing({
  title: "Volvo V60 2019",
  description: "Välskött familjebil...",
  price: 259000,
  condition: "GOOD",
  categorySlug: "fordon-bilar",
  locationLan: "vastra-gotalands-lan",
  locationKommun: "goteborg",
});
console.log("Created:", listing.id);

// List
const res = await fetch(`${BASE}/listings?page=1&limit=10`, {
  headers: { "Authorization": `Bearer ${API_KEY}` },
});
const { data: listings } = await res.json();

// Mark as sold
await fetch(`${BASE}/listings/${listing.id}/sold`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${API_KEY}` },
});
javascript

Python

import requests

API_KEY = "fyn_live_..."
BASE = "https://fyndare.se/api/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# Create listing
resp = requests.post(f"{BASE}/listings", headers=HEADERS, json={
    "title": "Volvo V60 2019",
    "description": "Välskött familjebil...",
    "price": 259000,
    "condition": "GOOD",
    "categorySlug": "fordon-bilar",
    "locationLan": "vastra-gotalands-lan",
    "locationKommun": "goteborg",
})
listing = resp.json()["data"]
print(f"Created: {listing['id']}")

# List
resp = requests.get(f"{BASE}/listings", headers=HEADERS)
listings = resp.json()["data"]

# Mark as sold
requests.post(f"{BASE}/listings/{listing['id']}/sold", headers=HEADERS)
python

Inventory Sync Pattern

// Sync your full inventory using the upsert pattern
for (const item of inventory) {
  await fetch(
    `${BASE}/listings/by-external-id/${item.sku}`,
    {
      method: "PUT",
      headers: {
        "Authorization": `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: item.name,
        description: item.description,
        price: item.price,
        condition: "NEW",
        categorySlug: item.category,
        locationLan: "vastra-gotalands-lan",
        locationKommun: "goteborg",
      }),
    }
  );
}

// Mark sold items
const soldIds = inventory
  .filter(i => i.sold)
  .map(i => i.fyndareId);

if (soldIds.length > 0) {
  await fetch(`${BASE}/listings/bulk/sold`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ids: soldIds }),
  });
}
javascript

Account

GET/me

Get your account info and API key details.

GET/me/usage

Get API usage statistics for the current key.

Query params: days (1-90, default 30)

Pre-made solutions

Don't want to build the integration from scratch? We ship ready-made packages that connect popular platforms to Fyndare with minimal configuration.

Fyndare Connector — WordPress / WooCommerce

A free WordPress plugin that detects whether you have WooCommerce products or a vehicle CPT and pushes your listings to Fyndare automatically. API key and webhook secret are generated on activation — no manual configuration required.

  • Works with and without WooCommerce (WP vehicle CPTs supported)
  • Real-time sync via webhooks — store changes mirrored on Fyndare within seconds
  • Open source under GPL-2.0
  • 15-minute scheduled polling as a safety net