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-discoveryPython
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();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:
| Method | Token format | Use case |
|---|---|---|
| JWT session | eyJhbGci... | Web UI users (login/register) |
| API key | own_<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
| Permission | What it allows |
|---|---|
read | All public endpoints + GET /api/me, GET /api/me/listings |
write | Everything in read + create/update/delete listings, reviews, collections, messages |
admin | Everything in write + admin endpoints (moderation, analytics) |
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
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | — | Full-text search (title, description, tags) |
category | string | — | Category slug (hierarchical) |
min_price | number | — | Minimum price (inclusive) |
max_price | number | — | Maximum price (inclusive) |
condition | enum | — | new, like_new, good, fair, poor, refurbished |
country | string | — | ISO 3166-1 alpha-2 (e.g. ES) |
city | string | — | City name (case-insensitive) |
sort | enum | newest | newest, price_asc, price_desc, relevance |
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (max 100) |
attr[...] | various | — | Dynamic 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.
| Format | Best for | Endpoint |
|---|---|---|
| JSON | Programmatic processing, field-level access | GET /api/listings/{id} |
| Markdown | Feeding to an LLM, context windows | GET /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));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.
| Operation | Method | Endpoint |
|---|---|---|
| Update | PUT | /api/listings/:id |
| Change status | PATCH | /api/listings/:id |
| Publish | POST | /api/listings/:id/publish |
| Mark sold | POST | /api/listings/:id/mark-sold |
| Renew | POST | /api/listings/:id/renew |
| Delete | DELETE | /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);Use the renew endpoint periodically to keep your listings from expiring. Listings auto-expire after 30 days unless renewed.