Skip to content

Agent Integration Cookbook

A complete end-to-end guide for building AI agents that interact with the Owning classifieds marketplace. Covers discovery, authentication, search, listing management, pricing, messaging, webhooks, and the MCP server — with code examples in Python, TypeScript, and curl.

Agent Flows

1. Discovery

Every Owning deployment exposes a machine-readable discovery document at /.well-known/ai.json. This is the entry point for any agent that encounters Owning for the first time — it describes the API surface, authentication scheme, rate limits, and available response formats.

The discovery document is also linked from robots.txt via the AI-Discovery directive, so crawlers and agents that respect robots.txt will find it automatically.

curl

# Fetch the discovery document
curl -s https://owning.pro/.well-known/ai.json | jq

# Fetch robots.txt to see the AI-Discovery directive
curl -s https://owning.pro/robots.txt | grep -i ai-discovery

Python

import requests

response = requests.get("https://owning.pro/.well-known/ai.json")
discovery = response.json()

print(f"Service: {discovery['name']}")
print(f"API base: {discovery['api']['base_url']}")
print(f"Version: {discovery['api']['version']}")

# List available endpoints
for key, endpoint in discovery['api']['endpoints'].items():
    print(f"  {key}: {endpoint['methods']} {endpoint['path']}")

TypeScript

import { OwningClient } from '@owning/sdk-ts';

// Using fetch (no SDK needed for discovery)
const res = await fetch('https://owning.pro/.well-known/ai.json');
const discovery = await res.json();
console.log(`Service: ${discovery.name}`);
console.log(`API base: ${discovery.api.base_url}`);

// Or use the SDK (defaults to https://api.owning.pro)
const client = new OwningClient();
Agent tip

Cache the discovery document. The API base URL and endpoint paths rarely change, but checking it periodically lets your agent adapt to new endpoints automatically.

2. Authentication

Owning supports two authentication methods, both sent via the Authorization: Bearer <token> header:

MethodToken formatUse case
JWT sessioneyJhbGci...Web UI users (login/register)
API keyown_<43 chars>Programmatic access (agents, integrations)

The auth middleware auto-detects: tokens starting with own_ are treated as API keys; all others as JWT sessions. API keys are the recommended method for agents — they're long-lived, scoped by permission (read, write, admin), and don't expire.

curl — Get an API key

# Step 1: Register
curl -s -X POST https://api.owning.pro/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"agent@example.com","password":"securepass123","name":"My Agent"}' \
  | jq '.token'

# Step 2: Create an API key (using the JWT from step 1)
curl -s -X POST https://api.owning.pro/api/api-keys \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Price Monitor Agent","permissions":"write"}' \
  | jq '.key'
# Warning: Save this key — it won't be shown again

# Step 3: Use the API key for all subsequent requests
curl -s https://api.owning.pro/api/me \
  -H "Authorization: Bearer own_aBcD123..."

Python

import requests

BASE_URL = "https://api.owning.pro"

# Step 1: Register (or login)
resp = requests.post(f"{BASE_URL}/api/auth/register", json={
    "email": "agent@example.com",
    "password": "securepass123",
    "name": "My Agent",
})
jwt_token = resp.json()["token"]

# Step 2: Create an API key
resp = requests.post(f"{BASE_URL}/api/api-keys",
    headers={"Authorization": f"Bearer {jwt_token}"},
    json={"name": "Price Monitor Agent", "permissions": "write"},
)
api_key = resp.json()["key"]  # Save this!

# Step 3: Use the API key
headers = {"Authorization": f"Bearer {api_key}"}
me = requests.get(f"{BASE_URL}/api/me", headers=headers).json()
print(f"Authenticated as: {me['name']}")

TypeScript

import { OwningClient } from '@owning/sdk-ts';

// Step 1: Register to get a JWT
const client = new OwningClient();
const session = await client.auth.register({
  email: 'agent@example.com',
  password: 'securepass123',
  name: 'My Agent',
});

// Step 2: Create an API key using the JWT
client.setAuthToken(session.token);
const apiKey = await client.auth.createApiKey({
  name: 'Price Monitor Agent',
  permissions: 'write',
});
console.log(`API key: ${apiKey.key}`);  // Save this!

// Step 3: Use the API key
const agentClient = new OwningClient({ apiKey: apiKey.key });
const me = await agentClient.auth.getProfile();
console.log(`Authenticated as: ${me.name}`);

API key permissions

PermissionWhat it allows
readAll public endpoints + GET /api/me, GET /api/me/listings
writeEverything in read + create/update/delete listings, reviews, collections, messages
adminEverything in write + admin endpoints (moderation, analytics)
Agent tip

Create separate API keys for separate agents. If one is compromised, you can revoke just that key without affecting others.

3. Searching Listings

The search endpoint (GET /api/listings) is public — no authentication required. It supports full-text search, category filtering, price ranges, condition filters, location filters, dynamic attribute filters, pagination, and sorting.

curl

