pipedrive

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package pipedrive contains the typed HTTP client for the Pipedrive REST API. The client has no MCP imports; it is reusable independently of the transport.

Index

Constants

View Source
const DefaultActivityType = "task"

DefaultActivityType is what Pipedrive's POST /activities falls back to when the body omits `type`. Tracked as a constant so the GoDoc and the LLM-facing tool description don't drift if upstream ever changes the default.

Variables

View Source
var (
	ErrUnauthorized          = errors.New("pipedrive: unauthorized")
	ErrForbiddenPermission   = errors.New("pipedrive: forbidden (permission)")
	ErrForbiddenBusinessRule = errors.New("pipedrive: forbidden (business rule)")
	ErrNotFound              = errors.New("pipedrive: not found")
	ErrRateLimited           = errors.New("pipedrive: rate limited")
	ErrServerError           = errors.New("pipedrive: server error")
	ErrValidation            = errors.New("pipedrive: validation")
)

Sentinel error classes. Tools branch on these via errors.Is.

Functions

func BaseURL

func BaseURL(domain string) string

BaseURL returns the v2 base URL for a Pipedrive workspace subdomain. The path suffix is stripped by hostOf in New, so the per-call API version (v2 by default; v1 for the notes carve-out) is composed in exec — see the apiVersion type below.

func ItemInt64

func ItemInt64(item map[string]any, key string) int64

ItemInt64 reads a numeric field from a SearchHit's Item map, tolerating both the float64 form Go's stdlib JSON decoder produces for arbitrary numbers and any direct int64 / int forms a future caller might pass through. Returns 0 for missing or non-numeric values; callers that need to distinguish "missing" from "zero" should check key existence themselves.

func ItemString

func ItemString(item map[string]any, key string) string

ItemString reads a string field from a SearchHit's Item map. Returns "" for missing or non-string values.

func WebURL

func WebURL(domain string, kind WebURLKind, id int64) string

WebURL returns the Pipedrive web-UI URL for a resource by id. Tool outputs include this so a human reading the LLM's transcript can jump to the entity in their browser.

Types

type APIError

type APIError struct {
	Class    error  // one of the sentinels
	Status   int    // HTTP status from Pipedrive
	Message  string // upstream `error` field, or a short summary
	Endpoint string // request path, useful for the 403 heuristic
}

APIError carries the wire-level detail. Wrap one of the sentinels above so callers can branch by class with errors.Is and still inspect the detail with errors.As.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

type Activity

type Activity struct {
	ID                      int64                 `json:"id"`
	Subject                 string                `json:"subject"`
	Type                    string                `json:"type"` // activity-type key (e.g. "call", "email", "meeting", "task"); free-form per workspace
	OwnerID                 int64                 `json:"owner_id"`
	DealID                  int64                 `json:"deal_id,omitempty"`
	PersonID                int64                 `json:"person_id,omitempty"`
	OrgID                   int64                 `json:"org_id,omitempty"`
	LeadID                  string                `json:"lead_id,omitempty"`
	ProjectID               int64                 `json:"project_id,omitempty"`
	DueDate                 string                `json:"due_date,omitempty"`
	DueTime                 string                `json:"due_time,omitempty"`
	Duration                string                `json:"duration,omitempty"`
	Busy                    bool                  `json:"busy"`
	Done                    bool                  `json:"done"`
	MarkedAsDoneTime        string                `json:"marked_as_done_time,omitempty"`
	Location                *ActivityLocation     `json:"location,omitempty"`
	Participants            []ActivityParticipant `json:"participants,omitempty"`
	Attendees               []ActivityAttendee    `json:"attendees,omitempty"`
	ConferenceMeetingClient string                `json:"conference_meeting_client,omitempty"`
	ConferenceMeetingURL    string                `json:"conference_meeting_url,omitempty"`
	ConferenceMeetingID     string                `json:"conference_meeting_id,omitempty"`
	PublicDescription       string                `json:"public_description,omitempty"`
	Note                    string                `json:"note,omitempty"`
	AddTime                 string                `json:"add_time"`
	UpdateTime              string                `json:"update_time"`
}

Activity is a Pipedrive activity record (subset). Pipedrive v2 dropped the v1 `_flag` suffixes — `busy_flag` → `busy`, `done_flag` → `done`. Activities do NOT carry a `custom_fields` block on v2; the v2 schema omits it entirely. Times use Pipedrive's date/time formats (DueDate is YYYY-MM-DD, DueTime / Duration are HH:MM, AddTime / UpdateTime are the v2 RFC3339-with-space). Strings are surfaced raw to the LLM so it can pattern-match without timezone surprises.

type ActivityAttendee

