my-crm-api
$
npx mdskill add myapihq/myapi/my-crm-apiManages engaged contacts and companies with lifecycle stages and event timelines.
- Promotes Goldfox leads to CRM contacts and auto-upserts from engagement webhooks.
- Depends on Goldfox APIs for lead discovery and webhooks for engagement ingestion.
- Uses fixed lifecycle stage enum and append-only event timeline to track engagement.
- Returns contact/company data with stage, timeline, and supports soft delete and restore.
SKILL.md
.github/skills/my-crm-apiView on GitHub ↗
---
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
<!-- llm:start -->
### 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:
1. You explicitly **promote** it (`myapi crm contacts promote <goldfox_person_id>`)
2. *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":"[email protected]"}`. 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
<!-- llm:end -->
## Commands
<!-- generated:start -->
### 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 |
<!-- generated:end -->
All commands accept `--org <id>` (or set default: `myapi config set-org <id>`) and `--json` for machine-readable output.
## Examples
<!-- llm:start -->
```bash
# Discover → promote → engage workflow
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
# Find everyone in 'qualified' for a follow-up email
myapi crm contacts search --stage qualified --json | jq -r '.contacts[].email'
# Re-engagement: contacts last engaged > 30 days ago (or never engaged at all)
myapi crm contacts search --max-last-engagement-days 30
# Manual add + tag with custom fields
myapi crm contacts create [email protected] \
--first-name Alice --stage warm \
--custom-json '{"intro_via":"riccardo","topic":"video editing"}'
# Update stage as the deal progresses — emits a stage_changed event
myapi crm contacts update <id> --stage qualified
myapi crm contacts update <id> --stage customer
# See the full engagement timeline
myapi crm contacts events <id>
# What landed in CRM from this Stripe webhook?
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
```bash
# Endpoint that ingests Stripe customer.created events
WH=$(myapi webhook create stripe --crm-email-path 'data.object.customer_email' --json)
URL=$(echo "$WH" | jq -r .url)
echo "Point Stripe at: $URL"
# Later, after Stripe fires...
myapi crm contacts search --source webhook --email "$STRIPE_CUSTOMER_EMAIL"
myapi crm contacts events <id> --kind webhook_received
```
<!-- llm:end -->
## 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.