Skip to content

Types

Stable types import path.

This module re-exports ID types, enums, and constants from affinity.models.types.

CompanyId

Bases: IntId

Called Organization in V1.

Source code in affinity/models/types.py
44
45
class CompanyId(IntId):
    """Called Organization in V1."""

DropdownOptionColor

Bases: IntEnum

Colors for dropdown options.

Affinity uses integer color codes for dropdown field options.

Source code in affinity/models/types.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
class DropdownOptionColor(IntEnum):
    """
    Colors for dropdown options.

    Affinity uses integer color codes for dropdown field options.
    """

    DEFAULT = 0
    BLUE = 1
    GREEN = 2
    YELLOW = 3
    ORANGE = 4
    RED = 5
    PURPLE = 6
    GRAY = 7

EnrichedFieldId

Bases: StrId

Enriched field IDs are strings in V2 (e.g., 'affinity-data-description').

Source code in affinity/models/types.py
190
191
class EnrichedFieldId(StrId):
    """Enriched field IDs are strings in V2 (e.g., 'affinity-data-description')."""

EntityType

Bases: OpenIntEnum

Entity types in Affinity.

Source code in affinity/models/types.py
279
280
281
282
283
284
class EntityType(OpenIntEnum):
    """Entity types in Affinity."""

    PERSON = 0
    ORGANIZATION = 1
    OPPORTUNITY = 8

FieldId

Bases: StrId

V2-style field id (e.g. 'field-123').

FieldId provides normalized comparison semantics: - FieldId(123) == FieldId("123")True - FieldId("field-123") == "field-123"True - FieldId("field-123") == 123True

This normalization is specific to FieldId because field IDs uniquely come from mixed sources (some APIs return integers, some return strings like "field-123"). Other TypedId subclasses (PersonId, CompanyId, etc.) don't have this problem - they consistently use integers.

Source code in affinity/models/types.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
class FieldId(StrId):
    """
    V2-style field id (e.g. 'field-123').

    FieldId provides normalized comparison semantics:
    - ``FieldId(123) == FieldId("123")`` → ``True``
    - ``FieldId("field-123") == "field-123"`` → ``True``
    - ``FieldId("field-123") == 123`` → ``True``

    This normalization is specific to FieldId because field IDs uniquely come
    from mixed sources (some APIs return integers, some return strings like
    "field-123"). Other TypedId subclasses (PersonId, CompanyId, etc.) don't
    have this problem - they consistently use integers.
    """

    def __new__(cls, value: Any) -> FieldId:
        """Normalize value to 'field-xxx' format at construction time."""
        if isinstance(value, cls):
            return value
        if isinstance(value, int):
            return str.__new__(cls, f"field-{value}")
        if isinstance(value, str):
            candidate = value.strip()
            if candidate.isdigit():
                return str.__new__(cls, f"field-{candidate}")
            if _FIELD_ID_RE.match(candidate):
                return str.__new__(cls, candidate)
        raise ValueError("FieldId must be an int, digits, or 'field-<digits>'")

    @classmethod
    def __get_pydantic_core_schema__(
        cls, source_type: Any, handler: GetCoreSchemaHandler
    ) -> CoreSchema:
        _ = source_type
        _ = handler

        def validate(value: Any) -> FieldId:
            # Use __new__ which handles all normalization
            return cls(value)

        return core_schema.no_info_plain_validator_function(validate)

    def __eq__(self, other: object) -> bool:
        """
        Normalize comparison for FieldId.

        Supports comparison with:
        - Other FieldId instances
        - Strings (e.g., "field-123" or "123")
        - Integers (e.g., 123)
        """
        if isinstance(other, FieldId):
            # Both are FieldId - compare string representations
            return str.__eq__(self, other)
        if isinstance(other, str):
            # Compare with string - could be "field-123" or "123"
            try:
                other_normalized = FieldId(other)
                return str.__eq__(self, other_normalized)
            except ValueError:
                return False
        if isinstance(other, int):
            # Compare with integer
            return str.__eq__(self, f"field-{other}")
        return NotImplemented

    def __hash__(self) -> int:
        """Hash the string representation for dict/set usage."""
        return str.__hash__(self)

    def __repr__(self) -> str:
        """Return a representation useful for debugging."""
        return f"FieldId({str.__repr__(self)})"

    def __str__(self) -> str:
        """Return the canonical string value (e.g., 'field-123')."""
        return str.__str__(self)

