| name | my-crm-api |
| version | 1.0.0 |
| description | The canonical store of engaged contacts + companies for an org. Auto-ingests from inbound webhooks via a configurable dot-path. Fixed lifecycle_stage enum (cold | warm | qualified | customer | churned). Append-only event timeline with reserved kinds. Soft delete + restore. Promote-from-Goldfox closes the discovery → engagement loop.
|
| triggers | ["crm","contact","company","lead","engagement","pipeline","lifecycle","qualified","customer","webhook ingest","promote"] |
| checksum | sha256-pending |
MyCRMAPI
The store that closes the funnel. Today's loop without CRM: discover people (Goldfox) → save audience → send email → track pixel → form-fill via webhook → … nothing. People who engage live nowhere. CRM is where they land — automatically.
Capabilities
Where CRM sits in the stack
- mypeopleapi / mycompanyapi — read-only Goldfox global lead universe (the crawl)
- myaudienceapi — saved filter snapshots over Goldfox
- mycrmapi — engaged contacts + companies, private to your org, with engagement history
A Goldfox row becomes a CRM contact when:
- You explicitly promote it (
myapi crm contacts promote <goldfox_person_id>)
- Or a downstream service receives engagement for that email and auto-upserts the contact
Lifecycle stages (fixed enum — same for contacts and companies)
cold | warm | qualified | customer | churned
Move stage with myapi crm contacts update <id> --stage qualified. Every stage change emits a stage_changed event with {from, to} so the timeline shows the journey.
Contact sources (fixed enum)
goldfox | email | pixel | webhook | manual
Set automatically based on how the contact entered the CRM. Useful filter when you want "only contacts I've personally added" (--source manual) vs "warmed via Goldfox outreach" (--source goldfox).
Event timeline — reserved kinds
created | promoted | stage_changed
email_sent | email_opened | email_clicked | email_replied
pixel_visit | webhook_received | payment
Agents cannot write events directly — the closed enum is intentional. If you need custom state, use mydatabaseapi (KV) keyed on the contact id; the curated timeline stays authoritative for engagement.
Engagement-event kinds bump last_engagement_at: email_sent/opened/clicked/replied, pixel_visit, webhook_received. Admin kinds (created, promoted, stage_changed) don't — promoting a Goldfox lead isn't engagement.
Auto-ingest
Today (v1):
- Webhook: configurable per endpoint via
crm_email_path — a JSON dot-path. Default "email" ingests {"email":"x@y.com"}. For Stripe, set data.object.customer_email; for GitHub, sender.email. Empty string disables ingest for that endpoint.
Coming next (backend wiring in progress):
- Email: every
myapi email message send writes email_sent; opens/clicks fire email_opened/email_clicked
- Pixel:
identify calls with an email write pixel_visit
If a contact doesn't exist for the matched email, it's auto-created with source= matching the originating service. The contact's company is auto-linked by email domain (creates the company on first sight).
Soft delete + restore
myapi crm contacts delete <id> sets deleted_at but retains the event timeline. By default soft-deleted contacts are excluded from search — pass --include-deleted to see them. Restore with myapi crm contacts restore <id>.
Goldfox enrichment (deferred)
A CRM contact promoted from Goldfox carries a goldfox_person_id reference. In v2 the GET response will join to Goldfox live, embedding the latest row inline as goldfox_person. Today that field is null until the BQ get-by-id helper lands. Search filters that hit Goldfox-only fields are not yet wired into CRM search.
Search filter — re-engagement semantics
--max-last-engagement-days N returns contacts last engaged more than N days ago. This intentionally includes contacts with no engagement at all (promoted-but-never-emailed Goldfox leads), because those are exactly the natural targets of a re-engagement campaign. If you want to distinguish "never tried" from "tried and went cold," layer --source goldfox (never engaged after promotion) or post-filter the JSON.
Failure modes
404 CONTACT_NOT_FOUND / COMPANY_NOT_FOUND
409 on duplicate email/domain in the same org
400 INVALID_STAGE if you pass a value outside the enum
400 GOLDFOX_PERSON_NOT_FOUND on promote with an unknown id
Commands
Contacts
| Command | What it does |
|---|
myapi crm contacts list [--limit N] | List all contacts (newest engagement first) |
myapi crm contacts search [--stage ...] [--source ...] [--email ...] [--min/max-last-engagement-days N] | Filter contacts |
myapi crm contacts create <email> [--first-name ...] [--last-name ...] [--stage ...] [--custom-json ...] | Manually create (source='manual') |
myapi crm contacts get <id> | Fetch one contact (with embedded Goldfox enrichment when available) |
myapi crm contacts update <id> [--stage ...] [...] | Patch fields. Stage change emits stage_changed event |
myapi crm contacts delete <id> | Soft delete (events retained) |
myapi crm contacts restore <id> | Restore a soft-deleted contact |
myapi crm contacts promote <goldfox_person_id> | Idempotent Goldfox → CRM promotion |
myapi crm contacts events <id> [--kind ...] | Timeline (newest first), filter by kind |
Companies
| Command | What it does |
|---|
myapi crm companies list / search / create / get / update / delete / restore | Same shape as contacts |
myapi crm companies promote <domain> | Goldfox company id IS its domain — pass the domain |
All commands accept --org <id> (or set default: myapi config set-org <id>) and --json for machine-readable output.
Examples
myapi people search --keyword saas --has-c-level --country US --limit 5 --json \
| jq -r '.people[].id' \
| while read pid; do myapi crm contacts promote "$pid"; done
myapi crm contacts search --stage qualified --json | jq -r '.contacts[].email'
myapi crm contacts search --max-last-engagement-days 30
myapi crm contacts create alice@acme.com \
--first-name Alice --stage warm \
--custom-json '{"intro_via":"riccardo","topic":"video editing"}'
myapi crm contacts update <id> --stage qualified
myapi crm contacts update <id> --stage customer
myapi crm contacts events <id>
myapi crm contacts search --source webhook --json \
| jq '.contacts[] | {email, last_engagement_at}'
End-to-end recipe — configure a webhook that auto-creates CRM contacts
WH=$(myapi webhook create stripe --crm-email-path 'data.object.customer_email' --json)
URL=$(echo "$WH" | jq -r .url)
echo "Point Stripe at: $URL"
myapi crm contacts search --source webhook --email "$STRIPE_CUSTOMER_EMAIL"
myapi crm contacts events <id> --kind webhook_received
Notes
- Reserved event kinds — no custom events in v1. If an agent needs custom state per contact, use
myapi database keyed by contact id. The curated timeline stays the authoritative engagement record.
- Event payloads carry an
external_id — the backend's idempotency key, equal to the underlying action's natural id (goldfox_person_id for promoted, delivery_id for webhook_received). Read the semantic field; external_id is a backend-internal duplicate. A legacy message_id on old rows holds the same value — safe to ignore.
- Email + Pixel auto-ingest not yet wired. Only
webhook_received events fire today. Email and pixel ingest are coming — the CLI surface stays unchanged when they land.
- Free in v1. Metered later if usage shows a need.
Run myapi crm --help or myapi crm <namespace> --help for inline reference.