Listings
Listings are the busiest resource. Seven endpoints — four that read or write, three that move a listing between the states it can be in.
The listing object
One listing, with its category, its tags and your directory's own custom fields folded in.
Fields
| Field | Type | Description |
|---|---|---|
| id | string | The listing's uuid. |
| public_id | string | The stable short id the listing is addressed by, here and on the public site. |
| slug | string | The listing's URL segment. It changes when the name does, and the old one keeps redirecting. |
| name | string | The business's name. |
| description | string | The description shown on the listing's public page. |
| website_url | string | The address we check for a linkback. |
| status | string | draft, published or archived. |
| verification_status | string | What the last linkback check found. |
| primary_category | object | The listing's category, with its id, name and full path. |
| tags | array | The listing's tags, as slugs. |
| properties | object | Every custom field your directory defines, with a null where a listing has no value, so the shape does not change from row to row. |
| created_at | string | When the listing was created. |
| updated_at | string | When the listing last changed, to the microsecond. This is the value you echo back as expected_updated_at. |
| content_updated_at | string | When the listing's published content last changed. This is what a client syncing incrementally wants — an enforcement pass does not move it. |
Every custom field your directory defines appears on every listing, with a null where it has no value, so the shape does not change from row to row. A value we cannot read comes back as null: a read reports what is stored, never what a write would have made of it.
Response
{
"data": {
"id": "018f7c2a-0000-7000-8000-0000000000c2",
"public_id": "9834hd7",
"slug": "my-cool-cafe",
"name": "My Cool Cafe",
"description": "Third-wave coffee in Kreuzberg.",
"website_url": "https://mycoolcafe.example",
"status": "published",
"verification_status": "verified",
"primary_category": {
"id": "018f7c2a-0000-7000-8000-0000000000a1",
"name": "Cafes",
"path": "food/cafes"
},
"tags": [
"wifi",
"outdoor-seating"
],
"properties": {
"opening_hours": "9-5"
},
"created_at": "2026-08-01T09:14:22Z",
"updated_at": "2026-08-27T11:02:05.418291Z",
"content_updated_at": "2026-08-27T11:02:05Z"
}
}
List listings
GET /api/v1/directories/DIRECTORY_ID/listings
listings:read The capability a key must carry to call this.
List every listing in the directory, in name order, a page at a time.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| page | integer | Which page to return. Counting starts at 1. A page past the end returns no rows rather than an error, and the envelope still says how many pages there are. |
| per_page | integer | How many rows a page holds. The default is 25 and the maximum is 100; anything larger is capped rather than refused. |
curl
curl -X GET \
-H "Authorization: Bearer YOUR_KEY" \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings?page=1&per_page=25"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings?page=1&per_page=25")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)Response
{
"data": [
{
"id": "018f7c2a-0000-7000-8000-0000000000c2",
"public_id": "9834hd7",
"slug": "my-cool-cafe",
"name": "My Cool Cafe",
"description": "Third-wave coffee in Kreuzberg.",
"website_url": "https://mycoolcafe.example",
"status": "published",
"verification_status": "verified",
"primary_category": {
"id": "018f7c2a-0000-7000-8000-0000000000a1",
"name": "Cafes",
"path": "food/cafes"
},
"tags": [
"wifi",
"outdoor-seating"
],
"properties": {
"opening_hours": "9-5"
},
"created_at": "2026-08-01T09:14:22Z",
"updated_at": "2026-08-27T11:02:05.418291Z",
"content_updated_at": "2026-08-27T11:02:05Z"
}
],
"meta": {
"page": 1,
"per_page": 25,
"count": 412,
"pages": 17
}
}Read a listing
GET /api/v1/directories/DIRECTORY_ID/listings/LISTING_ID
listings:read The capability a key must carry to call this.
Read one listing by its public id. The slug-and-id spelling the public site uses resolves here too.
A listing's identity here is its public_id. The slug-and-id spelling the public site uses resolves too, so a URL copied from the site works without editing.
curl
curl -X GET \
-H "Authorization: Bearer YOUR_KEY" \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)Create a listing
POST /api/v1/directories/DIRECTORY_ID/listings
listings:write The capability a key must carry to call this.
Create a listing. It starts as a draft — publishing is a separate call, so nothing goes live by accident.
Body parameters
| Parameter | Type | Description |
|---|---|---|
| name required | string | The business's name. Required when you create a listing. |
| description | string | The description shown on the listing's public page. |
| website_url | string | The address we check for a linkback back to you. |
| logo_url | string | A logo to mirror. We fetch and store it; there is no file upload on this API. |
| primary_category_id | string | The category the listing belongs to. Required before a listing can be published. |
| properties | object | Your directory's custom fields, as a JSON object keyed by field key. |
| tags | array | The tags to apply, as an array of names. Sending this replaces the listing's tags. |
A new listing is a draft. Publishing is a separate call, so nothing reaches the public site because a script sent one field too many.
There is no file upload on this API. Send logo_url and we fetch the image and store our own copy.
curl
curl -X POST \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Cool Cafe",
"website_url": "https://mycoolcafe.example",
"description": "Third-wave coffee in Kreuzberg.",
"primary_category_id": "018f7c2a-0000-7000-8000-0000000000a1",
"tags": [
"wifi",
"outdoor-seating"
]
}' \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
request["Content-Type"] = "application/json"
request.body = {
"name" => "My Cool Cafe",
"website_url" => "https://mycoolcafe.example",
"description" => "Third-wave coffee in Kreuzberg.",
"primary_category_id" => "018f7c2a-0000-7000-8000-0000000000a1",
"tags" => ["wifi", "outdoor-seating"]
}.to_json
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)Response
{
"data": {
"id": "018f7c2a-0000-7000-8000-0000000000c2",
"public_id": "9834hd7",
"slug": "my-cool-cafe",
"name": "My Cool Cafe",
"description": "Third-wave coffee in Kreuzberg.",
"website_url": "https://mycoolcafe.example",
"status": "published",
"verification_status": "verified",
"primary_category": {
"id": "018f7c2a-0000-7000-8000-0000000000a1",
"name": "Cafes",
"path": "food/cafes"
},
"tags": [
"wifi",
"outdoor-seating"
],
"properties": {
"opening_hours": "9-5"
},
"created_at": "2026-08-01T09:14:22Z",
"updated_at": "2026-08-27T11:02:05.418291Z",
"content_updated_at": "2026-08-27T11:02:05Z"
}
}Update a listing
PATCH /api/v1/directories/DIRECTORY_ID/listings/LISTING_ID
listings:write The capability a key must carry to call this.
Change a listing's details. Send only the attributes you are changing.
Body parameters
| Parameter | Type | Description |
|---|---|---|
| name required | string | The business's name. Required when you create a listing. |
| description | string | The description shown on the listing's public page. |
| website_url | string | The address we check for a linkback back to you. |
| logo_url | string | A logo to mirror. We fetch and store it; there is no file upload on this API. |
| primary_category_id | string | The category the listing belongs to. Required before a listing can be published. |
| properties | object | Your directory's custom fields, as a JSON object keyed by field key. |
| tags | array | The tags to apply, as an array of names. Sending this replaces the listing's tags. |
| expected_updated_at | string | The updated_at you last read, echoed back. If the record has moved since, nothing is written and you get 409 with the record as it now stands. Leave it out to write unconditionally. |
curl
curl -X PATCH \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Now open on Sundays."
}' \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID")
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
request["Content-Type"] = "application/json"
request.body = {
"description" => "Now open on Sundays."
}.to_json
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)Publish, unpublish, archive
A listing's three states, each moved by a call named for what it does. Publishing checks two things: the listing needs a primary category, and a draft counts against your plan's listing limit.
There is no DELETE. A listing carries slug history, linkback checks and enforcement events, which are the evidence behind an enforcement decision, so archiving is a listing's only exit — and a verb that lied about that is a verb your retry logic would believe.
POST /api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/publish
listings:write The capability a key must carry to call this.
Put the listing on the public site. A draft needs a primary category first, and your plan's listing limit is checked here.
curl
curl -X POST \
-H "Authorization: Bearer YOUR_KEY" \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/publish"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/publish")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)
POST /api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/unpublish
listings:write The capability a key must carry to call this.
Take the listing off the public site. It keeps everything it has and can be published again.
curl
curl -X POST \
-H "Authorization: Bearer YOUR_KEY" \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/unpublish"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/unpublish")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)
POST /api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/archive
listings:write The capability a key must carry to call this.
Archive the listing. This is a listing's only exit — there is no delete, because a listing carries the evidence behind an enforcement decision.
curl
curl -X POST \
-H "Authorization: Bearer YOUR_KEY" \
"https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/archive"Ruby
require "net/http"
require "json"
uri = URI("https://directories.prosvita.io/api/v1/directories/DIRECTORY_ID/listings/LISTING_ID/archive")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body)