__eq__(other: object) -> bool

Normalize comparison for FieldId.

Supports comparison with: - Other FieldId instances - Strings (e.g., "field-123" or "123") - Integers (e.g., 123)

Source code in affinity/models/types.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def __eq__(self, other: object) -> bool:
    """
    Normalize comparison for FieldId.

    Supports comparison with:
    - Other FieldId instances
    - Strings (e.g., "field-123" or "123")
    - Integers (e.g., 123)
    """
    if isinstance(other, FieldId):
        # Both are FieldId - compare string representations
        return str.__eq__(self, other)
    if isinstance(other, str):
        # Compare with string - could be "field-123" or "123"
        try:
            other_normalized = FieldId(other)
            return str.__eq__(self, other_normalized)
        except ValueError:
            return False
    if isinstance(other, int):
        # Compare with integer
        return str.__eq__(self, f"field-{other}")
    return NotImplemented

__hash__() -> int

Hash the string representation for dict/set usage.

Source code in affinity/models/types.py
129
130
131
def __hash__(self) -> int:
    """Hash the string representation for dict/set usage."""
    return str.__hash__(self)

__new__(value: Any) -> FieldId

Normalize value to 'field-xxx' format at construction time.

Source code in affinity/models/types.py
78
79
80
81
82
83
84
85
86
87
88
89
90
def __new__(cls, value: Any) -> FieldId:
    """Normalize value to 'field-xxx' format at construction time."""
    if isinstance(value, cls):
        return value
    if isinstance(value, int):
        return str.__new__(cls, f"field-{value}")
    if isinstance(value, str):
        candidate = value.strip()
        if candidate.isdigit():
            return str.__new__(cls, f"field-{candidate}")
        if _FIELD_ID_RE.match(candidate):
            return str.__new__(cls, candidate)
    raise ValueError("FieldId must be an int, digits, or 'field-<digits>'")

__repr__() -> str

Return a representation useful for debugging.

Source code in affinity/models/types.py
133
134
135
def __repr__(self) -> str:
    """Return a representation useful for debugging."""
    return f"FieldId({str.__repr__(self)})"

__str__() -> str

Return the canonical string value (e.g., 'field-123').

Source code in affinity/models/types.py
137
138
139
def __str__(self) -> str:
    """Return the canonical string value (e.g., 'field-123')."""
    return str.__str__(self)

FieldType

Bases: OpenStrEnum

Field types based on their source/scope. V2 API uses these string identifiers.

Source code in affinity/models/types.py
415
416
417
418
419
420
421
422
423
424
425
class FieldType(OpenStrEnum):
    """
    Field types based on their source/scope.
    V2 API uses these string identifiers.
    """

    ENRICHED = "enriched"
    LIST = "list"
    LIST_SPECIFIC = "list-specific"  # Alias used in some API responses
    GLOBAL = "global"
    RELATIONSHIP_INTELLIGENCE = "relationship-intelligence"

FieldValueChangeAction

Bases: OpenIntEnum

Types of changes that can occur to field values.

Source code in affinity/models/types.py
491
492
493
494
495
496
class FieldValueChangeAction(OpenIntEnum):
    """Types of changes that can occur to field values."""

    CREATE = 0
    DELETE = 1
    UPDATE = 2

FieldValueType

Bases: OpenStrEnum

Field value types (V2-first).

V2 represents valueType as strings (e.g. "dropdown-multi", "ranked-dropdown", "interaction"). V1 represents field value types as numeric codes; numeric inputs are normalized into the closest V2 string type where possible.