type ActivityAttendee struct {
	Email       string `json:"email,omitempty"`
	Name        string `json:"name,omitempty"`
	Status      string `json:"status,omitempty"` // accepted | declined | tentative | needsAction (Google-style)
	IsOrganizer bool   `json:"is_organizer,omitempty"`
	PersonID    int64  `json:"person_id,omitempty"`
	UserID      int64  `json:"user_id,omitempty"`
}

ActivityAttendee is one row in an Activity's attendees array. Attendees are calendar-style invitees (email, name, RSVP status). PersonID is non-zero when Pipedrive matched the email to an existing person; UserID is non-zero when it matched a Pipedrive user instead. Both can be zero for an external attendee.

type ActivityLocation

type ActivityLocation struct {
	Value      string `json:"value,omitempty"`
	Country    string `json:"country,omitempty"`
	Locality   string `json:"locality,omitempty"`
	PostalCode string `json:"postal_code,omitempty"`
}

ActivityLocation is Pipedrive v2's structured location for an activity (the place a meeting is held, etc.). Street-level components from the upstream schema (route, street_number, sublocality, admin_area_level_*) are intentionally omitted — humans care about "city/country, postal" which is what the LLM-facing output surfaces.

type ActivityParticipant

type ActivityParticipant struct {
	PersonID int64 `json:"person_id"`
	Primary  bool  `json:"primary"`
}

ActivityParticipant is one row in an Activity's participants array. Pipedrive marks one participant as primary (the contact the activity is principally tied to); the rest are co-participants. Distinct from Attendees, which are calendar invitees.

type AdditionalData

type AdditionalData struct {
	NextCursor   string        `json:"next_cursor,omitempty"`
	V1Pagination *V1Pagination `json:"pagination,omitempty"`
}

AdditionalData is the paging envelope returned alongside `data` on list endpoints.

V2 uses NextCursor — opaque token; empty string means "last page". V1 (notes carve-out only) uses V1Pagination, an offset-style {start, limit, next_start} block. The tool layer encodes v1's next_start as an opaque cursor string so the LLM-facing surface stays uniform across resources.

type Address

type Address struct {
	Value      string `json:"value,omitempty"`
	Country    string `json:"country,omitempty"`
	Locality   string `json:"locality,omitempty"`
	PostalCode string `json:"postal_code,omitempty"`
}

