openapi: '3.1.0'
info:
  title: PinPoint API
  version: '1.0.0'
  description: Universal customer delivery identity platform API

servers:
  - url: https://recipients.api.oproute.com
    description: Production

security:
  - CognitoRecipient: []

components:
  securitySchemes:
    CognitoRecipient:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Cognito JWT from pinpoint-recipients user pool
    CarrierApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: SHA-256-hashed carrier API key
    DriverJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Oproute Cognito JWT (Driver group)
    PlatformAdminJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Platform admin JWT for carrier management
    LiveSessionJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Ephemeral live-session token for customer or driver

  schemas:
    RecipientProfile:
      type: object
      properties:
        userId: {type: string}
        displayName: {type: string, nullable: true}
        phone: {type: string, nullable: true}
        email: {type: string, nullable: true}
        preferredLanguage: {type: string, nullable: true}
        pushToken: {type: string, nullable: true}
        createdAt: {type: string, format: date-time}
        lastActiveAt: {type: string, format: date-time, nullable: true}

    RecipientDeletionResponse:
      type: object
      properties:
        success: {type: boolean}
        deletionScheduledAt: {type: string, format: date-time}

    RecipientExportResponse:
      type: object
      properties:
        success: {type: boolean}
        exportRequestId: {type: string}
        queuedAt: {type: string, format: date-time}
        status: {type: string, enum: [queued]}
        deliveryChannel: {type: string, enum: [email]}

    NotificationPreferences:
      type: object
      properties:
        deliveryScheduled: {type: boolean}
        oneHourBefore: {type: boolean}
        liveTracking: {type: boolean}
        delivered: {type: boolean}
        failed: {type: boolean}
        addressReview: {type: boolean}
        dataExport: {type: boolean}

    NotificationPreferencesUpdateRequest:
      type: object
      properties:
        deliveryScheduled: {type: boolean}
        oneHourBefore: {type: boolean}
        liveTracking: {type: boolean}
        delivered: {type: boolean}
        failed: {type: boolean}
        addressReview: {type: boolean}
        dataExport: {type: boolean}

    NotificationHistoryItem:
      type: object
      properties:
        notificationId: {type: string}
        trigger:
          type: string
          enum:
            [
              delivery_scheduled,
              one_hour_before,
              live_tracking,
              delivered,
              failed,
              address_review,
              data_export,
            ]
        title: {type: string}
        body: {type: string}
        createdAt: {type: string, format: date-time}
        readAt: {type: string, format: date-time, nullable: true}

    NotificationListResponse:
      type: object
      properties:
        notifications:
          type: array
          items: {$ref: '#/components/schemas/NotificationHistoryItem'}

    PushTokenRequest:
      type: object
      required: [pushToken]
      properties:
        pushToken: {type: string}

    PushTokenResponse:
      type: object
      properties:
        success: {type: boolean}
        pushToken: {type: string}

    CarrierRegisterRequest:
      type: object
      required: [carrierName]
      properties:
        carrierId: {type: string}
        carrierName: {type: string}
        carrierLogoUrl: {type: string, format: uri}
        webhookUrl: {type: string, format: uri}

    CarrierProfile:
      type: object
      properties:
        carrierId: {type: string}
        carrierName: {type: string}
        carrierLogoUrl: {type: string, format: uri, nullable: true}
        webhookUrl: {type: string, format: uri, nullable: true}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
        apiKeyCount: {type: integer}

    CarrierUpdateRequest:
      type: object
      properties:
        carrierName: {type: string}
        carrierLogoUrl: {type: string, format: uri}

    CarrierApiKeyCreateResponse:
      type: object
      properties:
        keyId: {type: string}
        apiKey: {type: string}
        createdAt: {type: string, format: date-time}

    CarrierApiKeyDeleteResponse:
      type: object
      properties:
        success: {type: boolean}
        keyId: {type: string}

    CarrierWebhookUpdateRequest:
      type: object
      required: [webhookUrl]
      properties:
        webhookUrl: {type: string, format: uri}

    CarrierInviteListItem:
      type: object
      properties:
        tokenId: {type: string}
        shipmentId: {type: string}
        status: {type: string, enum: [pending, claimed, expired]}
        createdAt: {type: string, format: date-time}
        expiresAt: {type: string, format: date-time}
        openedAt: {type: string, format: date-time, nullable: true}
        claimedAt: {type: string, format: date-time, nullable: true}

    CarrierInviteListResponse:
      type: object
      properties:
        invites:
          type: array
          items: {$ref: '#/components/schemas/CarrierInviteListItem'}

    CarrierAnalyticsResponse:
      type: object
      properties:
        sent: {type: integer}
        opened: {type: integer}
        claimed: {type: integer}
        completed: {type: integer}

    Address:
      type: object
      required: [addressId, label, streetAddress, coordinateTier]
      properties:
        addressId: {type: string}
        label: {type: string}
        streetAddress: {type: string}
        city: {type: string}
        postcode: {type: string}
        country: {type: string}
        geocodedLat: {type: number}
        geocodedLng: {type: number}
        precisionPinLat: {type: number, nullable: true}
        precisionPinLng: {type: number, nullable: true}
        coordinateTier: {type: integer, minimum: 1, maximum: 5}
        geocodeConfidence: {type: number, minimum: 0, maximum: 1}
        geocodeProvider: {type: string, nullable: true}
        addressHash: {type: string}
        what3words: {type: string, nullable: true}
        plusCode: {type: string, nullable: true}
        deliveryNotes: {type: string, nullable: true}
        parkingInstructions: {type: string, nullable: true}
        accessCode:
          {
            type: string,
            nullable: true,
            description: 'Masked in recipient responses. Returned to drivers only with read:access_code consent.',
          }
        neighbourName: {type: string, nullable: true}
        neighbourAddress: {type: string, nullable: true}
        neighbourUnit: {type: string, nullable: true}
        neighbourAvailable: {type: boolean}
        preferredWindow:
          {type: string, enum: [morning, afternoon, evening, any]}
        isDefault: {type: boolean}
        activeIntelligenceId: {type: string, nullable: true}
        entrancePhotos:
          type: array
          maxItems: 3
          items: {$ref: '#/components/schemas/AddressEntrancePhoto'}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}

    AddressEntrancePhoto:
      type: object
      required: [photoId, objectKey, contentType, status, createdAt]
      properties:
        photoId: {type: string}
        objectKey: {type: string}
        contentType: {type: string, enum: [image/jpeg, image/png, image/webp]}
        fileName: {type: string, nullable: true}
        status: {type: string, enum: [pending, ready]}
        createdAt: {type: string, format: date-time}
        uploadedAt: {type: string, format: date-time, nullable: true}
        viewUrl: {type: string, format: uri, nullable: true}

    AddressEntrancePhotoUploadRequest:
      type: object
      required: [contentType]
      properties:
        contentType: {type: string, enum: [image/jpeg, image/png, image/webp]}
        fileName: {type: string}

    AddressEntrancePhotoUploadResponse:
      type: object
      required: [expiresAt, photo, uploadHeaders, uploadMethod, uploadUrl]
      properties:
        expiresAt: {type: string, format: date-time}
        photo: {$ref: '#/components/schemas/AddressEntrancePhoto'}
        uploadHeaders:
          type: object
          required: [content-type]
          properties:
            content-type: {type: string}
        uploadMethod: {type: string, enum: [PUT]}
        uploadUrl: {type: string, format: uri}

    AddressUpsertRequest:
      type: object
      required: [label, streetAddress, coordinateTier]
      properties:
        addressId: {type: string}
        label: {type: string}
        streetAddress: {type: string}
        city: {type: string}
        postcode: {type: string}
        country: {type: string}
        geocodedLat: {type: number}
        geocodedLng: {type: number}
        precisionPinLat: {type: number, nullable: true}
        precisionPinLng: {type: number, nullable: true}
        coordinateTier: {type: integer, minimum: 1, maximum: 5}
        geocodeConfidence: {type: number, minimum: 0, maximum: 1}
        what3words: {type: string, nullable: true}
        plusCode: {type: string, nullable: true}
        deliveryNotes: {type: string, nullable: true}
        parkingInstructions: {type: string, nullable: true}
        accessCode: {type: string, nullable: true}
        neighbourName: {type: string, nullable: true}
        neighbourAddress: {type: string, nullable: true}
        neighbourUnit: {type: string, nullable: true}
        neighbourAvailable: {type: boolean}
        preferredWindow:
          {type: string, enum: [morning, afternoon, evening, any]}
        isDefault: {type: boolean}

    AddressListResponse:
      type: object
      properties:
        addresses:
          type: array
          items: {$ref: '#/components/schemas/Address'}

    AddressIntelligence:
      type: object
      properties:
        addressHash: {type: string}
        correctedLat: {type: number, nullable: true}
        correctedLng: {type: number, nullable: true}
        correctedParkingInstructions: {type: string, nullable: true}
        correctedDeliveryNotes: {type: string, nullable: true}
        feedbackCount: {type: integer}
        positiveCount: {type: integer}
        negativeCount: {type: integer}
        confidenceAdjustment: {type: number}
        aiGeneratedInstruction: {type: string, nullable: true}
        lastUpdated: {type: string, format: date-time}
        reviewedByDispatcher: {type: boolean}

    AddressFeedbackRequest:
      type: object
      required: [shipmentId, addressHash, feedbackType]
      properties:
        shipmentId: {type: string}
        addressHash: {type: string}
        feedbackType:
          type: string
          enum:
            [
              correct,
              wrong_entrance,
              parking_wrong,
              building_number_diff,
              access_changed,
              other,
            ]
        notes: {type: string, nullable: true}
        actualLat: {type: number, nullable: true}
        actualLng: {type: number, nullable: true}

    AddressFeedbackResponse:
      type: object
      properties:
        success: {type: boolean}
        addressHash: {type: string}
        feedbackCount: {type: integer}

    AddressOwnerNotifyResponse:
      type: object
      properties:
        success: {type: boolean}
        addressHash: {type: string}
        notifiedCount: {type: integer}

    VerificationIssueRequest:
      type: object
      required: [shipmentId]
      properties:
        shipmentId: {type: string}

    VerificationIssueResponse:
      type: object
      properties:
        verificationId: {type: string}
        code: {type: string}
        qrPayload: {type: string}
        expiresAt: {type: string, format: date-time}
        oneTime: {type: boolean}

    VerificationVerifyRequest:
      type: object
      properties:
        presentedCode: {type: string}
        scannedJws: {type: string}
        lat: {type: number}
        lng: {type: number}

    VerificationVerifyResponse:
      type: object
      properties:
        valid: {type: boolean}
        consumedAt: {type: string, format: date-time}

    ConsentRecord:
      type: object
      properties:
        carrierId: {type: string}
        carrierName: {type: string}
        carrierLogoUrl: {type: string, nullable: true}
        scope: {type: array, items: {type: string}}
        grantedAt: {type: string, format: date-time}
        expiresAt: {type: string, format: date-time, nullable: true}
        revokedAt: {type: string, format: date-time, nullable: true}
        grantMethod: {type: string, enum: [invite, manual]}
        auditTrail:
          type: array
          items:
            type: object
            properties:
              action: {type: string, enum: [granted, updated, revoked]}
              consentPresentation:
                {type: string, enum: [default_bundle, manual]}
              ip: {type: string, nullable: true}
              scope: {type: array, items: {type: string}}
              timestamp: {type: string, format: date-time}

    ConsentListResponse:
      type: object
      properties:
        consents:
          type: array
          items: {$ref: '#/components/schemas/ConsentRecord'}

    ConsentUpdateRequest:
      type: object
      required: [scope]
      properties:
        scope: {type: array, items: {type: string}}

    ConsentWithdrawAllResponse:
      type: object
      properties:
        success: {type: boolean}
        revokedCount: {type: integer}

    CarrierRecipientConsentResponse:
      type: object
      properties:
        active: {type: boolean}
        carrierId: {type: string}
        consentScopes: {type: array, items: {type: string}}
        revokedAt: {type: string, format: date-time, nullable: true}
        userId: {type: string}

    DriverContextResponse:
      type: object
      required: [shipmentId, recipientProfileFound, consentValid]
      properties:
        shipmentId: {type: string}
        recipientProfileFound: {type: boolean}
        consentValid: {type: boolean}
        contextRevision: {type: string}
        address:
          type: object
          nullable: true
          properties:
            formattedAddress: {type: string}
            deliveryLat: {type: number}
            deliveryLng: {type: number}
            coordinateTier: {type: integer}
            coordinateTierLabel: {type: string}
            geocodeConfidence: {type: number}
            what3words: {type: string, nullable: true}
            plusCode: {type: string, nullable: true}
        deliveryInstructions:
          type: object
          nullable: true
          properties:
            notes: {type: string, nullable: true}
            parkingInstructions: {type: string, nullable: true}
            accessCode: {type: string, nullable: true}
            preferredWindow: {type: string, nullable: true}
        neighbourFallback:
          type: object
          nullable: true
          properties:
            available: {type: boolean}
            name: {type: string, nullable: true}
            address: {type: string, nullable: true}
            unit: {type: string, nullable: true}
            additionalNote: {type: string, nullable: true}
        addressIntelligence:
          type: object
          nullable: true
          properties:
            hasCorrections: {type: boolean}
            correctionNote: {type: string, nullable: true}
            correctedLat: {type: number, nullable: true}
            correctedLng: {type: number, nullable: true}
            confidence: {type: number}
        liveLocationAvailable: {type: boolean}
        liveSessionId: {type: string, nullable: true}
        entrancePhotos:
          type: array
          items:
            type: object
            required: [photoId, uploadedAt, viewUrl]
            properties:
              photoId: {type: string}
              uploadedAt: {type: string, format: date-time}
              viewUrl: {type: string, format: uri}
        meta:
          type: object
          properties:
            profileLastUpdated:
              {type: string, format: date-time, nullable: true}
            consentGrantedAt: {type: string, format: date-time, nullable: true}
            consentScopes: {type: array, items: {type: string}}

    DeliveryFeedItem:
      type: object
      properties:
        deliveryId: {type: string}
        carrierId: {type: string}
        carrierName: {type: string}
        carrierLogoUrl: {type: string, nullable: true}
        trackingNumber: {type: string, nullable: true}
        status:
          {
            type: string,
            enum:
              [
                scheduled,
                in_transit,
                out_for_delivery,
                delivered,
                attempted,
                returned,
              ],
          }
        statusHistory:
          type: array
          items:
            type: object
            properties:
              status:
                {
                  type: string,
                  enum:
                    [
                      scheduled,
                      in_transit,
                      out_for_delivery,
                      delivered,
                      attempted,
                      returned,
                    ],
                }
              timestamp: {type: string, format: date-time}
              note: {type: string, nullable: true}
        expectedDate: {type: string, nullable: true}
        deliveredAt: {type: string, format: date-time, nullable: true}
        addressId: {type: string, nullable: true}
        pinpointShipmentId: {type: string}
        lastUpdated: {type: string, format: date-time}

    DeliveryFeedListResponse:
      type: object
      properties:
        deliveries:
          type: array
          items: {$ref: '#/components/schemas/DeliveryFeedItem'}

    InviteCreateRequest:
      type: object
      required: [idempotencyKey, shipmentId, recipientEmail, carrierBrand]
      properties:
        idempotencyKey: {type: string}
        shipmentId: {type: string}
        recipientEmail: {type: string, format: email}
        carrierBrand: {type: string}
        expectedDeliveryDate: {type: string, format: date}
        deliveryChannel: {type: string, enum: [email], default: email}
        defaultAddressId: {type: string}

    InviteCreateResponse:
      type: object
      properties:
        tokenId: {type: string}
        deepLink: {type: string, format: uri}
        expiresAt: {type: string, format: date-time}
        status: {type: string, enum: [pending, claimed, expired]}

    InvitePublicLookupResponse:
      type: object
      properties:
        tokenId: {type: string}
        status: {type: string, enum: [pending, claimed, expired]}
        carrierName: {type: string}
        carrierLogoUrl: {type: string, nullable: true}
        shipmentReference: {type: string}
        expectedDeliveryDate: {type: string, nullable: true}

    InviteClaimResponse:
      type: object
      properties:
        success: {type: boolean}
        consentGranted: {type: boolean}
        consentScopes: {type: array, items: {type: string}}

    InviteStatusResponse:
      type: object
      properties:
        tokenId: {type: string}
        status: {type: string, enum: [pending, claimed, expired]}
        expiresAt: {type: string, format: date-time}
        claimedAt: {type: string, format: date-time, nullable: true}

    LiveSessionCreateRequest:
      type: object
      required: [shipmentId]
      properties:
        shipmentId: {type: string}
        driverName: {type: string}
        stopsAhead: {type: integer}
        triggerChannel: {type: string, enum: [push, silent], default: push}

    LiveSessionCreateResponse:
      type: object
      properties:
        sessionId: {type: string}
        customerToken: {type: string}
        driverToken: {type: string}
        wsEndpoint: {type: string, format: uri}
        expiresAt: {type: string, format: date-time}
        customerNotified: {type: boolean}

    LiveSessionStatusResponse:
      type: object
      properties:
        sessionId: {type: string}
        shipmentId: {type: string}
        recipientUserId: {type: string}
        carrierId: {type: string}
        status: {type: string, enum: [active, ended]}
        endReason:
          {
            type: string,
            enum: [delivered, timeout, customer_ended, driver_ended, error],
            nullable: true,
          }
        createdAt: {type: string, format: date-time}
        expiresAt: {type: string, format: date-time}
        endedAt: {type: string, format: date-time, nullable: true}
        driverName: {type: string, nullable: true}
        stopsAhead: {type: integer, nullable: true}
        triggerChannel: {type: string, enum: [push, silent]}
        customerNotified: {type: boolean}
        customerTokenHash: {type: string}
        driverTokenHash: {type: string}
        messages:
          type: array
          items: {$ref: '#/components/schemas/QuickReplyMessage'}

    LiveSessionEndResponse:
      type: object
      properties:
        success: {type: boolean}
        sessionId: {type: string}
        reason:
          {
            type: string,
            enum: [delivered, timeout, customer_ended, driver_ended, error],
          }

    QuickReplyRequest:
      type: object
      required: [replyCode]
      properties:
        replyCode:
          {
            type: string,
            enum:
              [
                ON_MY_WAY,
                FIVE_MIN,
                USE_SIDE_DOOR,
                LEAVE_SAFE,
                IM_HOME,
                ARRIVED,
              ],
          }

    QuickReplyMessage:
      type: object
      properties:
        messageId: {type: string}
        sessionId: {type: string}
        from: {type: string, enum: [customer, driver]}
        replyCode:
          {
            type: string,
            enum:
              [
                ON_MY_WAY,
                FIVE_MIN,
                USE_SIDE_DOOR,
                LEAVE_SAFE,
                IM_HOME,
                ARRIVED,
              ],
          }
        ts: {type: string, format: date-time}

    QuickReplyMessageListResponse:
      type: object
      properties:
        messages:
          type: array
          items: {$ref: '#/components/schemas/QuickReplyMessage'}

    GeocodeRequest:
      type: object
      required: [address]
      properties:
        address: {type: string}
        countryCode: {type: string, minLength: 2, maxLength: 2}

    ReverseGeocodeRequest:
      type: object
      required: [lat, lng]
      properties:
        lat: {type: number}
        lng: {type: number}

    GeocodeResponse:
      type: object
      properties:
        formattedAddress: {type: string}
        lat: {type: number}
        lng: {type: number}
        confidence: {type: number}
        coordinateTier: {type: integer}
        needsManualConfirmation: {type: boolean}
        agreement: {type: string, enum: [high, medium, low, single]}
        addressHash: {type: string}
        intelligence: {type: object, properties: {available: {type: boolean}}}
        providers:
          type: object
          additionalProperties:
            type: object
            properties:
              lat: {type: number}
              lng: {type: number}
              confidence: {type: number}

    PlaceSearchResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              formattedAddress: {type: string}
              lat: {type: number}
              lng: {type: number}
              addressHash: {type: string}

    CarrierEventRequest:
      type: object
      required: [eventType, shipmentId, status, timestamp]
      properties:
        eventId: {type: string}
        eventType:
          {
            type: string,
            enum:
              [
                DELIVERY_CREATED,
                STATUS_UPDATED,
                DELIVERY_COMPLETED,
                DELIVERY_FAILED,
              ],
          }
        shipmentId: {type: string}
        recipientPhone: {type: string}
        recipientEmail: {type: string, format: email}
        trackingNumber: {type: string}
        status:
          {
            type: string,
            enum:
              [
                scheduled,
                in_transit,
                out_for_delivery,
                delivered,
                attempted,
                returned,
              ],
          }
        statusNote: {type: string}
        expectedDate: {type: string, format: date}
        timestamp: {type: string, format: date-time}

    CarrierEventResponse:
      type: object
      properties:
        eventAccepted: {type: boolean}
        shipmentId: {type: string}
        deliveryId: {type: string}