Source code in affinity/models/types.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
class FieldValueType(OpenStrEnum):
    """
    Field value types (V2-first).

    V2 represents `valueType` as strings (e.g. "dropdown-multi", "ranked-dropdown", "interaction").
    V1 represents field value types as numeric codes; numeric inputs are normalized into the closest
    V2 string type where possible.
    """

    TEXT = "text"

    NUMBER = "number"
    NUMBER_MULTI = "number-multi"

    DATETIME = "datetime"  # V2 canonical (V1 docs call this "Date")

    LOCATION = "location"
    LOCATION_MULTI = "location-multi"

    DROPDOWN = "dropdown"
    DROPDOWN_MULTI = "dropdown-multi"
    RANKED_DROPDOWN = "ranked-dropdown"

    PERSON = "person"
    PERSON_MULTI = "person-multi"

    COMPANY = "company"  # V1 calls this "organization"
    COMPANY_MULTI = "company-multi"

    FILTERABLE_TEXT = "filterable-text"
    FILTERABLE_TEXT_MULTI = "filterable-text-multi"

    INTERACTION = "interaction"  # V2-only (relationship-intelligence)

    @classmethod
    def _missing_(cls, value: object) -> OpenStrEnum:
        # Normalize known V1 numeric codes to canonical V2 strings.
        if isinstance(value, int):
            mapping: dict[int, FieldValueType] = {
                0: cls.PERSON,
                1: cls.COMPANY,
                2: cls.TEXT,
                3: cls.NUMBER,
                4: cls.DATETIME,
                5: cls.LOCATION,
                7: cls.DROPDOWN,
                10: cls.FILTERABLE_TEXT,
            }
            if value in mapping:
                return mapping[value]

            # Keep "unknown numeric" inputs stable by caching under the int key as well.
            text = str(value)
            existing = cls._value2member_map_.get(text)
            if existing is not None:
                existing_enum = cast(OpenStrEnum, existing)
                cls._value2member_map_[value] = existing_enum
                return existing_enum
            created = super()._missing_(text)
            cls._value2member_map_[value] = created
            return created

        if isinstance(value, str):
            text = value.strip()
            lowered = text.lower()
            if lowered == "date":
                return cls.DATETIME
            if lowered in ("organization", "organisation"):
                return cls.COMPANY
            if lowered in ("organization-multi", "organisation-multi"):
                return cls.COMPANY_MULTI
            if lowered == "filterable_text":
                return cls.FILTERABLE_TEXT

        return super()._missing_(value)

InteractionType

Bases: OpenIntEnum

Types of interactions.

Source code in affinity/models/types.py
428
429
430
431
432
433
434
class InteractionType(OpenIntEnum):
    """Types of interactions."""

    MEETING = 0  # Also called Event
    CALL = 1
    CHAT_MESSAGE = 2
    EMAIL = 3

ListType

Bases: OpenIntEnum

Type of entities a list can contain.

Source code in affinity/models/types.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
class ListType(OpenIntEnum):
    """Type of entities a list can contain."""

    PERSON = 0
    ORGANIZATION = 1  # Company in V2 terminology
    OPPORTUNITY = 8

    @classmethod
    def _missing_(cls, value: object) -> OpenIntEnum:
        # V2 list endpoints commonly return string types (e.g. "company").
        if isinstance(value, str):
            text = value.strip().lower()
            if text in ("person", "people"):
                return cls.PERSON
            if text in ("company", "organization", "organisation"):
                return cls.ORGANIZATION
            if text in ("opportunity", "opportunities"):
                return cls.OPPORTUNITY
        return super()._missing_(value)

NoteType

Bases: OpenIntEnum

Types of notes.

Source code in affinity/models/types.py
474
475
476
477
478
479
480
class NoteType(OpenIntEnum):
    """Types of notes."""

    PLAIN_TEXT = 0
    EMAIL_DERIVED = 1  # Deprecated creation method
    HTML = 2
    AI_NOTETAKER = 3

PersonType

Bases: OpenStrEnum

Types of persons in Affinity.

Source code in affinity/models/types.py
287
288
289
290
291
292
class PersonType(OpenStrEnum):
    """Types of persons in Affinity."""

    INTERNAL = "internal"
    EXTERNAL = "external"
    COLLABORATOR = "collaborator"

ReminderResetType

Bases: OpenIntEnum

How recurring reminders get reset.

Source code in affinity/models/types.py
458
459
460
461
462
463
class ReminderResetType(OpenIntEnum):
    """How recurring reminders get reset."""

    INTERACTION = 0  # Email or meeting
    EMAIL = 1
    MEETING = 2

ReminderStatus

Bases: OpenIntEnum

Current status of a reminder.

Source code in affinity/models/types.py
466
467
468
469
470
471
class ReminderStatus(OpenIntEnum):
    """Current status of a reminder."""

    COMPLETED = 0
    ACTIVE = 1
    OVERDUE = 2

ReminderType

Bases: OpenIntEnum

Types of reminders.

Source code in affinity/models/types.py
451
452
453
454
455
class ReminderType(OpenIntEnum):
    """Types of reminders."""

    ONE_TIME = 0
    RECURRING = 1

