openapi: 3.0.3
info:
  title: Reachr API Documentation
  version: 1.0.0
  description: |
    This document describes the Reachr API, including OAuth 2.0 endpoints for external clients
    (e.g., Zapier) and endpoints used by Zapier to verify connection and manage webhooks.

    ## Authentication

    The API uses OAuth 2.0 for authentication and authorization. Clients must obtain an access token
    via the OAuth 2.0 Authorization Code flow or Refresh Token flow.

    ### Scopes

    - `zapier:read`: Required to verify connection and fetch minimal user profile.
    - `contacts:read`: Required to manage webhook subscriptions.

    ## Error Handling

    Errors are returned using standard HTTP status codes along with a JSON body containing
    an `error` code and optional `error_description`.

    ## Contact

    For questions or support, please contact <support@reachr.co>.

servers:
  - url: https://app.reachr.co/api

tags:
  - name: OAuth
    description: OAuth 2.0 endpoints used by external clients (e.g., Zapier) to obtain and refresh tokens.
  - name: Zapier
    description: Endpoints used by Zapier to verify connection and manage webhooks.

paths:
  /integrations/zapier/me:
    get:
      tags: [Zapier]
      summary: Verify connection and return minimal user profile
      description: |
        Validates a **Bearer** access token and returns a minimal user shape
        (`email`, `userId`). The token must include the `zapier:read` scope.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Successfully verified token and fetched user profile.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ZapierMeResponse"
              examples:
                success:
                  value:
                    email: user@reachr.co
                    userId: user_123
        "401":
          description: Invalid or missing Bearer token.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                invalid_token:
                  value: { error: "invalid_token" }
        "403":
          description: Insufficient scope (`zapier:read` required).
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                insufficient_scope:
                  value: { error: "insufficient_scope" }
        "500":
          description: Server error.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                server_error:
                  value: { error: "server_error" }

  /integrations/zapier/hooks/subscribe:
    post:
      tags: [Zapier]
      summary: Subscribe a Zap (register webhook target)
      description: |
        Registers a Zapier `hookUrl` for the authenticated user. The access token
        must include the `contacts:read` scope. On success, returns a subscription `id`
        that Zapier will later pass to **DELETE** for unsubscription.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubscribeRequest"
            examples:
              subscribe:
                value:
                  hookUrl: "https://hooks.zapier.com/hooks/standard/123456/abcdef/"
      responses:
        "200":
          description: Subscription created or updated.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscribeResponse"
              examples:
                ok:
                  value: { id: "sub_01J8X6Z8F6N9VWT6X2" }
        "400":
          description: Bad request (e.g., missing/invalid `hookUrl` or content-type).
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                invalid_request:
                  value: { error: "invalid_request" }
        "401":
          description: Invalid or missing Bearer token.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                invalid_token:
                  value: { error: "invalid_token" }
        "403":
          description: Insufficient scope (`contacts:read` required).
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                insufficient_scope:
                  value: { error: "insufficient_scope" }
        "500":
          description: Server error while creating subscription.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                server_error:
                  value: { error: "server_error" }

    delete:
      tags: [Zapier]
      summary: Unsubscribe a Zap (remove webhook target)
      description: |
        Deletes a webhook subscription for the authenticated user. The access token
        must include the `contacts:read` scope. Zapier passes the `id` returned from the
        **POST** call. A successful deletion returns an empty JSON object.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UnsubscribeRequest"
            examples:
              unsubscribe:
                value:
                  id: "sub_01J8X6Z8F6N9VWT6X2"
      responses:
        "200":
          description: Subscription deleted.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
              examples:
                ok:
                  value: {}
        "400":
          description: Bad request (e.g., missing/invalid `id`).
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                invalid_request:
                  value: { error: "invalid_request" }
        "401":
          description: Invalid or missing Bearer token.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                invalid_token:
                  value: { error: "invalid_token" }
        "403":
          description: Insufficient scope (`contacts:read` required).
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                insufficient_scope:
                  value: { error: "insufficient_scope" }
        "404":
          description: Subscription not found for the user.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                not_found:
                  value: { error: "not_found" }
        "500":
          description: Server error while deleting subscription.
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                server_error:
                  value: { error: "server_error" }

  /oauth/authorize:
    get:
      tags: [OAuth]
      summary: OAuth 2.0 Authorization Endpoint
      description: |
        Implements [RFC 6749 Section 3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1).

        This endpoint is used by OAuth 2.0 clients (e.g., Zapier) to obtain an authorization code
        on behalf of an authenticated user.

        The user must be logged in to Reachr. On success, the user is redirected back to the
        provided `redirect_uri` with an authorization `code` and the optional `state`.
      parameters:
        - in: query
          name: client_id
          required: true
          schema:
            type: string
          description: The client identifier issued during client registration.
        - in: query
          name: redirect_uri
          required: true
          schema:
            type: string
            format: uri
          description: Must exactly match one of the redirect URIs registered for the client.
        - in: query
          name: response_type
          required: true
          schema:
            type: string
            enum: [code]
          description: The response type. Only `"code"` is supported.
        - in: query
          name: scope
          required: false
          schema:
            type: string
          description: Space-delimited list of scopes requested.
        - in: query
          name: state
          required: false
          schema:
            type: string
          description: An opaque value used by the client to maintain state and mitigate CSRF.
        - in: query
          name: code_challenge
          required: false
          schema:
            type: string
          description: PKCE code challenge (optional).
        - in: query
          name: code_challenge_method
          required: false
          schema:
            type: string
            enum: [S256, plain]
          description: PKCE code challenge method (optional).
      responses:
        "302":
          description: Redirect back to the client with either an authorization code or an error.
          headers:
            Location:
              schema:
                type: string
              description: >
                The registered redirect URI with appended query parameters:
                - `code` (authorization code) if successful.
                - `error` (error code) and optional `state` if failure.
        "400":
          description: Invalid request (e.g., bad redirect URI, invalid scope).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: invalid_request
        "401":
          description: Unauthorized client or user not logged in.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: unauthorized_client
        "500":
          description: Server error.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: server_error

  /oauth/token:
    post:
      tags: [OAuth]
      summary: OAuth 2.0 Token Endpoint
      description: |
        Implements [RFC 6749 Section 3.2](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2).

        Accepts either:
        - **Authorization Code grant** (`grant_type=authorization_code`)
        - **Refresh Token grant** (`grant_type=refresh_token`)

        Client authentication is supported via:
        - **HTTP Basic** (`Authorization: Basic base64(client_id:client_secret)`)
        - **Form-POST** parameters (`client_id` and `client_secret` in the body)

        On success, returns a JSON body containing `access_token`, `token_type`, `expires_in`,
        and optionally `refresh_token` and `scope`.
      parameters:
        - in: header
          name: Authorization
          required: false
          description: HTTP Basic client authentication header (`Basic base64(client_id:client_secret)`).
          schema:
            type: string
            example: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - title: Authorization Code Grant
                  type: object
                  required:
                    - grant_type
                    - code
                    - redirect_uri
                  properties:
                    grant_type:
                      type: string
                      enum: [authorization_code]
                    code:
                      type: string
                      description: The authorization code previously issued by `/oauth/authorize`.
                    redirect_uri:
                      type: string
                      format: uri
                      description: Must exactly match the redirect URI used during authorization.
                    client_id:
                      type: string
                      description: Client identifier (required if not using HTTP Basic).
                    client_secret:
                      type: string
                      description: Client secret (required if not using HTTP Basic).
                - title: Refresh Token Grant
                  type: object
                  required:
                    - grant_type
                    - refresh_token
                  properties:
                    grant_type:
                      type: string
                      enum: [refresh_token]
                    refresh_token:
                      type: string
                      description: A valid, unexpired refresh token issued by this endpoint.
                    client_id:
                      type: string
                      description: Client identifier (required if not using HTTP Basic).
                    client_secret:
                      type: string
                      description: Client secret (required if not using HTTP Basic).
            examples:
              authorization_code:
                summary: Exchange code for tokens
                value:
                  grant_type: authorization_code
                  code: one_time_code_abc123
                  redirect_uri: https://zapier.com/dashboard/auth/oauth/return/App229042/
                  client_id: reachr-zapier-client-id
                  client_secret: s3cr3t
              refresh_token:
                summary: Rotate access token with a refresh token
                value:
                  grant_type: refresh_token
                  refresh_token: refresh_xyz987
                  client_id: reachr-zapier-client-id
                  client_secret: s3cr3t
      responses:
        "200":
          description: Successful token response.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              examples:
                authorization_code_success:
                  summary: Tokens issued from authorization code
                  value:
                    access_token: access_4f3c2d...
                    token_type: Bearer
                    expires_in: 3600
                    refresh_token: refresh_9a8b7c...
                    scope: zapier:read
                refresh_token_success:
                  summary: New access token from refresh token
                  value:
                    access_token: access_new_12345
                    token_type: Bearer
                    expires_in: 3600
                    refresh_token: refresh_new_67890
                    scope: zapier:read
        "400":
          description: Invalid request or grant (e.g., missing parameters, invalid/expired code or refresh token, unsupported grant).
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                invalid_request:
                  value: { error: "invalid_request" }
                invalid_grant:
                  value: { error: "invalid_grant" }
                unauthorized_client:
                  value: { error: "unauthorized_client" }
                unsupported_grant_type:
                  value: { error: "unsupported_grant_type" }
        "401":
          description: Invalid client authentication (inactive client, wrong auth method, bad secret).
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                invalid_client_basic:
                  value: { error: "invalid_client" }
        "500":
          description: Server error.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
              examples:
                server_error:
                  value: { error: "server_error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Send an OAuth 2.0 Bearer access token in the `Authorization` header:

        `Authorization: Bearer <access_token>`

        Token must include the `zapier:read` scope.
    clientAuthBasic:
      type: http
      scheme: basic
      description: HTTP Basic client authentication (`client_id:client_secret`).
  schemas:
    TokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
          description: Bearer access token.
        token_type:
          type: string
          enum: [Bearer]
          description: Token type; always "Bearer".
        expires_in:
          type: integer
          format: int32
          description: Number of seconds until the access token expires.
        refresh_token:
          type: string
          nullable: true
          description: Refresh token, if issued and still valid.
        scope:
          type: string
          nullable: true
          description: Space-delimited scopes granted.
    OAuthError:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: OAuth 2.0 error code.
          examples:
            - insufficient_scope
            - invalid_client
            - invalid_grant
            - invalid_request
            - invalid_token
            - server_error
            - unauthorized_client
            - unsupported_grant_type
        error_description:
          type: string
          nullable: true
          description: Optional human-readable error description.
    SubscribeRequest:
      type: object
      required: [hookUrl]
      properties:
        hookUrl:
          type: string
          format: uri
          description: Zapier's webhook target URL for this user+Zap.
    SubscribeResponse:
      type: object
      required: [id]
      properties:
        id:
          type: string
          description: Subscription identifier to use when unsubscribing.
    UnsubscribeRequest:
      type: object
      required: [id]
      properties:
        id:
          type: string
          description: The subscription id returned by the subscribe call.
    ZapierMeResponse:
      type: object
      required: [userId]
      properties:
        email:
          type: string
          format: email
          nullable: true
          description: The user's email if available.
        userId:
          type: string
          description: The Reachr user identifier associated with the token.
