openapi: 3.1.0
info:
  title: Relm API
  description: |
    Relm is an API-first CRM built for LLM agents and developers.
    All endpoints require a Bearer API key.
  version: "2026-07-02"
  contact:
    name: Relm
    url: https://relmcrm.com
servers:
  - url: https://relmcrm.com
    description: Production
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_... or sk_test_...
  parameters:
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 50, maximum: 200 }
    Cursor:
      name: cursor
      in: query
      description: Opaque cursor from a previous `next_cursor`.
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Any string ≤255 chars. Repeats within the workspace return the cached response.
      schema: { type: string }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
    List:
      type: object
      properties:
        object: { type: string, enum: [list] }
        data: { type: array, items: { type: object } }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }
    Contact:
      type: object
      properties:
        id: { type: string, example: con_abc123 }
        email: { type: [string, "null"], format: email }
        first_name: { type: [string, "null"] }
        last_name: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        company_id: { type: [string, "null"] }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Company:
      type: object
      properties:
        id: { type: string, example: cmp_abc123 }
        name: { type: string }
        domain: { type: [string, "null"] }
        metadata: { type: object, additionalProperties: true }
    Deal:
      type: object
      properties:
        id: { type: string, example: deal_abc123 }
        title: { type: string }
        value_cents: { type: integer }
        currency: { type: string, minLength: 3, maxLength: 3 }
        stage: { type: string, enum: [lead, qualified, proposal, won, lost] }
        close_date: { type: [string, "null"], format: date }
        company_id: { type: [string, "null"] }
        primary_contact_id: { type: [string, "null"] }
        metadata: { type: object, additionalProperties: true }
    Activity:
      type: object
      properties:
        id: { type: string, example: act_abc123 }
        type: { type: string, enum: [note, email, call, meeting, task] }
        subject: { type: [string, "null"] }
        body: { type: [string, "null"] }
        contact_id: { type: [string, "null"] }
        deal_id: { type: [string, "null"] }
        company_id: { type: [string, "null"] }
        occurred_at: { type: string, format: date-time }
paths:
  /v1/contacts:
    get:
      summary: List contacts
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: filter[email]
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content: { application/json: { schema: { $ref: "#/components/schemas/List" } } }
    post:
      summary: Create a contact
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Contact" }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Contact" } } } }
  /v1/contacts/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get:
      summary: Retrieve a contact
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Contact" } } } }
    patch:
      summary: Update a contact
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200": { description: OK }
    delete:
      summary: Delete a contact
      responses:
        "200": { description: Deleted }
  /v1/contacts/batch:
    post:
      summary: Bulk create/upsert contacts
      description: |
        Insert up to 500 contacts in one call. Pass `dedup_on: ["email"]` to upsert
        on `(workspace_id, email)` instead of failing on duplicates.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                contacts:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { $ref: "#/components/schemas/Contact" }
                dedup_on:
                  type: array
                  items: { type: string, enum: [email] }
      responses:
        "201": { description: Created }
  /v1/companies:
    get: { summary: List companies, responses: { "200": { description: OK } } }
    post: { summary: Create a company, responses: { "201": { description: Created } } }
  /v1/companies/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get: { summary: Retrieve a company, responses: { "200": { description: OK } } }
    patch: { summary: Update a company, responses: { "200": { description: OK } } }
    delete: { summary: Delete a company, responses: { "200": { description: Deleted } } }
  /v1/deals:
    get: { summary: List deals, responses: { "200": { description: OK } } }
    post: { summary: Create a deal, responses: { "201": { description: Created } } }
  /v1/deals/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get: { summary: Retrieve a deal, responses: { "200": { description: OK } } }
    patch: { summary: Update a deal, responses: { "200": { description: OK } } }
    delete: { summary: Delete a deal, responses: { "200": { description: Deleted } } }
  /v1/activities:
    get: { summary: List activities, responses: { "200": { description: OK } } }
    post: { summary: Create an activity, responses: { "201": { description: Created } } }
  /v1/activities/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get: { summary: Retrieve an activity, responses: { "200": { description: OK } } }
    patch: { summary: Update an activity, responses: { "200": { description: OK } } }
    delete: { summary: Delete an activity, responses: { "200": { description: Deleted } } }
  /v1/search:
    get:
      summary: Search across entities
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
      responses:
        "200": { description: OK }
  /v1/schema/fields:
    get:
      summary: List custom fields
      parameters:
        - name: object_type
          in: query
          schema: { type: string, enum: [contact, company, deal, activity] }
      responses:
        "200": { description: OK }
    post:
      summary: Define a custom field
      responses:
        "201": { description: Created }
  /v1/schema/fields/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get: { summary: Retrieve a custom field, responses: { "200": { description: OK } } }
    delete: { summary: Delete a custom field, responses: { "200": { description: Deleted } } }
  /v1/associations:
    get:
      summary: List associations
      parameters:
        - { name: from_type, in: query, schema: { type: string } }
        - { name: from_id,   in: query, schema: { type: string } }
        - { name: to_type,   in: query, schema: { type: string } }
        - { name: to_id,     in: query, schema: { type: string } }
      responses:
        "200": { description: OK }
    post:
      summary: Create an association
      responses:
        "201": { description: Created }
  /v1/associations/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    delete: { summary: Delete an association, responses: { "200": { description: Deleted } } }
  /v1/webhooks:
    get:
      summary: List webhook endpoints
      responses: { "200": { description: OK } }
    post:
      summary: Create a webhook endpoint (returns signing secret)
      responses: { "201": { description: Created } }
  /v1/webhooks/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
    get:    { summary: Retrieve an endpoint, responses: { "200": { description: OK } } }
    patch:  { summary: Update an endpoint,   responses: { "200": { description: OK } } }
    delete: { summary: Delete an endpoint,   responses: { "200": { description: Deleted } } }
  /v1/webhooks/{id}/replay/{deliveryId}:
    parameters:
      - { name: id,         in: path, required: true, schema: { type: string } }
      - { name: deliveryId, in: path, required: true, schema: { type: string } }
    post:
      summary: Replay a delivery
      responses: { "200": { description: OK } }