WebhookEvent

Bases: OpenStrEnum

Supported webhook events (27 total).

Events cover CRUD operations on Affinity entities:

  • Lists: created, updated, deleted
  • List entries: created, deleted
  • Notes: created, updated, deleted
  • Fields: created, updated, deleted
  • Field values: created, updated, deleted
  • Persons: created, updated, deleted
  • Organizations (companies): created, updated, deleted, merged
  • Opportunities: created, updated, deleted
  • Files: created, deleted
  • Reminders: created, updated, deleted

This enum extends OpenStrEnum for forward compatibility - any unknown events from Affinity are preserved as strings rather than raising errors.

See the webhooks guide for complete documentation and usage examples.

Source code in affinity/models/types.py
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
class WebhookEvent(OpenStrEnum):
    """
    Supported webhook events (27 total).

    Events cover CRUD operations on Affinity entities:

    - **Lists**: created, updated, deleted
    - **List entries**: created, deleted
    - **Notes**: created, updated, deleted
    - **Fields**: created, updated, deleted
    - **Field values**: created, updated, deleted
    - **Persons**: created, updated, deleted
    - **Organizations (companies)**: created, updated, deleted, merged
    - **Opportunities**: created, updated, deleted
    - **Files**: created, deleted
    - **Reminders**: created, updated, deleted

    This enum extends ``OpenStrEnum`` for forward compatibility - any unknown
    events from Affinity are preserved as strings rather than raising errors.

    See the webhooks guide for complete documentation and usage examples.
    """

    LIST_CREATED = "list.created"
    LIST_UPDATED = "list.updated"
    LIST_DELETED = "list.deleted"
    LIST_ENTRY_CREATED = "list_entry.created"
    LIST_ENTRY_DELETED = "list_entry.deleted"
    NOTE_CREATED = "note.created"
    NOTE_UPDATED = "note.updated"
    NOTE_DELETED = "note.deleted"
    FIELD_CREATED = "field.created"
    FIELD_UPDATED = "field.updated"
    FIELD_DELETED = "field.deleted"
    FIELD_VALUE_CREATED = "field_value.created"
    FIELD_VALUE_UPDATED = "field_value.updated"
    FIELD_VALUE_DELETED = "field_value.deleted"
    PERSON_CREATED = "person.created"
    PERSON_UPDATED = "person.updated"
    PERSON_DELETED = "person.deleted"
    ORGANIZATION_CREATED = "organization.created"
    ORGANIZATION_UPDATED = "organization.updated"
    ORGANIZATION_DELETED = "organization.deleted"
    ORGANIZATION_MERGED = "organization.merged"
    OPPORTUNITY_CREATED = "opportunity.created"
    OPPORTUNITY_UPDATED = "opportunity.updated"
    OPPORTUNITY_DELETED = "opportunity.deleted"
    FILE_CREATED = "file.created"
    FILE_DELETED = "file.deleted"
    REMINDER_CREATED = "reminder.created"
    REMINDER_UPDATED = "reminder.updated"
    REMINDER_DELETED = "reminder.deleted"

field_id_to_v1_numeric(field_id: AnyFieldId) -> int

Convert v2 FieldId into v1 numeric field_id.

Accepts: - FieldId('field-123') -> 123 Rejects: - EnrichedFieldId(...) (cannot be represented as v1 numeric id)

Source code in affinity/models/types.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def field_id_to_v1_numeric(field_id: AnyFieldId) -> int:
    """
    Convert v2 FieldId into v1 numeric field_id.

    Accepts:
    - FieldId('field-123') -> 123
    Rejects:
    - EnrichedFieldId(...) (cannot be represented as v1 numeric id)
    """
    if isinstance(field_id, EnrichedFieldId):
        raise ValueError(
            "Field IDs must be 'field-<digits>' for v1 conversion; "
            "enriched/relationship-intelligence IDs (e.g., 'affinity-data-*', "
            "'source-of-introduction') are not supported."
        )

    match = _FIELD_ID_RE.match(str(field_id))
    if match is None:
        raise ValueError(
            "Field IDs must be 'field-<digits>' for v1 conversion; "
            "enriched/relationship-intelligence IDs (e.g., 'affinity-data-*', "
            "'source-of-introduction') are not supported."
        )
    return int(match.group(1))