# Full-text search
curl -s "https://api.owning.pro/api/listings?q=bayliner&limit=5" | jq '.results[] | {title, slug, price}'

# Category + price range
curl -s "https://api.owning.pro/api/listings?category=boats&min_price=10000&max_price=50000&sort=price_asc&limit=10" | jq

# Dynamic attribute filters (boats)
curl -s "https://api.owning.pro/api/listings?category=boats&attr[boat_category]=motor&attr[min_length]=10&limit=10"

# Search results as Markdown
curl -s -H "Accept: text/markdown" "https://api.owning.pro/api/listings?q=bayliner&limit=5"

Python

import requests

BASE_URL = "https://api.owning.pro"

# Full-text search
resp = requests.get(f"{BASE_URL}/api/listings", params={
    "q": "bayliner",
    "limit": 5,
})
for listing in resp.json()["results"]:
    print(f"{listing['title']} — {listing['price']['amount']} {listing['price']['currency']}")

# Paginate through all results
all_listings = []
page = 1
while True:
    resp = requests.get(f"{BASE_URL}/api/listings", params={
        "category": "boats", "limit": 100, "page": page,
    })
    data = resp.json()
    all_listings.extend(data["results"])
    if page >= data["pagination"]["pages"]:
        break
    page += 1
print(f"Total: {len(all_listings)} listings")

TypeScript

import { OwningClient } from '@owning/sdk-ts';

const client = new OwningClient(); // no auth needed for search

// Full-text search
const results = await client.listings.search({
  q: 'bayliner',
  limit: 5,
});
for (const listing of results.results) {
  console.log(`${listing.title} — ${listing.price.amount} ${listing.price.currency}`);
}

// Category + price range + sort
const boats = await client.listings.search({
  category: 'boats',
  min_price: 10000,
  max_price: 50000,
  sort: 'price_asc',
  limit: 10,
});
console.log(`Found ${boats.pagination.total} listings`);

Search parameters

ParameterTypeDefaultDescription
qstringFull-text search (title, description, tags)
categorystringCategory slug (hierarchical)
min_pricenumberMinimum price (inclusive)
max_pricenumberMaximum price (inclusive)
conditionenumnew, like_new, good, fair, poor, refurbished
countrystringISO 3166-1 alpha-2 (e.g. ES)
citystringCity name (case-insensitive)
sortenumnewestnewest, price_asc, price_desc, relevance
pageinteger1Page number
limitinteger20Results per page (max 100)
attr[...]variousDynamic attribute filters (category-specific)

Autocomplete suggestions

The GET /api/search/suggest endpoint provides autocomplete suggestions for a partial search query. It merges results from three sources: category names, popular listing titles, and frequent search terms. Use it with a 200-300ms debounce on the client side.

# Autocomplete suggestions
curl -s "https://api.owning.pro/api/search/suggest?q=boat&limit=8" | jq

# Response:
# {
#   "query": "boat",
#   "suggestions": [
#     { "type": "category", "text": "Boats", "category_id": "boats", "category_path": ["vehicles", "boats"] },
#     { "type": "listing", "text": "Bayliner 175 Bowrider", "listing_id": "lst_...", "slug": "bayliner-175-bowrider" },
#     { "type": "term", "text": "boat trailer", "count": 12 }
#   ]
# }

Suggestion types: category — category name with ID and path. listing — active listing title with ID and slug. term — frequent search term with usage count.

4. Reading Listing Details

Every listing is available in two formats: JSON (structured, for programmatic use) and Markdown (human/LLM-readable, for AI consumption). Both are public — no auth required.

FormatBest forEndpoint
JSONProgrammatic processing, field-level accessGET /api/listings/{id}
MarkdownFeeding to an LLM, context windowsGET /api/listings/{id}.md

curl

# Get listing as JSON
curl -s "https://api.owning.pro/api/listings/lagoon-400-s2-JP12QW" | jq

# Get listing as Markdown (append .md)
curl -s "https://api.owning.pro/api/listings/lagoon-400-s2-JP12QW.md"

# Get listing as Markdown (via Accept header)
curl -s -H "Accept: text/markdown" "https://api.owning.pro/api/listings/lagoon-400-s2-JP12QW"

Python

import requests

BASE_URL = "https://api.owning.pro"
slug = "lagoon-400-s2-JP12QW"

# JSON — for programmatic access
resp = requests.get(f"{BASE_URL}/api/listings/{slug}")
listing = resp.json()
print(f"Title: {listing['title']}")
print(f"Price: {listing['price']['amount']} {listing['price']['currency']}")

# Markdown — for LLM consumption
resp = requests.get(f"{BASE_URL}/api/listings/{slug}.md")
print(resp.text[:500])

TypeScript

import { OwningClient } from '@owning/sdk-ts';

const client = new OwningClient();
const slug = 'lagoon-400-s2-JP12QW';

// JSON — structured data
const listing = await client.listings.get(slug);
console.log(`Title: ${listing.title}`);
console.log(`Price: ${listing.price.amount} ${listing.price.currency}`);