Address is Pipedrive v2's structured address record. Returned by /organizations/{id} as the org's primary address (Pipedrive's data model puts the org-level address there; persons inherit nothing at the typed-struct level). Address-typed *custom* fields on any resource decode into raw map entries via custom_fields, not into this type. /api/v2/itemSearch returns a different, flat string-only shape — the search-side code does not decode into this type. Street-level components (route, street_number, sublocality, admin areas) are not surfaced today; add them back if a tool starts needing them.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a thin wrapper around net/http for the Pipedrive REST API. Safe for concurrent use across goroutines. There is one instance per process; the MCP server hands a pointer to every tool's Register() function.

The per-resource field caches (dealFields, …) live on the Client because their lifetime matches the process and they reuse the Client's transport. Callers go through the typed accessor methods (ResolveDealCustomFields, WarmDealFields, ReloadDealFields) rather than the unexported field.

func New

func New(opts Options) *Client

New constructs a Client with sensible defaults for retry policy.

The HTTP client is configured to refuse redirects. Pipedrive's v2 JSON API does not redirect during normal operation, and Go's default redirect-follower forwards custom request headers (including the `x-api-token` we attach below) to the redirect target verbatim. Returning ErrUseLastResponse short-circuits the follow so a stray 3xx surfaces as an "unexpected status" error rather than silently leaking the token to whatever host the Location header named.

func (*Client) CreateActivity added in v0.2.0

func (c *Client) CreateActivity(ctx context.Context, req CreateActivityRequest) (*Activity, error)

CreateActivity posts a new activity via /api/v2/activities. Returns the created activity as Pipedrive echoes it (full record with id, structured Location parsed server-side, and resolved type).

func (*Client) CreateDeal added in v0.2.0

func (c *Client) CreateDeal(ctx context.Context, req CreateDealRequest) (*Deal, error)

CreateDeal posts a new deal via /api/v2/deals. Returns the created deal as Pipedrive echoes it (full record with id, defaults resolved, and custom_fields nested as usual).

func (*Client) CreateNote

func (c *Client) CreateNote(ctx context.Context, req CreateNoteRequest) (*Note, error)

CreateNote posts a new note via /api/v1/notes. Returns the created note (Pipedrive echoes the full record on success).

func (*Client) CreateOrganization added in v0.2.0

func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (*Organization, error)

CreateOrganization posts a new organization via /api/v2/organizations. Returns the created org as Pipedrive echoes it (full record with id, structured Address parsed server-side, and custom_fields).

func (*Client) CreatePerson added in v0.2.0

func (c *Client) CreatePerson(ctx context.Context, req CreatePersonRequest) (*Person, error)

CreatePerson posts a new person via /api/v2/persons. Returns the created person as Pipedrive echoes it (full record with id and custom_fields nested as usual).

func (*Client) DeleteNote

func (c *Client) DeleteNote(ctx context.Context, id int64) error

DeleteNote removes a note via DELETE /api/v1/notes/{id}. v1 deletes are SOFT — the record persists with active_flag=false and is still readable via GetNote, but list_notes filters it out by default.

func (*Client) GetActivity

func (c *Client) GetActivity(ctx context.Context, id int64, opts GetActivityOptions) (*Activity, error)

GetActivity fetches a single activity by ID.

func (*Client) GetDeal

func (c *Client) GetDeal(ctx context.Context, id int64) (*Deal, error)

GetDeal fetches a single deal by ID. custom_fields are nested under the deal's `custom_fields` object per Pipedrive v2 — caller resolves hash keys to names via the per-Client FieldCache.

func (*Client) GetNote

func (c *Client) GetNote(ctx context.Context, id int64) (*Note, error)

GetNote fetches a single note by ID via /api/v1/notes/{id}. v2 has no equivalent endpoint; see types.go for the carve-out rationale.

func (*Client) GetOrganization

func (c *Client) GetOrganization(ctx context.Context, id int64) (*Organization, error)

GetOrganization fetches a single organization by ID. custom_fields are nested under the org's `custom_fields` object per Pipedrive v2 — caller resolves hash keys to names via the per-Client FieldCache.

func (*Client) GetPerson

func (c *Client) GetPerson(ctx context.Context, id int64) (*Person, error)

GetPerson fetches a single person by ID. custom_fields are nested under the person's `custom_fields` object per Pipedrive v2 — caller resolves hash keys to names via the per-Client FieldCache.

func (*Client) ItemSearch

func (c *Client) ItemSearch(ctx context.Context, opts SearchOptions) ([]SearchHit, string, error)

ItemSearch issues a free-text search across the requested item types. NextCursor is the authoritative "more results upstream" signal; an empty string means the page is the last. Tool callers must surface that to the LLM (see internal/tools/search.go).

func (*Client) ListActivities

func (c *Client) ListActivities(ctx context.Context, opts ListActivitiesOptions) ([]Activity, string, error)

ListActivities issues a /activities list with the given filters. Returns the page plus the next cursor (empty string = end of results). Cursor pagination is opaque; callers pass whatever NextCursor was returned on the prior page.

func (*Client) ListDealFields

func (c *Client) ListDealFields(ctx context.Context) ([]Field, error)

ListDealFields returns the field metadata for deals. Used by the auth probe (limit=1 form is in probe.go) and by the per-Client deal field cache.

func (*Client) ListDeals

func (c *Client) ListDeals(ctx context.Context, opts ListDealsOptions) ([]Deal, string, error)

ListDeals issues a /deals list with the given filters. Returns the page plus the next cursor (empty string = end of results). Cursor pagination is opaque; callers pass whatever NextCursor was returned on the prior page.

func (*Client) ListNotes

func (c *Client) ListNotes(ctx context.Context, opts ListNotesOptions) ([]Note, *V1Pagination, error)

ListNotes issues a /api/v1/notes list with the given filters. Returns the page plus the v1 pagination block (or nil if the response carries no pagination, e.g. on a 0-row response).

func (*Client) ListOrganizationFields

func (c *Client) ListOrganizationFields(ctx context.Context) ([]Field, error)

ListOrganizationFields returns the field metadata for organizations.

func (*Client) ListOrganizations

func (c *Client) ListOrganizations(ctx context.Context, opts ListOrganizationsOptions) ([]Organization, string, error)

ListOrganizations issues a /organizations list with the given filters. Returns the page plus the next cursor (empty string = end).

func (*Client) ListPersonFields

func (c *Client) ListPersonFields(ctx context.Context) ([]Field, error)

ListPersonFields returns the field metadata for persons. Used by the per-Client person field cache.

func (*Client) ListPersons

func (c *Client) ListPersons(ctx context.Context, opts ListPersonsOptions) ([]Person, string, error)

ListPersons issues a /persons list with the given filters. Returns the page plus the next cursor (empty string = end of results).

func (*Client) ListPipelines

func (c *Client) ListPipelines(ctx context.Context) ([]Pipeline, error)

ListPipelines returns every pipeline the API token's user can see. Pipedrive workspaces typically have a small handful (≤ 20), so no pagination wrapper is needed at this scale; if a workspace ever exceeds the response cap, we'll add cursor support here.

func (*Client) ListStages

func (c *Client) ListStages(ctx context.Context, pipelineID int64) ([]Stage, error)

ListStages returns the stages, optionally filtered to one pipeline. Pass pipelineID = 0 to return stages across every pipeline.

func (*Client) ProbeAuth

func (c *Client) ProbeAuth(ctx context.Context) error

ProbeAuth confirms the API token is valid for the workspace by hitting GET /api/v2/dealFields?limit=1. That endpoint is chosen because it is confirmed to exist on Pipedrive API v2 (/api/v2/users does not), every workspace has at least one deal field, and the payload is tiny.

func (*Client) ReloadDealFields

func (c *Client) ReloadDealFields(ctx context.Context) (int, error)

ReloadDealFields clears and re-fetches the deal-field cache, returning the count of fields now cached. Used by the refresh_field_cache tool to pick up custom-field renames or additions without restarting the server. Reload is followed by an eager Load so the next get_deal/list_deals call sees fresh data without paying the round-trip itself.

func (*Client) ReloadOrganizationFields

func (c *Client) ReloadOrganizationFields(ctx context.Context) (int, error)

ReloadOrganizationFields clears and re-fetches the organization-field cache, returning the count of fields now cached. See ReloadDealFields for the rationale.

func (*Client) ReloadPersonFields

func (c *Client) ReloadPersonFields(ctx context.Context) (int, error)

ReloadPersonFields clears and re-fetches the person-field cache, returning the count of fields now cached. See ReloadDealFields for the rationale.

func (*Client) ResolveDealCustomFields

func (c *Client) ResolveDealCustomFields(ctx context.Context, raw map[string]any) map[string]any

ResolveDealCustomFields returns a copy of raw with hash keys replaced by their human-readable names. Sole entry point tool packages need; the underlying cache is unexported.

func (*Client) ResolveOrganizationCustomFields

func (c *Client) ResolveOrganizationCustomFields(ctx context.Context, raw map[string]any) map[string]any

ResolveOrganizationCustomFields delegates to the per-Client org field cache.

func (*Client) ResolvePersonCustomFields

func (c *Client) ResolvePersonCustomFields(ctx context.Context, raw map[string]any) map[string]any

ResolvePersonCustomFields returns a copy of raw with hash keys replaced by their human-readable names. Sole entry point tool packages need; the underlying cache is unexported.

func (*Client) WarmDealFields

func (c *Client) WarmDealFields(ctx context.Context)

WarmDealFields eagerly triggers the deal-field cache load so the first user-facing get_deal/list_deals call doesn't pay the /dealFields round-trip on the critical path. Errors are silently swallowed: a failed warm-up just means the first real call pays the latency, exactly as the lazy path would.

func (*Client) WarmOrganizationFields

func (c *Client) WarmOrganizationFields(ctx context.Context)

WarmOrganizationFields eagerly triggers the cache load.

func (*Client) WarmPersonFields

func (c *Client) WarmPersonFields(ctx context.Context)

WarmPersonFields eagerly triggers the cache load.

type ContactPoint

type ContactPoint struct {
	Value   string `json:"value"`
	Primary bool   `json:"primary"`
	Label   string `json:"label,omitempty"`
}

ContactPoint is one row in a Person's emails / phones array. Pipedrive returns these as arrays of {value, primary, label} objects rather than flat strings so a single record can carry multiple addresses.

type CreateActivityRequest added in v0.2.0

type CreateActivityRequest struct {
	Subject           string                `json:"subject"`
	Type              string                `json:"type,omitempty"`
	DueDate           string                `json:"due_date,omitempty"` // YYYY-MM-DD
	DueTime           string                `json:"due_time,omitempty"` // HH:MM
	Duration          string                `json:"duration,omitempty"` // HH:MM
	DealID            int64                 `json:"deal_id,omitempty"`
	PersonID          int64                 `json:"person_id,omitempty"`
	OrgID             int64                 `json:"org_id,omitempty"`
	LeadID            string                `json:"lead_id,omitempty"` // UUID
	OwnerID           int64                 `json:"owner_id,omitempty"`
	Note              string                `json:"note,omitempty"`               // private; HTML allowed
	PublicDescription string                `json:"public_description,omitempty"` // shared with attendees
	Location          string                `json:"location,omitempty"`           // single-line; server-parsed
	Participants      []ActivityParticipant `json:"participants,omitempty"`
	Done              bool                  `json:"done,omitempty"`
	Busy              bool                  `json:"busy,omitempty"`
}

CreateActivityRequest is the JSON body for POST /api/v2/activities. v2 requires `subject`. Type defaults to DefaultActivityType upstream when omitted; the tool layer encourages the LLM to set it explicitly. Validating Type against the workspace's activityTypes enum is intentionally deferred: it would need a cache + probe, and Pipedrive's 400 on invalid type already surfaces cleanly as [validation] via the standard error mapping.

Location is a single-line string on input — Pipedrive parses it server-side into the structured ActivityLocation response (same pattern as POST /organizations).

Done and Busy use Go's zero-value-is-false default; sending false is equivalent to omitting (json:omitempty). For the rare "create already-marked-done" path the LLM sets Done=true.

type CreateDealRequest added in v0.2.0

type CreateDealRequest struct {
	Title             string  `json:"title"`
	Value             float64 `json:"value,omitempty"`
	Currency          string  `json:"currency,omitempty"`
	PipelineID        int64   `json:"pipeline_id,omitempty"`
	StageID           int64   `json:"stage_id,omitempty"`
	OwnerID           int64   `json:"owner_id,omitempty"`
	PersonID          int64   `json:"person_id,omitempty"`
	OrgID             int64   `json:"org_id,omitempty"`
	ExpectedCloseDate string  `json:"expected_close_date,omitempty"` // YYYY-MM-DD
	Probability       *int    `json:"probability,omitempty"`         // 0-100; nil = use stage default
}

CreateDealRequest is the JSON body for POST /api/v2/deals. v2 requires `title` only; everything else has Pipedrive-side defaults (currency = workspace default, value = 0, owner_id = the API token's user, status = open, stage_id = first stage of the default pipeline, ...). The tool layer enforces title non-empty client-side so a typo surfaces as [validation] instead of an upstream 400.

Custom fields are intentionally omitted from this v0 — writing them needs the inverse name→hash resolver on FieldCache, which is a separate slice. Callers wanting to set custom fields today can edit the deal in the Pipedrive UI after creation.

type CreateNoteRequest

type CreateNoteRequest struct {
	Content   string `json:"content"`
	DealID    int64  `json:"deal_id,omitempty"`
	PersonID  int64  `json:"person_id,omitempty"`
	OrgID     int64  `json:"org_id,omitempty"`
	LeadID    string `json:"lead_id,omitempty"`
	ProjectID int64  `json:"project_id,omitempty"`
}

CreateNoteRequest is the JSON body for POST /api/v1/notes. Pipedrive requires Content + at least one anchor (DealID / PersonID / OrgID / LeadID); the tool layer enforces this client-side so a typo surfaces as [validation] instead of an upstream 400.

type CreateOrganizationRequest added in v0.2.0

type CreateOrganizationRequest struct {
	Name    string `json:"name"`
	OwnerID int64  `json:"owner_id,omitempty"`
	Address string `json:"address,omitempty"` // single-line; server-parsed
}

CreateOrganizationRequest is the JSON body for POST /api/v2/organizations. v2 requires `name`. Address is a single-line string on input — Pipedrive parses it into the structured response shape (Address.Country / Locality / PostalCode) server-side.

type CreatePersonRequest added in v0.2.0

type CreatePersonRequest struct {
	Name      string         `json:"name"`
	FirstName string         `json:"first_name,omitempty"`
	LastName  string         `json:"last_name,omitempty"`
	Emails    []ContactPoint `json:"emails,omitempty"`
	Phones    []ContactPoint `json:"phones,omitempty"`
	OrgID     int64          `json:"org_id,omitempty"`
	OwnerID   int64          `json:"owner_id,omitempty"`
}

CreatePersonRequest is the JSON body for POST /api/v2/persons. v2 requires `name`; first_name+last_name are an alternative the API can derive `name` from but this struct surfaces both for callers that already have the parts.

Emails and phones are each a list of {value, primary, label}. Multiple `primary: true` entries are silently coerced by Pipedrive — last one wins.

type Deal

type Deal struct {
	ID                int64          `json:"id"`
	Title             string         `json:"title"`
	Value             float64        `json:"value"`
	Currency          string         `json:"currency"`
	Status            string         `json:"status"` // open | won | lost | deleted
	StageID           int64          `json:"stage_id"`
	PipelineID        int64          `json:"pipeline_id"`
	OwnerID           int64          `json:"owner_id"`
	PersonID          int64          `json:"person_id"`
	OrgID             int64          `json:"org_id"`
	ExpectedCloseDate string         `json:"expected_close_date"`
	WonTime           string         `json:"won_time,omitempty"`
	LostTime          string         `json:"lost_time,omitempty"`
	LostReason        string         `json:"lost_reason,omitempty"`
	AddTime           string         `json:"add_time"`
	UpdateTime        string         `json:"update_time"`
	Probability       *int           `json:"probability,omitempty"`
	CustomFields      map[string]any `json:"custom_fields,omitempty"`
}

Deal is a Pipedrive deal record (subset). Custom fields are nested under custom_fields per the v2 API; the LLM-facing rendering in internal/tools/ resolves the 40-char hash keys into names via the per-Client deal field cache.

Times use Pipedrive's RFC3339-ish format with space (e.g. "2026-04-26 10:00:00"). They are surfaced to the LLM as raw strings so it can pattern-match without timezone surprises; callers that need time.Time should parse explicitly.

type Field

type Field struct {
	Key  string `json:"field_code"` // 40-char hash for custom fields; plain identifier for built-ins ("id", "title", ...)
	Name string `json:"field_name"` // human-readable label
}

Field is a Pipedrive field-metadata record (the subset the field caches need: hash-key → human-name resolution). Returned by /dealFields, /personFields, /organizationFields. v2 renamed `key` → `field_code` and `name` → `field_name` from v1; this struct is v2-shaped.

type FieldCache

type FieldCache struct {
	// contains filtered or unexported fields
}

FieldCache lazy-loads Pipedrive field metadata for one resource type and resolves the 40-char custom-field hash keys back to their human-readable names. Loaded behind a per-entry sync.Once: concurrent first-callers see one fetch, all subsequent callers observe the same error if the fetch failed. Reload() swaps the entry so the next access refetches, while in-flight callers safely complete on their own entry — never on a Reload-corrupted Once.

func NewFieldCache

func NewFieldCache(fetch func(context.Context) ([]Field, error)) *FieldCache

NewFieldCache wraps fetch; fetch is invoked at most once per Reload cycle and must return a stable snapshot of the resource's fields.

func (*FieldCache) Count

func (fc *FieldCache) Count() int

Count returns the number of fields currently cached. Returns 0 if the cache hasn't been loaded or the load failed. Used by the refresh_field_cache tool to surface a per-resource sanity check the LLM (and operator) can read after triggering a reload.

Count never triggers once.Do — calling once.Do(no-op) here would race with a concurrent first Load and silently seal the once, causing the real fetch to be skipped. Instead it reads the loaded flag (set inside once.Do *after* byKey is written), which provides the happens-before edge to byKey for free.

func (*FieldCache) Load

func (fc *FieldCache) Load(ctx context.Context) error

Load triggers the underlying fetch on first call. Every subsequent caller sees the same error so the LLM-facing tool can decide whether to fall back to raw output.

func (*FieldCache) Reload

func (fc *FieldCache) Reload()

Reload clears the cache so the next access refetches. Concurrent callers in flight at the moment of Reload finish their load on the outgoing entry (its once.Do is unaffected by this swap), and their already-acquired entry pointer continues to surface the old data. The next batch of callers sees a fresh entry and a fresh fetch.

func (*FieldCache) Resolve

func (fc *FieldCache) Resolve(ctx context.Context, raw map[string]any) map[string]any

Resolve returns a copy of raw with hash keys replaced by names. Unknown keys pass through verbatim so the LLM never silently loses data when the cache lags behind a freshly-created field; on cache load failure, raw is returned untouched.

Reads work off the entry snapshot rather than fc.cur so a Reload mid-call doesn't risk a torn read. Each entry's byKey is set under once.Do, which provides the happens-before for safe concurrent reads.

type GetActivityOptions

type GetActivityOptions struct {
	IncludeAttendees bool
}

GetActivityOptions controls the per-request shape of GetActivity. IncludeAttendees toggles `include_fields=attendees`. Future opt-ins (additional `include_fields` values Pipedrive may add) extend this struct without breaking the call signature.

type ItemType

type ItemType string

ItemType is the closed set of item types Pipedrive's /itemSearch endpoint accepts. The constants are reused by the tools package for input validation so the enum has a single source of truth.

const (
	ItemTypeDeal         ItemType = "deal"
	ItemTypePerson       ItemType = "person"
	ItemTypeOrganization ItemType = "organization"
	ItemTypeProduct      ItemType = "product"
	ItemTypeFile         ItemType = "file"
	ItemTypeLead         ItemType = "lead"
)

ItemType values matching Pipedrive's /itemSearch `item_types` enum.

type ListActivitiesOptions

type ListActivitiesOptions struct {
	OwnerID          int64
	DealID           int64
	LeadID           string
	PersonID         int64
	OrgID            int64
	Done             *bool
	UpdatedSince     string // RFC3339, e.g. 2026-01-01T00:00:00Z
	UpdatedUntil     string // RFC3339
	SortBy           string // id | update_time | add_time | due_date
	SortDirection    string // asc | desc
	IncludeAttendees bool
	Limit            int
	Cursor           string
}

ListActivitiesOptions filters a /activities list call. Zero values mean "no filter on this dimension".

v2 dropped the v1 `type` and `due_date` query params (filtering by activity-type or absolute due-date is not supported on the list endpoint — callers can sort by due_date instead). v2 list params are drawn from Pipedrive's official spec: filter_id, owner_id, deal_id, lead_id, person_id, org_id, done, updated_since, updated_until, sort_by, sort_direction, include_fields, limit, cursor.

Done is a tri-state: nil = no filter (the default), &true = only completed, &false = only open. The tool layer maps a human-friendly `status: open|done|all` enum to this field.

IncludeAttendees toggles `include_fields=attendees`; off by default, set to true to populate the Attendees slice on each activity. Pipedrive returns attendees only when explicitly requested.

type ListDealsOptions

type ListDealsOptions struct {
	Status        string // open | won | lost | deleted
	PipelineID    int64
	StageID       int64
	OwnerID       int64
	PersonID      int64
	OrgID         int64
	UpdatedSince  string // RFC3339
	UpdatedUntil  string // RFC3339
	SortBy        string // id | update_time | add_time
	SortDirection string // asc | desc
	Limit         int
	Cursor        string // opaque pagination token from a previous response
}

ListDealsOptions filters a /deals list call. Zero values mean "no filter on this dimension". Limit is clamped to [1, 500] by Pipedrive; the tool layer applies its own (smaller) cap before calling here.

Pipedrive v2 returns custom_fields nested in every deal record by default — there is no opt-in query parameter, and supplying `include_fields=custom_fields` is rejected with a 400.

type ListNotesOptions

type ListNotesOptions struct {
	UserID       int64
	DealID       int64
	PersonID     int64
	OrgID        int64
	LeadID       string // UUID
	ProjectID    int64
	StartDate    string // YYYY-MM-DD
	EndDate      string // YYYY-MM-DD
	UpdatedSince string // RFC3339
	Sort         string // "<field> asc|desc, ..." — v1 accepts either case; tools/effectiveSort emits lowercase
	Start        int
	Limit        int
}

ListNotesOptions filters a /api/v1/notes list call. v1 uses offset-style pagination (Start + Limit) — the tool layer encodes the int next_start as an opaque cursor string so the LLM-facing surface stays uniform with the v2 resources.

v1 supports many more filter dimensions than v2 list endpoints (start_date / end_date / pinned_to_X_flag) — wire only what the LLM-facing tools actually expose to keep the surface narrow.

type ListOrganizationsOptions

type ListOrganizationsOptions struct {
	OwnerID       int64
	UpdatedSince  string // RFC3339
	UpdatedUntil  string // RFC3339
	SortBy        string // id | update_time | add_time
	SortDirection string // asc | desc
	Limit         int
	Cursor        string
}

ListOrganizationsOptions filters a /organizations list call. v2's /organizations does not accept owner-of-deals or person-count filters; the available list-time dimensions are owner + update window. Zero values mean "no filter on this dimension".

type ListPersonsOptions

type ListPersonsOptions struct {
	OwnerID       int64
	OrgID         int64
	UpdatedSince  string // RFC3339
	UpdatedUntil  string // RFC3339
	SortBy        string // id | update_time | add_time
	SortDirection string // asc | desc
	Limit         int
	Cursor        string
}

ListPersonsOptions filters a /persons list call. Zero values mean "no filter on this dimension". v2 supports more params (filter_id, ids, deal_id, include_fields, custom_fields) — wire only what the LLM-facing tools actually expose to keep the surface narrow.

type Note

type Note struct {
	ID                       int64  `json:"id"`
	Content                  string `json:"content"`
	UserID                   int64  `json:"user_id"`
	LastUpdateUserID         *int64 `json:"last_update_user_id,omitempty"`
	DealID                   *int64 `json:"deal_id,omitempty"`
	PersonID                 *int64 `json:"person_id,omitempty"`
	OrgID                    *int64 `json:"org_id,omitempty"`
	LeadID                   string `json:"lead_id,omitempty"`
	ProjectID                *int64 `json:"project_id,omitempty"`
	AddTime                  string `json:"add_time"`
	UpdateTime               string `json:"update_time"`
	ActiveFlag               bool   `json:"active_flag"`
	PinnedToDealFlag         bool   `json:"pinned_to_deal_flag,omitempty"`
	PinnedToPersonFlag       bool   `json:"pinned_to_person_flag,omitempty"`
	PinnedToOrganizationFlag bool   `json:"pinned_to_organization_flag,omitempty"`
	PinnedToLeadFlag         bool   `json:"pinned_to_lead_flag,omitempty"`
	PinnedToProjectFlag      bool   `json:"pinned_to_project_flag,omitempty"`
}

Note is a Pipedrive v1 note record. v2 has no /notes endpoint (Pipedrive officially recommends staying on v1 for notes per developer-community thread, 2025-05). The carve-out is documented in CLAUDE.md hard rule #1.

AddTime / UpdateTime use v1's `YYYY-MM-DD HH:MM:SS` format (UTC, space-separated — distinct from v2's RFC3339).

Foreign keys are pointer-typed because v1 returns null when the note isn't anchored to that entity. LeadID is a UUID string; the rest are int IDs. The pinned-to-* fields come back as actual JSON booleans on v1 (verified live, 2026-04 — earlier external docs claimed 0/1 ints; the docs were wrong).

type Options

type Options struct {
	BaseURL string
	Token   string
	Timeout time.Duration
	Logger  *slog.Logger
}

Options configures a new Client. BaseURL is the v2 base (e.g. "https://acme.pipedrive.com/api/v2"); only the scheme+host portion is retained — the per-request API path is composed in `do`.

type Organization

type Organization struct {
	ID           int64          `json:"id"`
	Name         string         `json:"name"`
	Address      *Address       `json:"address,omitempty"`
	OwnerID      int64          `json:"owner_id"`
	PeopleCount  int            `json:"people_count,omitempty"`
	AddTime      string         `json:"add_time"`
	UpdateTime   string         `json:"update_time"`
	CustomFields map[string]any `json:"custom_fields,omitempty"`
}

Organization is a Pipedrive organization record (subset). Custom fields nest under custom_fields per the v2 API.

type Person

type Person struct {
	ID           int64          `json:"id"`
	Name         string         `json:"name"`
	FirstName    string         `json:"first_name,omitempty"`
	LastName     string         `json:"last_name,omitempty"`
	Emails       []ContactPoint `json:"emails,omitempty"`
	Phones       []ContactPoint `json:"phones,omitempty"`
	OrgID        int64          `json:"org_id"`
	OwnerID      int64          `json:"owner_id"`
	AddTime      string         `json:"add_time"`
	UpdateTime   string         `json:"update_time"`
	CustomFields map[string]any `json:"custom_fields,omitempty"`
}

Person is a Pipedrive person record (subset). Custom fields are nested under custom_fields per the v2 API; the LLM-facing rendering in internal/tools/ resolves the 40-char hash keys into names via the per-Client person field cache.

type Pipeline

type Pipeline struct {
	ID      int64  `json:"id"`
	Name    string `json:"name"`
	OrderNr int    `json:"order_nr"`
	Active  bool   `json:"active"`
}

Pipeline is a Pipedrive pipeline (a deal flow grouping). Subset of the /api/v2/pipelines response that we surface to LLM clients.

type SearchHit

type SearchHit struct {
	Score float64
	Item  map[string]any
}

SearchHit is one record returned by /api/v2/itemSearch. The pipedrive layer keeps the per-type record as a raw map so the tools layer can decide how to render it for the LLM (deal `title` vs person `name`, strip-list of internal fields, etc.) without leaking presentation concerns into the HTTP layer.

type SearchOptions

type SearchOptions struct {
	Term       string
	ItemTypes  []string
	ExactMatch bool
	Limit      int
	Cursor     string
}

SearchOptions filters an /itemSearch call. Term is required and must be at least 2 characters (1 if ExactMatch). Empty ItemTypes means "search all of deal | person | organization | product | file | lead". Limit is clamped by the tool layer.

type Stage

type Stage struct {
	ID              int64  `json:"id"`
	Name            string `json:"name"`
	OrderNr         int    `json:"order_nr"`
	Active          bool   `json:"active_flag"`
	PipelineID      int64  `json:"pipeline_id"`
	DealProbability int    `json:"deal_probability"`
}

Stage is a Pipedrive stage within a pipeline. Subset of /api/v2/stages.

Note: Active maps to the upstream `active_flag` field, while Pipeline uses `active`. This is Pipedrive's API, not a copy-paste error — confirmed against /api/v2/stages and /api/v2/pipelines responses.

type V1Pagination

type V1Pagination struct {
	Start                 int  `json:"start"`
	Limit                 int  `json:"limit"`
	MoreItemsInCollection bool `json:"more_items_in_collection"`
	NextStart             int  `json:"next_start,omitempty"`
}

V1Pagination is Pipedrive v1's offset-style paging envelope. MoreItemsInCollection signals whether NextStart is meaningful; when false, the caller is on the last page.

type WebURLKind

type WebURLKind string

WebURLKind identifies which Pipedrive web-UI route to build a URL for.

const (
	WebURLPipeline     WebURLKind = "pipeline"
	WebURLDeal         WebURLKind = "deal"
	WebURLPerson       WebURLKind = "person"
	WebURLOrganization WebURLKind = "organization"
	WebURLActivity     WebURLKind = "activity"
)

WebURL kinds covering the resource types the LLM-facing tools surface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL