# Emissar Resolve v0.1 lookup API, a draft for comment. Prose, normative rules and rationale: https://emissar.ai/specs/resolve/v0.1 (section 8).
# Schemas live in https://emissar.ai/specs/schemas/resolve-v0.1.schema.json and are referenced, not copied.
openapi: 3.1.0
info:
  title: Emissar Resolve lookup API
  version: 0.1.0-draft
  summary: Public, read-only lookup from a phone number, domain or brand name to an A2A Agent Card URL.
  description: |
    A draft for comment, published 2026-10-05.

    A client resolves an identifier BEFORE it contacts the business: it looks up the identifier, and if a
    record exists it fetches the Agent Card at `cardUrl` and talks A2A to the endpoint the card declares.
    If there is no record (404), it uses the business's normal channels. Resolve defines nothing that
    happens on a telephone call.

    Lookups are anonymous and rate limited per client. Every response carries CORS
    `Access-Control-Allow-Origin: *`. Positive and negative answers are cacheable; use `If-None-Match`
    with the `ETag` to revalidate. Non-canonical identifiers are redirected (308) to the canonical path;
    clients that normalize first (spec section 5) avoid the extra round trip.
servers:
  - url: https://resolve.emissar.ai
    description: Emissar's registry host (spec section 13).
tags:
  - name: lookup
paths:
  /v0/tel/{number}:
    get:
      tags: [lookup]
      operationId: lookupTel
      summary: Resolve an E.164 telephone number.
      description: |
        `number` is the canonical E.164 form: `+`, country code and national significant number, digits only
        (for example `+15195550142`). Other spellings that normalize (spaces, hyphens, dots, parentheses,
        a `tel:` prefix, full-width digits) are redirected with 308. National-format numbers without `+`
        are rejected with 400 because the country is unknown.
      parameters:
        - name: number
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
          example: "+15195550142"
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "200": { $ref: "#/components/responses/Found" }
        "304": { $ref: "#/components/responses/NotModified" }
        "308": { $ref: "#/components/responses/Canonical" }
        "400": { $ref: "#/components/responses/InvalidIdentifier" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v0/domain/{domain}:
    get:
      tags: [lookup]
      operationId: lookupDomain
      summary: Resolve a domain name (exact host).
      description: |
        `domain` is the canonical form: lowercase A-labels, no trailing dot. Uppercase, a trailing dot and
        Unicode (U-label) input are redirected with 308. Matching is exact: `www.example.com` and
        `example.com` are different identifiers. IP literals, single-label names and special-use names
        are rejected with 400.
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string, maxLength: 253 }
          example: opensea.io
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "200": { $ref: "#/components/responses/Found" }
        "304": { $ref: "#/components/responses/NotModified" }
        "308": { $ref: "#/components/responses/Canonical" }
        "400": { $ref: "#/components/responses/InvalidIdentifier" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v0/brand/{country}/{name}:
    get:
      tags: [lookup]
      operationId: lookupBrand
      summary: Resolve a brand name within one country.
      description: |
        `country` is an ISO 3166-1 alpha-2 code (uppercase in the canonical path). `name` is the brand key
        (spec section 5.3); a brand as people write it (for example `Example Telecom, Inc.`) is redirected
        with 308 to its key. A brand answer can hold several records (different organizations can hold the
        same name in different fields of trade); clients MUST NOT pick one automatically without other
        evidence (spec section 8.4).
      parameters:
        - name: country
          in: path
          required: true
          schema: { type: string, minLength: 2, maxLength: 2 }
          example: CA
        - name: name
          in: path
          required: true
          schema: { type: string, maxLength: 200 }
          example: exampletelecom
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "200": { $ref: "#/components/responses/Found" }
        "304": { $ref: "#/components/responses/NotModified" }
        "308": { $ref: "#/components/responses/Canonical" }
        "400": { $ref: "#/components/responses/InvalidIdentifier" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
components:
  parameters:
    IfNoneMatch:
      name: If-None-Match
      in: header
      required: false
      description: ETag from an earlier 200. A match returns 304 with no content (RFC 9110 section 13.1.2).
      schema: { type: string }
  headers:
    ETag:
      description: Strong validator over the RFC 8785 canonical form of the response body.
      schema: { type: string }
    CacheControl:
      description: |
        200: `public, max-age=N` with N at most 3600 and never past the earliest record expiry.
        404: `public, max-age=300`. 400: `public, max-age=3600`. 308: `public, max-age=86400`. 429: `no-store`.
      schema: { type: string }
    Location:
      description: Canonical path for the identifier.
      schema: { type: string }
    RetryAfter:
      description: Seconds until the client's rate-limit window resets (RFC 9110 section 10.2.3).
      schema: { type: integer, minimum: 0 }
    RateLimitPolicy:
      description: Informative. Syntax of draft-ietf-httpapi-ratelimit-headers-11, for example `"default";q=60;w=60`.
      schema: { type: string }
    RateLimit:
      description: Informative. Remaining quota and seconds to reset, for example `"default";r=12;t=41`.
      schema: { type: string }
    AccessControlAllowOrigin:
      description: Always `*`. Lookups are public.
      schema: { type: string, const: "*" }
  responses:
    Found:
      description: One or more records. `records` has exactly one entry for tel and domain lookups.
      headers:
        ETag: { $ref: "#/components/headers/ETag" }
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
        RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
        RateLimit: { $ref: "#/components/headers/RateLimit" }
        Access-Control-Allow-Origin: { $ref: "#/components/headers/AccessControlAllowOrigin" }
      content:
        application/json:
          schema:
            $ref: "../schemas/resolve-v0.1.schema.json#/$defs/LookupResponse"
    NotModified:
      description: The cached answer is still current. No content.
      headers:
        ETag: { $ref: "#/components/headers/ETag" }
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
    Canonical:
      description: Permanent redirect to the canonical path of a normalizable identifier. No content.
      headers:
        Location: { $ref: "#/components/headers/Location" }
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
    InvalidIdentifier:
      description: The identifier cannot be normalized (spec section 5). Retrying will not help.
      headers:
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
      content:
        application/problem+json:
          schema:
            $ref: "../schemas/resolve-v0.1.schema.json#/$defs/Problem"
    NotFound:
      description: |
        Negative answer: Resolve holds no current record for this identifier. This is a normal result, not a
        failure; the client uses the business's normal channels. Cacheable for 300 seconds.
      headers:
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
        RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
        RateLimit: { $ref: "#/components/headers/RateLimit" }
      content:
        application/problem+json:
          schema:
            $ref: "../schemas/resolve-v0.1.schema.json#/$defs/Problem"
    RateLimited:
      description: The client exceeded its lookup quota (RFC 6585 section 4). Never stored by caches.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
        Cache-Control: { $ref: "#/components/headers/CacheControl" }
        RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
        RateLimit: { $ref: "#/components/headers/RateLimit" }
      content:
        application/problem+json:
          schema:
            $ref: "../schemas/resolve-v0.1.schema.json#/$defs/Problem"
    Unavailable:
      description: Temporary failure. Clients treat it like a negative answer for this attempt and do not cache it.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/problem+json:
          schema:
            $ref: "../schemas/resolve-v0.1.schema.json#/$defs/Problem"