// Markdown — for LLM context
const markdown = await client.listings.getMarkdown(slug);
console.log(markdown.substring(0, 500));
Agent tip

When evaluating a listing with an LLM, fetch the Markdown version. It's optimized for token efficiency — you get all the key information in a format the model can reason about directly, without needing to parse JSON fields.

5. Creating Listings

Creating a listing is a two-step process: create (draft status) → publish (active status). This lets your agent prepare a listing, validate it, and optionally upload images before making it visible.

curl

# Step 1: Create a draft listing
curl -s -X POST https://api.owning.pro/api/listings \
  -H "Authorization: Bearer own_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Lagoon 400 S2 — Excellent Condition",
    "description": "2018 Lagoon 400 S2 catamaran in excellent condition...",
    "category_id": "boats",
    "condition": "good",
    "price": { "amount": 50000, "currency": "EUR", "negotiable": true },
    "location": { "country": "ES", "city": "Palma", "shipping": false },
    "tags": ["catamaran", "lagoon", "sailing"],
    "attributes": { "length": 11.97, "year": 2018, "material": "fiberglass" }
  }' | jq '.id'

# Step 2: Publish the listing
curl -s -X POST https://api.owning.pro/api/listings/lst_01J.../publish \
  -H "Authorization: Bearer own_..." | jq '.status'

Python

import requests

BASE_URL = "https://api.owning.pro"
API_KEY = "own_..."
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

# Step 1: Create a draft listing
resp = requests.post(f"{BASE_URL}/api/listings", headers=headers, json={
    "title": "Lagoon 400 S2 — Excellent Condition",
    "description": "2018 Lagoon 400 S2 catamaran in excellent condition...",
    "category_id": "boats",
    "condition": "good",
    "price": {"amount": 50000, "currency": "EUR", "negotiable": True},
    "location": {"country": "ES", "city": "Palma", "shipping": False},
    "tags": ["catamaran", "lagoon", "sailing"],
    "attributes": {"length": 11.97, "year": 2018, "material": "fiberglass"},
})
listing_id = resp.json()["id"]

# Step 2: Publish
resp = requests.post(f"{BASE_URL}/api/listings/{listing_id}/publish", headers=headers)
print(f"Published: {resp.json()['status']}")

TypeScript

import { OwningClient } from '@owning/sdk-ts';

const client = new OwningClient({ apiKey: 'own_...' });

// Step 1: Create a draft listing
const created = await client.listings.create({
  title: 'Lagoon 400 S2 — Excellent Condition',
  description: '2018 Lagoon 400 S2 catamaran in excellent condition...',
  category_id: 'boats',
  condition: 'good',
  price: { amount: 50000, currency: 'EUR', negotiable: true },
  location: { country: 'ES', city: 'Palma', shipping: false },
  tags: ['catamaran', 'lagoon', 'sailing'],
  attributes: { length: 11.97, year: 2018, material: 'fiberglass' },
});

// Step 2: Publish
const published = await client.listings.publish(created.id);
console.log(`Published: ${published.status}`);

6. Managing Listings

Once a listing is created, you can update it, change its status, renew it, mark it as sold, or delete it. All management operations require authentication and ownership.

OperationMethodEndpoint
UpdatePUT/api/listings/:id
Change statusPATCH/api/listings/:id
PublishPOST/api/listings/:id/publish
Mark soldPOST/api/listings/:id/mark-sold
RenewPOST/api/listings/:id/renew
DeleteDELETE/api/listings/:id

curl

# Update price
curl -s -X PUT https://api.owning.pro/api/listings/lst_01J... \
  -H "Authorization: Bearer own_..." \
  -H "Content-Type: application/json" \
  -d '{"price": {"amount": 45000, "currency": "EUR"}}' | jq '.price'

# Pause a listing
curl -s -X PATCH https://api.owning.pro/api/listings/lst_01J... \
  -H "Authorization: Bearer own_..." \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}' | jq '.status'

# Mark as sold
curl -s -X POST https://api.owning.pro/api/listings/lst_01J.../mark-sold \
  -H "Authorization: Bearer own_..." | jq '.status'

# Renew (extends expiration by 30 days)
curl -s -X POST https://api.owning.pro/api/listings/lst_01J.../renew \
  -H "Authorization: Bearer own_..." | jq '.expires_at'

TypeScript

import { OwningClient } from '@owning/sdk-ts';

const client = new OwningClient({ apiKey: 'own_...' });
const id = 'lst_01J...';

// Update price
const updated = await client.listings.update(id, {
  price: { amount: 45000, currency: 'EUR', negotiable: false },
});

// Pause
const paused = await client.listings.patchStatus(id, 'paused');

// Mark sold
const sold = await client.listings.markSold(id);

// Renew
const renewed = await client.listings.renew(id);

// Delete
const result = await client.listings.delete(id);
Agent tip

Use the renew endpoint periodically to keep your listings from expiring. Listings auto-expire after 30 days unless renewed.

Cookbook — Owning.pro API | Owning Docs