openapi: 3.1.0
info:
  title: APP v0 (Agent Payment Protocol) for the Okidooki ecosystem
  version: "0"
  description: |
    One protocol, three seller networks. Every route is POST {gateway}/a2a/v0/{route} with a JSON body.
    Auth: `Authorization: Bearer <key>`, `X-Timestamp` (unix seconds, 5-minute window), `X-Signature` = hex(HMAC-SHA256(key, `${timestamp}.${rawBody}`)).
    Keys and sandbox users come from the developer portal (https://okidooki.app/webapp/developers). Registry: https://okidooki.app/.well-known/app-v0.json
servers:
  - url: https://europe-west1-okidooki-nightlife.cloudfunctions.net/a2aGateway
    description: Okidooki Night (venues, tables, events, tickets)
  - url: https://nowtrust.app
    description: NowTrust (licensed security guards, SOS, spending mandates)
  - url: https://europe-west1-make-it-fun-app.cloudfunctions.net/a2aGateway
    description: MakeItFun (print-on-demand merch with AI / text / image designs)
security:
  - apiKeyHmac: []
components:
  securitySchemes:
    apiKeyHmac:
      type: http
      scheme: bearer
      description: Bearer key plus X-Timestamp and X-Signature headers (HMAC-SHA256 over `${timestamp}.${rawBody}`).
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      schema: { type: string }
      description: Required in practice for `order`; the same key returns the same order.
  schemas:
    UserRef:
      type: object
      required: [network, uid]
      properties:
        network: { type: string, description: Your integration slug (the key's network). }
        uid: { type: string, description: Your user id. }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, enum: [auth, invalid_request, link_required, forbidden, mandate_scope, not_found, quote_expired, seller_unavailable, shipping_address_required, invalid_state, rate_limited, internal] }
            message: { type: string }
            retryable: { type: boolean }
    Card:
      type: object
      properties:
        protocol: { type: string, const: okidooki-app }
        version: { type: string, const: "0" }
        network: { type: string }
        description: "{ type: string }"
        endpoints: { type: object, additionalProperties: { type: string, format: uri } }
        auth: { type: object }
        settlement: { type: array, items: { type: string } }
        currencies: { type: array, items: { type: string } }
        capabilities: { type: array, items: { type: string } }
        linking: { type: object }
    Seller:
      type: object
      properties:
        sellerId: { type: string, description: "e.g. product:71, venue:abc, guard:xyz" }
        name: { type: string }
        image: { type: [string, "null"] }
        capabilities: { type: array, items: { type: string } }
      additionalProperties: true
    Quote:
      type: object
      properties:
        quoteId: { type: string }
        sellerId: { type: string }
        quantity: { type: integer }
        lines: { type: array, items: { type: object, properties: { label: { type: string }, amount: { type: integer, description: minor units } } } }
        total: { type: integer, description: Minor units (CHF centimes). }
        currency: { type: string }
        expiresAt: { type: string, format: date-time }
        terms: { type: object }
        shipping: { type: object, description: "MakeItFun: methodId and options[]" }
      additionalProperties: true
    Order:
      type: object
      properties:
        orderId: { type: string }
        status: { type: string, enum: [needs_user_confirmation, paid, in_production, shipped, confirmed, active, completed, failed, refunded, cancelled, expired] }
        total: { type: integer }
        currency: { type: string }
        confirmUrl: { type: [string, "null"], description: Where the user confirms/pays (seller app). }
        reason: { type: [string, "null"] }
        expiresAt: { type: [string, "null"], format: date-time }
        tracking: { type: [object, "null"] }
      additionalProperties: true
    Mandate:
      type: object
      description: Limits the user set for THIS agent (okidooki.app/webapp/connect). Amounts in centimes; null = no limit.
      properties:
        perOrderMax: { type: [integer, "null"] }
        monthlyMax: { type: [integer, "null"] }
        capabilities: { type: [array, "null"], items: { type: string }, description: "null = everything the network offers" }
        validUntil: { type: [string, "null"], format: date-time }
        autoApprove: { type: boolean, description: "NowTrust: bookings within the limits settle without a tap" }
        spentThisMonth: { type: integer }
        remainingThisMonth: { type: [integer, "null"] }
        expired: { type: boolean }
    Connect:
      type: object
      properties:
        connectId: { type: string }
        status: { type: string, enum: [pending, approved, declined, expired] }
        url: { type: string, format: uri, description: Send the user here. }
        networks: { type: array, items: { type: string } }
        results: { type: [object, "null"], additionalProperties: { type: object, properties: { status: { type: string, enum: [linked, needs_code, declined, failed] }, code: { type: string }, deepLink: { type: string }, webLink: { type: string }, mandate: { $ref: "#/components/schemas/Mandate" } } } }
        mandate: { $ref: "#/components/schemas/Mandate" }
        expiresInSeconds: { type: integer }
    WebhookEvent:
      type: object
      description: Delivered by POST to your webhook URL. Verify X-APPV0-Signature (t=<unix>,v1=hex(HMAC-SHA256(secret, t + '.' + rawBody))) within 5 minutes.
      properties:
        id: { type: string }
        type: { type: string, enum: [ping, order.status, sos.status, connect.approved, link.revoked] }
        network: { type: string }
        createdAt: { type: string, format: date-time }
        data: { type: object, additionalProperties: true, description: "order.status: orderId, status, previousStatus, userRef, total, currency, tracking?, reason?; sos.status: emergencyId, status, previousStatus, guardId, estimatedArrival; connect.approved: connectId, userRef, results, mandate; link.revoked: userRef, networks" }
    PreviewResult:
      type: object
      properties:
        jobId: { type: [string, "null"] }
        status: { type: string, enum: [queued, generating, done, failed] }
        designId: { type: string }
        mockupURL: { type: [string, "null"] }
        mockupURLs: { type: array, items: { type: string } }
        mockupsByPlacement: { type: [object, "null"] }
        colorAlternatives: { type: array, items: { type: object } }
        placements: { type: array, items: { type: string } }
        retailPrice: { type: number }
        currency: { type: string }
        printSpec: { type: [object, "null"], description: print file px/dpi, placed size, effective dpi, ok }
        warnings: { type: array, items: { type: string } }
        pollAfterSeconds: { type: integer }
        existing: { type: boolean, description: "A preview was already running for this user; poll it." }
  responses:
    Err:
      description: Protocol error
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
paths:
  /card:
    get:
      x-networks: [okidooki, makeitfun]
      security: []
      summary: Agent card (Okidooki, MakeItFun). NowTrust serves it at /a2a/v0/card and a Google A2A card at /.well-known/agent.json.
      responses: { "200": { description: Card, content: { application/json: { schema: { $ref: "#/components/schemas/Card" } } } } }
  /a2a/v0/discover:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: What the network sells (sellers), or one seller in depth (sellerId). MakeItFun also lists occasion templates (occasions:true).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sellerId: { type: string }
                productType: { type: string, description: "MakeItFun: tshirt, hoodie, cap, mug, poster, bag, phone_case, sticker, bottle, blanket, patch, tank, polo, tote, pillow, mousepad" }
                styles: { type: boolean, description: "MakeItFun: all curated looks (Design Curator) with cached preview pictures" }
                suggest: { type: object, description: "MakeItFun: top looks for a product type / occasion / vibe / city / event", properties: { productType: { type: string }, occasion: { type: string }, vibe: { type: string }, city: { type: string }, event: { type: object, properties: { name: { type: string }, genre: { type: string }, city: { type: string }, date: { type: string } } }, count: { type: integer } } }
                query: { type: string }
                categoryId: { type: integer }
                country: { type: string, default: CH }
                occasions: { type: boolean }
                slots: { type: object }
                cityName: { type: string, description: "Okidooki: city (or near / cityId)" }
                near: { type: object, properties: { lat: { type: number }, lng: { type: number } } }
                kind: { type: string, description: "NowTrust: guard" }
                partySize: { type: integer, description: "NowTrust: guests to protect (1-5000); sizes the team" }
                intent: { type: string, description: "NowTrust: birthday | party | corporate | event | wedding | club | vip | escort | home | shop (maps to a security type)" }
                securityType: { type: string, enum: [event_security, bodyguard, nightclub_security, private_property, escort_security], description: "NowTrust: explicit type (wins over intent)" }
                now: { type: boolean, description: "NowTrust: available right now" }
                limit: { type: integer }
      responses:
        "200": { description: Sellers, content: { application/json: { schema: { type: object, properties: { sellers: { type: array, items: { $ref: "#/components/schemas/Seller" } } }, additionalProperties: true } } } }
        default: { $ref: "#/components/responses/Err" }
  /a2a/v0/quote:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Firm price (30 minutes) for a seller/variant/design and the linked user's situation (address, mandate).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
                userRef: { $ref: "#/components/schemas/UserRef" }
                sellerId: { type: string, description: "NowTrust: guard:<id> or company:<id> from discover; MakeItFun: product:<id>; Okidooki: venue:<id> / event:<id>" }
                designId: { type: string, description: "MakeItFun: from preview" }
                variantId: { type: integer }
                quantity: { type: integer, minimum: 1, maximum: 10 }
                team: { type: array, description: "MakeItFun: one unit per member with name/number printed; size = that member's own size (same colour), priced per size", items: { type: object, properties: { name: { type: string }, number: { type: string }, size: { type: string } } } }
                teamPlacement: { type: string }
                shippingMethod: { type: string }
                durationHours: { type: number, minimum: 1, maximum: 24, description: "NowTrust: booking length" }
                when: { type: string, format: date-time, description: "NowTrust: start (ISO 8601, not in the past, within the booking horizon)" }
                location: { type: object, description: "NowTrust: where the guard is needed", properties: { lat: { type: number }, lng: { type: number }, address: { type: string } } }
                intent: { type: string, description: "NowTrust: see discover" }
                securityType: { type: string, enum: [event_security, bodyguard, nightclub_security, private_property, escort_security] }
      responses:
        "200": { description: Quote, content: { application/json: { schema: { $ref: "#/components/schemas/Quote" } } } }
        default: { $ref: "#/components/responses/Err" }
  /a2a/v0/order:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Place the order for a quote. The user confirms/pays in the seller's app (confirmUrl); poll status.
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        content: { application/json: { schema: { type: object, required: [userRef, quoteId], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, quoteId: { type: string }, note: { type: string } } } } }
      responses:
        "200": { description: Order, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        default: { $ref: "#/components/responses/Err" }
  /a2a/v0/orders:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Latest orders of the user placed with this key.
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, limit: { type: integer } } } } } }
      responses: { "200": { description: Orders, content: { application/json: { schema: { type: object, properties: { orders: { type: array, items: { $ref: "#/components/schemas/Order" } } } } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/status:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Order status (payment, production, tracking).
      requestBody: { content: { application/json: { schema: { type: object, required: [orderId], properties: { orderId: { type: string }, userRef: { $ref: "#/components/schemas/UserRef" } } } } } }
      responses: { "200": { description: Order, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/cancel:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Cancel (free before payment / production; refund rules per network).
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef, orderId], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, orderId: { type: string } } } } } }
      responses: { "200": { description: Order, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/link/start:
    post:
      x-networks: [nowtrust, makeitfun]
      summary: "Start linking (MakeItFun, NowTrust): code + deep link the user enters in the seller app."
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, displayName: { type: string } } } } } }
      responses: { "200": { description: Pending or linked, content: { application/json: { schema: { type: object, properties: { status: { type: string, enum: [pending, linked] }, code: { type: [string, "null"] }, deepLink: { type: [string, "null"] }, webLink: { type: string }, expiresInSeconds: { type: integer } } } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/link/status:
    post:
      x-networks: [okidooki, nowtrust, makeitfun]
      summary: Is the user linked, what the account can do (address, card, ID), and the mandate the user set for this agent (`mandate`; NowTrust also `partnerMandate`). order / sos outside the mandate fail with 403 mandate_scope.
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" } } } } } }
      responses: { "200": { description: Status, content: { application/json: { schema: { type: object, properties: { linked: { type: boolean } }, additionalProperties: true } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/connect/start:
    post:
      x-networks: [okidooki]
      summary: Okidooki gateway. Universal "Connect with Okidooki" for your user on several networks at once (one page, one mandate).
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, networks: { type: array, items: { type: string, enum: [okidooki, nowtrust, makeitfun] } }, displayName: { type: string }, mandate: { $ref: "#/components/schemas/Mandate", description: Suggested limits; the user decides. } } } } } }
      responses: { "200": { description: Connect (status pending; url for the user), content: { application/json: { schema: { $ref: "#/components/schemas/Connect" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/connect/status:
    post:
      x-networks: [okidooki]
      summary: Okidooki gateway. Outcome of a connect (poll every 10 s); after approved use link/status on each network.
      requestBody: { content: { application/json: { schema: { type: object, properties: { connectId: { type: string }, userRef: { $ref: "#/components/schemas/UserRef" } } } } } }
      responses: { "200": { description: Connect, content: { application/json: { schema: { $ref: "#/components/schemas/Connect" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/venues/menu:
    post:
      x-networks: [okidooki]
      summary: "Drink or food menu of a venue with prices and item ids"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [venueId]
              properties:
        venueId: { type: string }
        menuType: { type: string, enum: [drinks, food], default: drinks }
        userRef: { $ref: '#/components/schemas/UserRef', description: "Linked user (optional on public catalog routes; sets the city)" }
      responses:
        '200':
          description: "{ venue, count, items: [{ id, name, price, currency, category }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/venues/live:
    post:
      x-networks: [okidooki]
      summary: "Live city pulse: venues ranked by people checked in now, tonight's events, live promotions, friends out"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        cityId: { type: string, description: "Overrides the user's city" }
      responses:
        '200':
          description: "{ venues: [{ id, name, checkedInNow, trend, ... }], events, promotions, friendsOut }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/drinks/cheapest:
    post:
      x-networks: [okidooki]
      summary: "Cheapest drinks across the city's venues"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
        drinkQuery: { type: string, description: "e.g. beer, mojito" }
        cityId: { type: string }
        near: { type: object, description: "Point used to pick the city when no user is linked", properties: { lat: { type: number }, lng: { type: number } } }
        userRef: { $ref: '#/components/schemas/UserRef', description: "Linked user (optional on public catalog routes; sets the city)" }
      responses:
        '200':
          description: "{ count, drinks: [{ venueId, venue, name, price, currency }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/offerings:
    post:
      x-networks: [okidooki]
      summary: "What can be bought at a venue (menu items) or an event (ticket tiers)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
        venueId: { type: string }
        eventId: { type: string }
        userRef: { $ref: '#/components/schemas/UserRef', description: "Linked user (optional on public catalog routes; sets the city)" }
      responses:
        '200':
          description: "{ items: [{ id, name, price, currency }] } or { tiers: [{ ticketTypeId, name, price, available }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/menu/quote:
    post:
      x-networks: [okidooki]
      summary: "Price a drinks/food basket for the linked user; payment happens in the app on the venue page (confirmUrl)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, venueId, items]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        venueId: { type: string }
        items: { type: array, items: { type: object, properties: { id: { type: string }, quantity: { type: integer } } } }
      responses:
        '200':
          description: "{ lines, totalCents, currency, needs_user_confirmation: true, confirmUrl, appUrl }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/guestlist/join:
    post:
      x-networks: [okidooki]
      summary: "Put the linked user on an event's guest list (mandate capability guestlist)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, eventId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        eventId: { type: string }
        plusOnes: { type: integer, minimum: 0, maximum: 10 }
      responses:
        '200':
          description: "{ ok, status, entryId?, waitlisted? } or userLinks when the event needs in-app action"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/guestlist/leave:
    post:
      x-networks: [okidooki]
      summary: "Remove the linked user from an event's guest list"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, eventId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        eventId: { type: string }
      responses:
        '200':
          description: "{ ok }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/promotions:
    post:
      x-networks: [okidooki]
      summary: "Live and upcoming free-of-charge offers (FOC) in the city"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
        query: { type: string }
        cityId: { type: string }
        near: { type: object, description: "Point used to pick the city when no user is linked", properties: { lat: { type: number }, lng: { type: number } } }
        userRef: { $ref: '#/components/schemas/UserRef', description: "Linked user (optional on public catalog routes; sets the city)" }
      responses:
        '200':
          description: "{ count, promotions: [{ id, title, venue, reward, startsAt, endsAt, remaining }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/promotions/claim:
    post:
      x-networks: [okidooki]
      summary: "Claim a promotion for the linked user; the venue approves, the QR shows in the app (capability promotions)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, promotionId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        promotionId: { type: string }
      responses:
        '200':
          description: "{ ok, claimId, message }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/checkin:
    post:
      x-networks: [okidooki]
      summary: "Check the linked user in at a venue (capability checkin)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, venueId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        venueId: { type: string }
      responses:
        '200':
          description: "{ ok, venue }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/djs:
    post:
      x-networks: [okidooki]
      summary: "Bookable DJs and artists with ids"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
        query: { type: string, description: "name, genre or city" }
        userRef: { $ref: '#/components/schemas/UserRef', description: "Linked user (optional on public catalog routes; sets the city)" }
      responses:
        '200':
          description: "{ count, djs: [{ id, stageName, genres, city, rate }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/djs/book:
    post:
      x-networks: [okidooki]
      summary: "DJ booking request for the linked user; the DJ answers with a price in the app (capability book_dj)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, djId, date]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        djId: { type: string }
        date: { type: string, description: "YYYY-MM-DD" }
        time: { type: string, description: "HH:mm (default 22:00)" }
        durationHours: { type: integer, minimum: 1, maximum: 12, default: 3 }
        eventType: { type: string, description: "party, wedding, corporate, club night ..." }
        locationText: { type: string }
        budget: { type: string }
        message: { type: string }
      responses:
        '200':
          description: "{ bookingId, status: requested, dj, date, time, userLinks }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/djs/bookings:
    post:
      x-networks: [okidooki]
      summary: "The linked user's DJ booking requests"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ count, bookings: [{ bookingId, djId, djStageName, date, time, status, price? }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/djs/cancel:
    post:
      x-networks: [okidooki]
      summary: "Cancel a DJ booking request"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, bookingId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        bookingId: { type: string }
      responses:
        '200':
          description: "{ bookingId, status: cancelled }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/tickets:
    post:
      x-networks: [okidooki]
      summary: "The linked user's purchased tickets"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ count, tickets: [{ id, eventId, eventName, tier, status, qr? }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/reservations:
    post:
      x-networks: [okidooki]
      summary: "The linked user's upcoming table reservations"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ count, reservations: [{ id, venueId, venue, date, time, partySize, status }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/reservations/cancel:
    post:
      x-networks: [okidooki]
      summary: "Cancel one of the linked user's table reservations"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, reservationId]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        reservationId: { type: string }
      responses:
        '200':
          description: "{ ok }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/reviews:
    post:
      x-networks: [okidooki, nowtrust]
      summary: "Reviews and average rating of a venue (Okidooki: venueId) or a guard / company (NowTrust: sellerId)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
        venueId: { type: string }
        limit: { type: integer, default: 20, maximum: 50 }
      responses:
        '200':
          description: "{ venueId, count, averageRating, reviews: [{ rating, text, author, createdAt }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/friends/nearby:
    post:
      x-networks: [okidooki]
      summary: "Which of the linked user's friends are out right now and how far (capability social)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ count, outNow, friends: [{ uid, displayName, distanceKm?, venue? }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/profile:
    post:
      x-networks: [okidooki]
      summary: "The linked user's profile with their consent (capability profile)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ uid, displayName, username, cityId, cityName, avatarUrl, partyStatus, partyMode, roles, friendsCount }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/gifts/send:
    post:
      x-networks: [okidooki]
      summary: "Start a gift to another Okidooki user; chosen and paid in the app (capability social)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, toUid]
              properties:
        userRef: { $ref: '#/components/schemas/UserRef' }
        toUid: { type: string }
      responses:
        '200':
          description: "{ needs_user_confirmation: true, userLinks: [{ kind: gift, url, webUrl }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/link/confirm:
    post:
      x-networks: [okidooki]
      summary: Okidooki only. Confirm the pair from the buyer side.
      requestBody: { content: { application/json: { schema: { type: object, required: [okidookiUid, externalUid], properties: { okidookiUid: { type: string }, externalUid: { type: string } } } } } }
      responses: { "200": { description: Linked }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/link/revoke:
    post:
      x-networks: [okidooki, makeitfun]
      summary: End the link for a user.
      requestBody: { content: { application/json: { schema: { type: object, properties: { userRef: { $ref: "#/components/schemas/UserRef" }, externalUid: { type: string } } } } } }
      responses: { "200": { description: Unlinked }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/preview:
    post:
      x-networks: [makeitfun]
      summary: MakeItFun. Render a design on a product (AI prompt, image URL, exact text or a saved design); returns a job to poll. One running preview per user.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [userRef, sellerId, variantId, design]
              properties:
                userRef: { $ref: "#/components/schemas/UserRef" }
                sellerId: { type: string }
                variantId: { type: integer }
                placement: { type: string, description: One of the placements discover(sellerId) lists; omit for the product default. }
                positionMode: { type: string, enum: [centered, stretched, logo_top_left, logo_top_right, logo_bottom_left, logo_bottom_right] }
                baseDesignId: { type: string, description: Add this placement to an earlier design of the same variant. }
                design:
                  type: object
                  properties:
                    kind: { type: string, enum: [prompt, imageUrl, text, savedDesignId, style] }
                    styleId: { type: string, description: "kind style: from discover { suggest } / { styles: true }" }
                    slots: { type: object, description: "kind style: values for the look's slots (name, place, year, team...)" }
                    prompt: { type: string }
                    imageUrl: { type: string, format: uri }
                    editPrompt: { type: string }
                    text: { type: string }
                    font: { type: string, enum: [bebas, anton, archivo, bangers, luckiest, righteous, pacifico, lobster, marker, poppins] }
                    color: { type: string }
                    outlineColor: { type: string }
                    arc: { type: number }
                    designId: { type: string }
      responses: { "200": { description: Job or result, content: { application/json: { schema: { $ref: "#/components/schemas/PreviewResult" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/preview/status:
    post:
      x-networks: [makeitfun]
      summary: "MakeItFun. Result of a preview job (jobId optional: latest job of the user)."
      requestBody: { content: { application/json: { schema: { type: object, properties: { jobId: { type: string }, userRef: { $ref: "#/components/schemas/UserRef" } } } } } }
      responses: { "200": { description: Result, content: { application/json: { schema: { $ref: "#/components/schemas/PreviewResult" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/designs:
    post:
      x-networks: [makeitfun]
      summary: MakeItFun. The linked user's saved designs (My Designs), newest first, with the designIds quote/order accept. Reusing a saved design costs nothing.
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, limit: { type: integer, default: 20, maximum: 50 } } } } } }
      responses: { "200": { description: Designs, content: { application/json: { schema: { type: object, properties: { designs: { type: array, items: { $ref: "#/components/schemas/PreviewResult" } } } } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/styles/feedback:
    post:
      x-networks: [makeitfun]
      summary: "MakeItFun. Thumbs up / down on a curated look (Design Curator ranking signal)."
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef, styleId], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, styleId: { type: string }, vote: { type: string, enum: [up, down] } } } } } }
      responses: { "200": { description: Recorded }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/sandbox/pay:
    post:
      x-networks: [makeitfun]
      summary: MakeItFun, TEST keys only. Settle a sandbox order as if the user paid.
      requestBody: { content: { application/json: { schema: { type: object, required: [orderId], properties: { orderId: { type: string } } } } } }
      responses: { "200": { description: Order paid, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/sandbox/approve:
    post:
      x-networks: [nowtrust]
      summary: NowTrust, TEST keys only. Settle a sandbox order as if the user approved and paid (no charge, no booking, no guard notified).
      requestBody: { content: { application/json: { schema: { type: object, required: [orderId], properties: { orderId: { type: string } } } } } }
      responses: { "200": { description: Order confirmed (sandbox true), content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/categories:
    post:
      x-networks: [makeitfun]
      summary: "Printful catalog categories shippable to a country, with product counts"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                country: { type: string, default: CH }
      responses:
        '200':
          description: "{ country, categories: [{ categoryId, title, imageUrl, parentId, productCount }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/products:
    post:
      x-networks: [makeitfun]
      summary: "Products of a category (sellerId product:<id> for discover)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [categoryId]
              properties:
                categoryId: { type: integer }
                country: { type: string, default: CH }
                limit: { type: integer, default: 60, maximum: 100 }
      responses:
        '200':
          description: "{ country, categoryId, total, products: [{ sellerId, productId, title, imageUrl, brand, techniques, placements, priceFromCHF, allOverPrint }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/sizes:
    post:
      x-networks: [makeitfun]
      summary: "Size chart of a product (available sizes and measurement tables), cached 7 days"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sellerId: { type: string, description: "product:<id>" }
                productId: { type: integer }
                unit: { type: string, enum: [cm, inches], default: cm }
      responses:
        '200':
          description: "{ productId, unit, available: [string], sizeTables: [{ type, unit, description, imageUrl, measurements: [{ name, values: [{ size, value, min, max }] }] }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/shipping:
    post:
      x-networks: [makeitfun]
      summary: "Shipping options and prices (CHF) for variants to a country/zip, or to the linked user's saved address, without a quote"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items: { type: array, items: { type: object, properties: { variantId: { type: integer }, quantity: { type: integer } } } }
                recipient: { type: object, properties: { country_code: { type: string }, zip: { type: string }, city: { type: string }, state_code: { type: string } } }
                userRef: { $ref: '#/components/schemas/UserRef' }
      responses:
        '200':
          description: "{ shipTo, currency, rates: [{ id, name, rateCHF, minDeliveryDays, maxDeliveryDays }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/upload:
    post:
      x-networks: [makeitfun]
      summary: "Upload an image for the linked user's designs (base64 PNG/JPEG/WebP up to 15 MB, or a URL fetched server-side)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                imageBase64: { type: string, description: "raw base64 or data URL" }
                imageUrl: { type: string, format: uri }
      responses:
        '200':
          description: "{ imageUrl } to use as preview design { kind: imageUrl, imageUrl }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/mockups:
    post:
      x-networks: [makeitfun]
      summary: "Extra mockup views (lifestyle, flat, folded, hanger...) of a saved design; done within ~45 s or returns a taskKey to poll"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, designId]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                designId: { type: string }
                optionGroups: { type: array, items: { type: string }, description: "subset of the product's option groups (default: up to 4 lifestyle/flat views)" }
      responses:
        '200':
          description: "{ designId, status: done|pending, taskKey, optionGroups, available, views: [{ title, url, placement }], pollAfterSeconds? }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/mockups/status:
    post:
      x-networks: [makeitfun]
      summary: "Result of a pending mockups task"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, taskKey]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                taskKey: { type: string }
      responses:
        '200':
          description: "{ designId, status, taskKey, views }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/receipt:
    post:
      x-networks: [nowtrust, makeitfun]
      summary: "Stripe receipt of a paid order (NowTrust: orderId or bookingId; MakeItFun: orderId, sandbox orders say sandbox: true)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                orderId: { type: string }
      responses:
        '200':
          description: "{ orderId, status, paymentStatus, amount, currency, receiptUrl, paidAt, refunded, amountRefunded, invoiceUrl }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/sos:
    post:
      x-networks: [nowtrust]
      summary: NowTrust. Raise an SOS for the linked user (dispatch of licensed guards); mandate and card rules apply.
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, location: { type: object, properties: { lat: { type: number }, lng: { type: number } } }, address: { type: string }, emergencyType: { type: string }, description: { type: string } } } } } }
      responses: { "200": { description: Emergency, content: { application/json: { schema: { type: object, additionalProperties: true } } } }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/sos/status:
    post:
      x-networks: [nowtrust]
      summary: NowTrust. Status of an SOS.
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef, emergencyId], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, emergencyId: { type: string } } } } } }
      responses: { "200": { description: Emergency }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/sos/cancel:
    post:
      x-networks: [nowtrust]
      summary: NowTrust. Cancel an SOS (fee rules apply once a guard is en route).
      requestBody: { content: { application/json: { schema: { type: object, required: [userRef, emergencyId], properties: { userRef: { $ref: "#/components/schemas/UserRef" }, emergencyId: { type: string } } } } } }
      responses: { "200": { description: Emergency }, default: { $ref: "#/components/responses/Err" } }
  /a2a/v0/sellers:
    post:
      x-networks: [nowtrust]
      summary: "Profile of a guard (guard:<id>) or company (company:<id>)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sellerId]
              properties:
                sellerId: { type: string }
      responses:
        '200':
          description: "{ sellerId, kind, name, rating, reviewCount, totalJobs, hourlyRate, currency, specializations, languages, bio, city, serviceRadiusKm, availableNow, licensed, company } or { kind: company, guards, rating, hourlyRateFrom, specializations }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/availability:
    post:
      x-networks: [nowtrust]
      summary: "Day-by-day availability of a guard (busy slots from confirmed jobs), up to 30 days"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sellerId]
              properties:
                sellerId: { type: string, description: "guard:<id>" }
                from: { type: string, description: "YYYY-MM-DD (default today)" }
                days: { type: integer, default: 7, maximum: 30 }
      responses:
        '200':
          description: "{ sellerId, from, to, timezone, days: [{ date, busy: [{ startTime, endTime }], free }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/reviews/create:
    post:
      x-networks: [nowtrust]
      summary: "Review a completed booking of the linked user (one review per booking; mandate capability review)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, bookingId, rating]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                bookingId: { type: string }
                rating: { type: integer, minimum: 1, maximum: 5 }
                comment: { type: string }
      responses:
        '200':
          description: "{ reviewId, bookingId, rating, status: published }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/bookings:
    post:
      x-networks: [nowtrust]
      summary: "All bookings of the linked user (in-app and by any agent) plus agent orders awaiting approval"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                limit: { type: integer, default: 20, maximum: 50 }
      responses:
        '200':
          description: "{ count, bookings: [{ bookingId, status, paymentStatus, securityType, date, startTime, endTime, durationHours, address, totalCHF, guard, guardArrivedAt, completedAt, incidentReport }], ordersAwaitingApproval: [{ orderId, when, totalCHF, from }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/bookings/status:
    post:
      x-networks: [nowtrust]
      summary: "One booking in detail (guard, arrival, completion, incident report, review state, guard phone while active)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, bookingId]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                bookingId: { type: string }
      responses:
        '200':
          description: "Booking view + { reviewed, guardPhone }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/messages:
    post:
      x-networks: [nowtrust]
      summary: "Chat history between the linked user and the guard of a booking"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, bookingId]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                bookingId: { type: string }
                limit: { type: integer, default: 50, maximum: 100 }
      responses:
        '200':
          description: "{ bookingId, guard, count, messages: [{ messageId, from: user|guard, text, read, at }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/messages/send:
    post:
      x-networks: [nowtrust]
      summary: "Send a chat message to the guard of an active booking (capability message)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, bookingId, message]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                bookingId: { type: string }
                message: { type: string, maxLength: 2000 }
      responses:
        '200':
          description: "{ messageId, bookingId, status: sent }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/incidents:
    post:
      x-networks: [nowtrust]
      summary: "Incident reports written by guards on the linked user's bookings and SOS calls"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                days: { type: integer, default: 90, maximum: 365 }
      responses:
        '200':
          description: "{ days, count, reports: [{ kind: booking|sos, id, date, at, report, guardId }] }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/series/quote:
    post:
      x-networks: [nowtrust]
      summary: "Quote a multi-day or recurring series for one seller: up to 12 firm quotes (occurrences, or when + durationHours + repeat)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, sellerId]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                sellerId: { type: string }
                when: { type: string, description: "ISO 8601 start of the first occurrence" }
                durationHours: { type: integer, minimum: 1, maximum: 24 }
                repeat: { type: object, properties: { every: { type: string, enum: [day, week] }, count: { type: integer, minimum: 2, maximum: 12 } } }
                occurrences: { type: array, maxItems: 12, items: { type: object, properties: { when: { type: string }, durationHours: { type: integer } } } }
                location: { type: object, properties: { lat: { type: number }, lng: { type: number }, address: { type: string } } }
      responses:
        '200':
          description: "{ sellerId, occurrences, quoted, unavailable: [{ when, reason }], quotes: [{ quoteId, when, durationHours, total, expiresAt }], total, currency, mandate, cardOnFile }"
          content:
            application/json:
              schema:
                type: object
  /a2a/v0/series/order:
    post:
      x-networks: [nowtrust]
      summary: "Book every quote of a series (Idempotency-Key recommended); each booking settles within the mandate or waits for approval in NowTrust"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userRef, quoteIds]
              properties:
                userRef: { $ref: '#/components/schemas/UserRef' }
                quoteIds: { type: array, maxItems: 12, items: { type: string } }
      responses:
        '200':
          description: "{ seriesId, orders: [Order], failed: [{ quoteId, code, reason }], booked, awaitingApproval, confirmUrl }"
          content:
            application/json:
              schema:
                type: object