paths:
  /v1/invite:
    post:
      operationId: createInvite
      security: [{CarrierApiKey: []}]
      summary: Generate an invite token for a shipment recipient
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/InviteCreateRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InviteCreateResponse'}

  /v1/invite/{token}:
    get:
      operationId: getInvite
      security: []
      summary: Validate invite token and return carrier context
      parameters:
        - name: token
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InvitePublicLookupResponse'}
        '404':
          description: Token not found or expired

  /v1/invite/{token}/claim:
    post:
      operationId: claimInvite
      security: [{CognitoRecipient: []}]
      summary: Claim invite token and grant default consent
      parameters:
        - name: token
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InviteClaimResponse'}

  /v1/invite/{token}/status:
    get:
      operationId: getInviteStatus
      security: [{CarrierApiKey: []}]
      summary: Check whether an invite token is pending, claimed, or expired
      parameters:
        - name: token
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InviteStatusResponse'}

  /v1/recipient/me:
    get:
      operationId: getRecipient
      summary: Get own recipient profile
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/RecipientProfile'}
    put:
      operationId: updateRecipient
      summary: Update own profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: {type: string}
                preferredLanguage: {type: string}
                pushToken: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/RecipientProfile'}
    delete:
      operationId: deleteRecipient
      summary: Initiate account deletion
      responses:
        '202':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/RecipientDeletionResponse'}

  /v1/recipient/me/export:
    post:
      operationId: requestRecipientExport
      summary: Queue a GDPR data export for the authenticated recipient
      responses:
        '202':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/RecipientExportResponse'}

  /v1/recipient/me/notifications:
    get:
      operationId: listRecipientNotifications
      summary: List notification history for the authenticated recipient
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/NotificationListResponse'}

  /v1/recipient/me/notification-preferences:
    put:
      operationId: updateRecipientNotificationPreferences
      summary: Update recipient notification preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                $ref: '#/components/schemas/NotificationPreferencesUpdateRequest',
              }
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/NotificationPreferences'}

  /v1/recipient/me/push-token:
    post:
      operationId: registerRecipientPushToken
      summary: Register or update the recipient push token
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PushTokenRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PushTokenResponse'}

  /v1/recipient/me/addresses:
    get:
      operationId: listAddresses
      summary: List saved delivery addresses
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AddressListResponse'}
    post:
      operationId: createAddress
      summary: Add a new delivery address
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AddressUpsertRequest'}
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}

  /v1/recipient/me/addresses/{addressId}:
    get:
      operationId: getAddress
      summary: Get a saved delivery address
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}
    put:
      operationId: updateAddress
      summary: Update a saved delivery address
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AddressUpsertRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}
    delete:
      operationId: deleteAddress
      summary: Delete a saved delivery address
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: {type: boolean}

  /v1/recipient/me/addresses/{addressId}/default:
    post:
      operationId: setDefaultAddress
      summary: Set a saved delivery address as default
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}

  /v1/recipient/me/addresses/{addressId}/intelligence:
    get:
      operationId: getAddressIntelligence
      summary: Get address intelligence summary
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AddressIntelligence'}

  /v1/recipient/me/addresses/{addressId}/entrance-photos/upload-url:
    post:
      operationId: createAddressEntrancePhotoUploadUrl
      summary: Create a presigned upload URL for an entrance photo
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {$ref: '#/components/schemas/AddressEntrancePhotoUploadRequest'}
      responses:
        '200':
          content:
            application/json:
              schema:
                {
                  $ref: '#/components/schemas/AddressEntrancePhotoUploadResponse',
                }

  /v1/recipient/me/addresses/{addressId}/entrance-photos/{photoId}/complete:
    post:
      operationId: completeAddressEntrancePhotoUpload
      summary: Mark a previously uploaded entrance photo as ready for driver use
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
        - name: photoId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}

  /v1/recipient/me/addresses/{addressId}/entrance-photos/{photoId}:
    delete:
      operationId: deleteAddressEntrancePhoto
      summary: Remove an entrance photo from a saved address
      parameters:
        - name: addressId
          in: path
          required: true
          schema: {type: string}
        - name: photoId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Address'}

  /v1/recipient/me/consents:
    get:
      operationId: listConsents
      summary: List recipient consents
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConsentListResponse'}

  /v1/recipient/me/consents/{carrierId}:
    get:
      operationId: getConsent
      summary: Get a single recipient consent
      parameters:
        - name: carrierId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConsentRecord'}
    put:
      operationId: updateConsent
      summary: Update scopes for a recipient consent
      parameters:
        - name: carrierId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ConsentUpdateRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConsentRecord'}
    delete:
      operationId: revokeConsent
      summary: Revoke a recipient consent
      parameters:
        - name: carrierId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConsentRecord'}

  /v1/recipient/me/consents/withdraw-all:
    post:
      operationId: withdrawAllConsents
      summary: Revoke all recipient consents
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConsentWithdrawAllResponse'}

  /v1/carrier/register:
    post:
      operationId: registerCarrier
      security: [{PlatformAdminJwt: []}]
      summary: Register a carrier for PinPoint API access
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CarrierRegisterRequest'}
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierProfile'}

  /v1/carrier/me:
    get:
      operationId: getCarrierProfile
      security: [{CarrierApiKey: []}]
      summary: Get the authenticated carrier profile
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierProfile'}
    put:
      operationId: updateCarrierProfile
      security: [{CarrierApiKey: []}]
      summary: Update the authenticated carrier profile
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CarrierUpdateRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierProfile'}

  /v1/carrier/me/api-keys:
    post:
      operationId: createCarrierApiKey
      security: [{CarrierApiKey: []}]
      summary: Generate a new carrier API key
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierApiKeyCreateResponse'}

  /v1/carrier/me/api-keys/{keyId}:
    delete:
      operationId: revokeCarrierApiKey
      security: [{CarrierApiKey: []}]
      summary: Revoke a carrier API key
      parameters:
        - name: keyId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierApiKeyDeleteResponse'}

  /v1/carrier/me/webhook:
    put:
      operationId: updateCarrierWebhook
      security: [{CarrierApiKey: []}]
      summary: Update the carrier webhook URL
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CarrierWebhookUpdateRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierProfile'}

  /v1/carrier/me/invites:
    get:
      operationId: listCarrierInvites
      security: [{CarrierApiKey: []}]
      summary: List invites sent by the authenticated carrier
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierInviteListResponse'}

  /v1/carrier/me/analytics:
    get:
      operationId: getCarrierAnalytics
      security: [{CarrierApiKey: []}]
      summary: Get invite funnel analytics for the authenticated carrier
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierAnalyticsResponse'}

  /v1/carrier/me/recipients/{userId}/consent:
    get:
      operationId: getCarrierRecipientConsent
      security: [{CarrierApiKey: []}]
      summary: Get consent status for a recipient under the current carrier
      parameters:
        - name: userId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema:
                {$ref: '#/components/schemas/CarrierRecipientConsentResponse'}

  /v1/recipient/me/deliveries:
    get:
      operationId: listDeliveries
      summary: List recipient deliveries
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/DeliveryFeedListResponse'}

  /v1/recipient/me/deliveries/{deliveryId}:
    get:
      operationId: getDelivery
      summary: Get delivery detail
      parameters:
        - name: deliveryId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/DeliveryFeedItem'}

  /v1/driver-context/{shipmentId}:
    get:
      operationId: getDriverContext
      security: [{CarrierApiKey: []}, {DriverJwt: []}]
      summary: Get full driver context package for a shipment
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/DriverContextResponse'}

  /v1/driver-context/by-recipient/{userId}:
    get:
      operationId: getDriverContextByRecipient
      security: [{CarrierApiKey: []}]
      summary: Get driver context using the recipient default address
      parameters:
        - name: userId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/DriverContextResponse'}

  /v1/live-session:
    post:
      operationId: createLiveSession
      security: [{CarrierApiKey: []}]
      summary: Create a live location session
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/LiveSessionCreateRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LiveSessionCreateResponse'}

  /v1/live-session/{sessionId}:
    get:
      operationId: getLiveSession
      security: [{CarrierApiKey: []}, {LiveSessionJwt: []}]
      summary: Get live session status
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LiveSessionStatusResponse'}
    delete:
      operationId: endLiveSession
      security: [{CarrierApiKey: []}]
      summary: End a live session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LiveSessionEndResponse'}

  /v1/live-session/by-shipment/{shipmentId}:
    get:
      operationId: getLiveSessionByShipment
      security: [{CarrierApiKey: []}]
      summary: Get active or ended live session by shipment
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LiveSessionStatusResponse'}

  /v1/live-session/{sessionId}/messages:
    get:
      operationId: listLiveSessionMessages
      security: [{LiveSessionJwt: []}]
      summary: List quick replies for a live session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema:
                {$ref: '#/components/schemas/QuickReplyMessageListResponse'}
    post:
      operationId: createLiveSessionMessage
      security: [{LiveSessionJwt: []}]
      summary: Send a quick reply for a live session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/QuickReplyRequest'}
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/QuickReplyMessage'}

  /v1/geocode:
    post:
      operationId: geocode
      security: [{CognitoRecipient: []}, {CarrierApiKey: []}]
      summary: Geocode an address using the local deterministic provider for development
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/GeocodeRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/GeocodeResponse'}

  /v1/geocode/reverse:
    post:
      operationId: reverseGeocode
      security: [{CognitoRecipient: []}]
      summary: Reverse geocode a latitude and longitude
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ReverseGeocodeRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/GeocodeResponse'}

  /v1/places/search:
    get:
      operationId: searchPlaces
      security: [{CognitoRecipient: []}]
      summary: Search places for address entry assistance
      parameters:
        - name: q
          in: query
          required: false
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PlaceSearchResponse'}

  /v1/what3words/{words}:
    get:
      operationId: resolveWhat3Words
      security: [{CognitoRecipient: []}]
      summary: Resolve a What3Words string to coordinates
      parameters:
        - name: words
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/GeocodeResponse'}

  /v1/pluscode/{code}:
    get:
      operationId: resolvePlusCode
      security: [{CognitoRecipient: []}]
      summary: Resolve a Plus Code to coordinates
      parameters:
        - name: code
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/GeocodeResponse'}

  /v1/address-feedback:
    post:
      operationId: submitAddressFeedback
      security: [{DriverJwt: []}]
      summary: Submit delivery outcome feedback for an address
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AddressFeedbackRequest'}
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AddressFeedbackResponse'}

  /v1/address-intelligence/{hash}:
    get:
      operationId: getAddressIntelligenceByHash
      security: [{CarrierApiKey: []}, {DriverJwt: []}]
      summary: Get intelligence for a normalized address hash
      parameters:
        - name: hash
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AddressIntelligence'}

  /v1/address-intelligence/{hash}/notify-owner:
    post:
      operationId: notifyAddressOwner
      security: [{CarrierApiKey: []}]
      summary: Notify the address owner that review is needed
      parameters:
        - name: hash
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AddressOwnerNotifyResponse'}

  /v1/verification:
    post:
      operationId: issueVerification
      security: [{CarrierApiKey: []}]
      summary: Issue a one-time handover verification token
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/VerificationIssueRequest'}
      responses:
        '201':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/VerificationIssueResponse'}

  /v1/verification/{verificationId}/verify:
    post:
      operationId: verifyVerification
      security: [{DriverJwt: []}]
      summary: Verify a presented PIN or scanned QR handover token
      parameters:
        - name: verificationId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/VerificationVerifyRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/VerificationVerifyResponse'}

  /v1/carrier-events:
    post:
      operationId: pushCarrierEvent
      security: [{CarrierApiKey: []}]
      summary: Push delivery status update to the recipient feed
      parameters:
        - name: x-pinpoint-signature
          in: header
          required: true
          description: HMAC-SHA256 signature of the raw request body
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CarrierEventRequest'}
      responses:
        '200':
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CarrierEventResponse'}
