Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Surfing Dog Search Reports API
version: '1'
license:
name: ODbL-1.0 (the directory data; town names and centres © GeoNames, CC BY 4.0)
url: https://opendatacommons.org/licenses/odbl/1-0/
summary: A public directory of businesses an AI can ask, book or buy from, through their own inboxes and other agent doors.
description: 'Everything here is a public read, no sign-in, the same answer for everyone (a free directory key only lets a list page deeper than 200 places), except two calls about door reports: POST /v1/door-reports, where an agent says how a business''s door went, and GET /v1/door-reports/me, signed, which answers the signer''s own reporting credit. Signed reads may count against a contributor''s own allowance; the answers are the same. The directory''s order is published at GET /v1/ranking and cannot be bought; every query parameter only leaves businesses out. This API takes no orders or payments and books nothing itself; each result lists the business''s own doors (a listing''s doors, or a member''s protocols). A business this network found on its own website is marked as found, with the date it was checked; only facts it published itself are shown, never a phone or an email. Errors are RFC 9457 problem documents. The protocol behind this API, with its JSON Schemas and test vectors, is public; the MCP server at /mcp offers the same reads as tools, and can carry a person''s request to a business''s own inbox when the business allows it.'
servers:
- url: https://network.surfingdog.ai
tags:
- name: Reports
description: 'How a business''s doors went, as agents say: private to the business.'
paths:
/v1/door-reports:
post:
operationId: reportDoors
tags:
- Reports
summary: Say how a business's door went
description: 'After an agent uses a business''s door (its inbox, MCP, A2A or API), done or not, it says how it went: ok, refused (an honest no), invalid (the caller''s own fault), fail, or info (the door needs a person), with a class for every result but ok. A report goes privately to the business as counts by day; a report that the door could not be reached makes this network read the door itself, at most twice a day per door for reports no proof binds. On its own a report changes nothing public. Each report is taken or ignored on its own, with the first reason that applies: unknown_door, unknown_op, bad_class, bad_time, superseded (a later report in the call is the same attempt), over_limit. The body is strict JSON with closed objects: a member not named here (in any letter case), a member named twice, or a value of the wrong type or outside its pattern is refused whole with 400 malformed, and a report never carries a pass, a key, a person, an email, a name, a phone, an address, a message, an item id, a free-text reason or a trace; more than 100 reports, more than 128 KB or a token over 1 KB is 413 too_large. Unsigned calls are welcome; sdi-agent/1 with a self-held key, or Web Bot Auth with a platform''s key directory, says more about where reports come from, never who. A signature that does not verify makes the call unsigned, and Sdi-Signature says why. The same call is the MCP tool report_door. A guide: /report.md.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- v
- reports
additionalProperties: false
properties:
v:
type: integer
const: 1
reports:
type: array
minItems: 1
maxItems: 100
items:
type: object
required:
- domain
- door
- op
- at
- intent
- result
additionalProperties: false
properties:
domain:
type: string
pattern: ^[a-z0-9.-]{1,253}$
description: The business's domain, as the directory lists it.
door:
type: object
required:
- type
- url
additionalProperties: false
description: 'The door used, its type and URL exactly as the directory shows them: for a member''s inbox, type inbox at the card''s inbox.url, inbox.mcp or inbox.rest; else one of a listing''s doors. A door the directory does not show for that domain, or a URL with a query or a fragment, is ignored (unknown_door).'
properties:
type:
type: string
pattern: ^[a-z0-9:_-]{1,48}$
description: inbox, mcp, a2a, openapi, api, ucp, acp, nlweb, or platform:<slug>.
url:
type: string
minLength: 1
maxLength: 2048
op:
type: string
pattern: ^[a-z][a-z0-9_]{0,31}$
description: What was tried, from the capability vocabulary (GET /v1/score-rules); another is ignored (unknown_op).
examples:
- find
- catalogue
- availability
- message
- negotiate
- book
- order
- signup
- pay
- change
- cancel
- track
- return
- receipt
- feedback
- subscription
- vouchers
- waitlist
- support
- policies
tool:
type: string
pattern: ^[a-z0-9_.-]{1,64}$
at:
type: integer
minimum: 0
description: 'When the door was used, in Unix seconds, kept to the minute: within the last 7 days, and no later than a minute after the call (else bad_time).'
ms:
type: integer
minimum: 0
maximum: 600000
description: How long the door took, in milliseconds. Private to the business.
intent:
type: string
pattern: ^[A-Za-z0-9_-]{8,32}$
description: 'One per attempt, the same for its retries: a retried attempt counts once, and its last result wins.'
result:
type: string
enum:
- ok
- refused
- invalid
- fail
- info
class:
type: string
pattern: ^[a-z0-9_.]{1,64}$
examples:
- dns.name_not_resolved
- dns.failed
- tcp.timed_out
- tcp.refused
- tcp.reset
- tls.cert.invalid
- tls.failed
- http.error
- http.response.invalid
- abandoned
- timeout
- operation.timeout
- protocol.internal
- protocol.advertised_missing
- protocol.version
- protocol.invalid_answer
- operation.tool_error
- operation.failed
- operation.internal
- payment_failed
- slot_taken
- out_of_stock
- item_unavailable
- outside_limits
- wrong_state
- customer_side
- escalation
- auth
- person
- client.invalid_params
- client.parse_error
- client.invalid_request
- client.mcp_headers
- client.mcp_refused
- client.http
- client.rate_limited
description: For every result but ok, what happened, a class of that result (vocab/door-report-classes.json, /report.md); none with ok. Else bad_class.
token:
type: string
maxLength: 1024
pattern: ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$
description: The door's signed outcome token for this answer, when it gave one, as it came.
responses:
'202':
description: 'Every report accepted (what proved it, what it was kept as: both private) or ignored (why), by its place in reports.'
content:
application/json:
schema:
type: object
required:
- accepted
- ignored
- next
properties:
accepted:
type: array
items:
type: object
required:
- i
- proof
- counts
properties:
i:
type: integer
minimum: 0
proof:
type: string
enum:
- none
- signature
- token
description: token, a door token that verified; signature, a signature that verified; else none.
counts:
type: string
enum:
- counter
- row
description: 'What the report was kept as: a per-door daily counter, or a row (a platform''s signed report); both private.'
ignored:
type: array
items:
type: object
required:
- i
- why
properties:
i:
type: integer
minimum: 0
why:
type: string
enum:
- unknown_door
- unknown_op
- bad_class
- bad_time
- superseded
- over_limit
next:
type: string
'400':
$ref: '#/components/responses/Problem'
'413':
$ref: '#/components/responses/Problem'
'429':
$ref: '#/components/responses/Problem'
/v1/door-reports/me:
get:
operationId: getReporter
tags:
- Reports
summary: Your own reporting credit, and what you read with
description: 'Signed sdi-agent/1 with your own key, or Web Bot Auth as your platform, it answers the signer''s credit over the last 30 days and whether that makes it a contributor. A signed report earns its signer 1 point for its business, once a day, when the signer''s own signed reads returned that business today or yesterday (UTC), whatever the report said, and at most 10 points a day; a platform earns none at a business on its own registrable domain. A contributor has 30 points in the last 30 days, on at least 7 different days, and reads /mcp, /a2a, /v1/businesses and /v1/businesses/{domain} on an allowance of its own: 1,800 a minute for a key, 6,000 for a platform, on top of the 600 a minute every address has. Nothing else changes for it: the same answers and the same order. The signature is checked before anything is looked up, so there is no way to ask about anyone else. Unsigned: 401 signature_required. A guide: /report.md.'
responses:
'200':
description: The signer's credit and allowance.
content:
application/json:
schema:
type: object
required:
- reporter
- credit
- contributor
- threshold
- reads
- contributor_reads
- guide
- as_of
properties:
reporter:
type: object
required:
- type
properties:
type:
type: string
enum:
- key
- platform
keyid:
type: string
description: A key's RFC 7638 thumbprint.
origin:
type: string
description: A platform's origin, as its Signature-Agent named it.
platform:
type: string
description: 'The platform''s registrable domain: every origin of it is one reporter.'
credit:
type: object
description: 'Over the window: the points, and the days that earned any.'
required:
- points
- days
properties:
points:
type: integer
minimum: 0
days:
type: integer
minimum: 0
contributor:
type: boolean
threshold:
type: object
description: 'The rule, as packages/spec''s REPORTER_CREDIT_RULE gives it: a contributor has min_points points on min_days days in the last window_days, each day counting at most per_day_at_most.'
required:
- window_days
- min_days
- min_points
- per_day_at_most
properties:
window_days:
type: integer
min_days:
type: integer
min_points:
type: integer
per_day_at_most:
type: integer
reads:
$ref: '#/components/schemas/ReadAllowance'
contributor_reads:
$ref: '#/components/schemas/ReadAllowance'
guide:
type: string
format: uri
description: This network's guide for agents, /report.md, which publishes the rule.
as_of:
type: string
format: date-time
'401':
description: signature_required (the call is not signed), or why its signature was not used (bad_signature, expired, replayed_signature), also in Sdi-Signature.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
'429':
description: 'rate_limited: over 60 calls a minute from one address or its network. Try again after Retry-After seconds.'
headers:
Retry-After:
schema:
type: integer
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
components:
schemas:
Problem:
type: object
required:
- type
- title
- status
properties:
type:
type: string
title:
type: string
status:
type: integer
code:
type: string
description: 'What to do differently, when a status alone does not say: cursor_expired, not_found, too_large, rate_limited, category_unresolved, page_too_deep.'
detail:
type: string
instance:
type: string
candidates:
type: array
items:
type: string
description: 'category_unresolved: up to 5 categories to try.'
ReadAllowance:
type: object
required:
- per_minute
- burst
- per
properties:
per_minute:
type: integer
burst:
type: integer
per:
type: string
enum:
- address
- key
- platform
description: What it is counted per.
per_address:
type: integer
description: 'A contributor''s: the most of its kind''s reads from one address.'
responses:
Problem:
description: An RFC 9457 problem document.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
externalDocs:
description: The network protocol
url: https://github.com/surfingdogai/inbox/blob/main/docs/protocol/network.md