openapi: 3.1.0
info:
  title: USR Apps API
  version: "1.0"
  summary: One API for leads, contacts, tickets, people and events across USR Apps.
  description: |
    REST API for partners and customers building on USR Apps.
    All requests use HTTPS and JSON. Authenticate with OAuth 2.0 (USR SSO).
    Sandbox: https://sandbox.api.usrinfo.tech/v1 (sample data, free, no approval).
  contact:
    name: USR Developer Support
    email: support@usrinfo.tech
    url: https://usrinfo.tech/developers.html
  termsOfService: https://usrinfo.tech/terms.html
servers:
  - url: https://api.usrinfo.tech/v1
    description: Production (India region)
  - url: https://sandbox.api.usrinfo.tech/v1
    description: Sandbox with sample data
security:
  - oauth2: []
tags:
  - name: Apps
  - name: Leads
  - name: Contacts
  - name: Tickets
  - name: People
  - name: Events
  - name: Webhooks
paths:
  /me:
    get:
      tags: [Apps]
      summary: Who this token belongs to
      operationId: getMe
      responses:
        "200":
          description: Workspace and client details
          content:
            application/json:
              schema:
                type: object
                properties:
                  workspace_id: { type: string, example: ws_8f2k1 }
                  workspace_name: { type: string, example: Acme Ltd }
                  client_id: { type: string, example: partner-acme-sync }
                  scopes: { type: array, items: { type: string }, example: [leads:read, leads:write] }
                  region: { type: string, example: in }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /apps:
    get:
      tags: [Apps]
      summary: List USR apps (public catalogue)
      operationId: listApps
      security: []
      responses:
        "200":
          description: Catalogue
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/App" } }
  /leads:
    get:
      tags: [Leads]
      summary: List leads
      operationId: listLeads
      security: [{ oauth2: [leads:read] }]
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          schema: { type: string, enum: [new, contacted, qualified, converted, unqualified] }
      responses:
        "200":
          description: A page of leads
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Lead" } }
                  next_cursor: { type: [string, "null"] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Leads]
      summary: Create a lead
      description: Creates a lead in USR CRM Lite. Send an Idempotency-Key so retries never create duplicates. Leads with the same email in the last 24 hours are merged.
      operationId: createLead
      security: [{ oauth2: [leads:write] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lead" }
        "200":
          description: Merged into an existing lead
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lead" }
        "400": { $ref: "#/components/responses/Invalid" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /leads/{id}:
    get:
      tags: [Leads]
      summary: Get a lead
      operationId: getLead
      security: [{ oauth2: [leads:read] }]
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: The lead
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lead" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Leads]
      summary: Update a lead
      operationId: updateLead
      security: [{ oauth2: [leads:write] }]
      parameters: [{ $ref: "#/components/parameters/Id" }]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lead" }
        "404": { $ref: "#/components/responses/NotFound" }
  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      operationId: listContacts
      security: [{ oauth2: [contacts:read] }]
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: email
          in: query
          schema: { type: string, format: email }
      responses:
        "200":
          description: A page of contacts
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Contact" } }
                  next_cursor: { type: [string, "null"] }
    post:
      tags: [Contacts]
      summary: Create or update a contact (matched by email)
      operationId: upsertContact
      security: [{ oauth2: [contacts:write] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Contact" }
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
  /tickets:
    post:
      tags: [Tickets]
      summary: Raise a support ticket in USR Desk
      operationId: createTicket
      security: [{ oauth2: [tickets:write] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TicketInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ticket" }
    get:
      tags: [Tickets]
      summary: List tickets
      operationId: listTickets
      security: [{ oauth2: [tickets:read] }]
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: A page of tickets
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Ticket" } }
                  next_cursor: { type: [string, "null"] }
  /tickets/{id}:
    get:
      tags: [Tickets]
      summary: Get a ticket
      operationId: getTicket
      security: [{ oauth2: [tickets:read] }]
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: The ticket
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ticket" }
  /people:
    get:
      tags: [People]
      summary: List employees (USR HRIS directory)
      description: Read-only directory. Personal data beyond work details needs the people:read.sensitive scope and the customer's admin approval.
      operationId: listPeople
      security: [{ oauth2: [people:read] }]
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: A page of people
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Person" } }
                  next_cursor: { type: [string, "null"] }
  /events:
    post:
      tags: [Events]
      summary: Send website or product events
      description: Same batch format that usr-track.js sends. Events show on the lead timeline in USR CRM Lite and Sales Assistant.
      operationId: sendEvents
      security: [{ oauth2: [events:write] }, { siteKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventBatch" }
      responses:
        "202": { description: Accepted }
  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook subscriptions
      operationId: listWebhooks
      security: [{ oauth2: [webhooks:manage] }]
      responses:
        "200":
          description: Subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
    post:
      tags: [Webhooks]
      summary: Subscribe to events
      operationId: createWebhook
      security: [{ oauth2: [webhooks:manage] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, example: "https://partner.example.com/usr/webhooks" }
                events: { type: array, items: { $ref: "#/components/schemas/EventType" } }
                description: { type: string }
      responses:
        "201":
          description: Created. The signing secret is shown once.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Webhook"
                  - type: object
                    properties:
                      secret: { type: string, example: whsec_3fA9... }
  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Delete a subscription
      operationId: deleteWebhook
      security: [{ oauth2: [webhooks:manage] }]
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "204": { description: Deleted }
  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a test event to your endpoint
      operationId: testWebhook
      security: [{ oauth2: [webhooks:manage] }]
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "202": { description: Test event queued }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: USR SSO (OpenID Connect). Use client credentials for server-to-server, authorization code with PKCE when acting for a signed-in user.
      flows:
        clientCredentials:
          tokenUrl: https://auth.usrinfo.tech/realms/usr/protocol/openid-connect/token
          scopes: &scopes
            leads:read: Read leads
            leads:write: Create and update leads
            contacts:read: Read contacts
            contacts:write: Create and update contacts
            tickets:read: Read tickets
            tickets:write: Create tickets
            people:read: Read the employee directory
            events:write: Send events
            webhooks:manage: Manage webhook subscriptions
        authorizationCode:
          authorizationUrl: https://auth.usrinfo.tech/realms/usr/protocol/openid-connect/auth
          tokenUrl: https://auth.usrinfo.tech/realms/usr/protocol/openid-connect/token
          scopes: *scopes
    siteKey:
      type: apiKey
      in: header
      name: X-USR-Site-Key
      description: Publishable key for browsers. Can only create leads and send events, and only from the website origins you allow.
  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    UpdatedSince:
      name: updated_since
      in: query
      description: ISO 8601 timestamp (UTC). Use it for incremental sync.
      schema: { type: string, format: date-time }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Any unique string up to 64 characters. Retries with the same key return the first result for 24 hours.
      schema: { type: string, maxLength: 64 }
  responses:
    Invalid:
      description: The request failed validation
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Unauthorized:
      description: Missing or expired token, or a scope is missing
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    NotFound:
      description: Not found
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Conflict:
      description: The Idempotency-Key was reused with a different body
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    RateLimited:
      description: Too many requests. Wait for the seconds in Retry-After.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
  schemas:
    Problem:
      type: object
      description: RFC 9457 problem details
      properties:
        type: { type: string, example: "https://usrinfo.tech/developers.html#errors" }
        title: { type: string, example: Validation failed }
        status: { type: integer, example: 400 }
        detail: { type: string, example: "contact.email is not a valid email address" }
        request_id: { type: string, example: req_7Hk2p }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              message: { type: string }
    App:
      type: object
      properties:
        id: { type: string, example: crm }
        name: { type: string, example: USR CRM Lite }
        category: { type: string, example: sales }
        summary: { type: string }
        url: { type: string, format: uri }
    LeadInput:
      type: object
      required: [contact]
      properties:
        external_id: { type: string, maxLength: 64, description: Your own id for this lead }
        contact:
          type: object
          properties:
            name: { type: string, maxLength: 120 }
            email: { type: string, format: email }
            phone: { type: string, description: E.164, example: "+919876543210" }
        company:
          type: object
          properties:
            name: { type: string }
            industry: { type: string }
            size_band: { type: string, example: "101-500" }
            country: { type: string, example: IN }
        interest:
          type: object
          properties:
            type: { type: string, enum: [start, demo, quote, requirement, partner], default: start }
            apps: { type: array, items: { type: string }, example: [crm, desk] }
            request: { type: string, maxLength: 200 }
            preferred_slot: { type: string, format: date-time }
        source:
          type: object
          properties:
            channel: { type: string, example: partner_site }
            utm_source: { type: string }
            utm_medium: { type: string }
            utm_campaign: { type: string }
            landing_page: { type: string }
            referrer: { type: string }
        consent:
          type: object
          required: [contact, captured_at]
          description: Required by DPDP and GDPR. Leads without consent are rejected.
          properties:
            contact: { type: boolean, const: true }
            captured_at: { type: string, format: date-time }
            text: { type: string, description: The exact consent text the person saw }
        visitor_id: { type: string, description: "From USRTrack.id(). Links website visits to the lead." }
    Lead:
      allOf:
        - $ref: "#/components/schemas/LeadInput"
        - type: object
          properties:
            id: { type: string, example: ld_01J9ZK3 }
            status: { type: string, enum: [new, contacted, qualified, converted, unqualified] }
            owner: { type: [string, "null"] }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    Contact:
      type: object
      required: [email]
      properties:
        id: { type: string, readOnly: true }
        name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        company: { type: string }
        tags: { type: array, items: { type: string } }
        updated_at: { type: string, format: date-time, readOnly: true }
    TicketInput:
      type: object
      required: [subject, requester]
      properties:
        subject: { type: string, maxLength: 200 }
        description: { type: string, maxLength: 5000 }
        priority: { type: string, enum: [low, medium, high, urgent], default: medium }
        app: { type: string, example: pms }
        requester:
          type: object
          required: [email]
          properties:
            name: { type: string }
            email: { type: string, format: email }
    Ticket:
      allOf:
        - $ref: "#/components/schemas/TicketInput"
        - type: object
          properties:
            id: { type: string, example: TCK-48213 }
            status: { type: string, enum: [open, pending, resolved, closed] }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    Person:
      type: object
      properties:
        id: { type: string }
        employee_code: { type: string }
        name: { type: string }
        work_email: { type: string, format: email }
        department: { type: string }
        designation: { type: string }
        manager_id: { type: [string, "null"] }
        status: { type: string, enum: [active, notice, exited] }
        updated_at: { type: string, format: date-time }
    EventBatch:
      type: object
      required: [visitor_id, events]
      properties:
        site: { type: string }
        visitor_id: { type: string }
        session_id: { type: string }
        events:
          type: array
          maxItems: 50
          items:
            type: object
            required: [n, t]
            properties:
              n: { type: string, description: Event name, example: page_view }
              t: { type: string, format: date-time }
              p: { type: string, description: Page path }
              pr: { type: object, additionalProperties: { type: string } }
    EventType:
      type: string
      enum: [lead.created, lead.updated, lead.converted, deal.won, contact.updated, ticket.created, ticket.resolved, invoice.paid, employee.joined, employee.exited, review.completed]
    Webhook:
      type: object
      properties:
        id: { type: string, example: wh_2Lq8 }
        url: { type: string, format: uri }
        events: { type: array, items: { $ref: "#/components/schemas/EventType" } }
        status: { type: string, enum: [active, paused, failing] }
        created_at: { type: string, format: date-time }
