Skill v1.0.1
currentAutomated scan100/100+2 new
name: my-crm-api version: 1.0.1 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-285ad94b1266588d103119ec139f11f0d19831bcbfd12779130f9fbeaf7f33f5
MyCRMAPI
The store that closes the funnel. Without CRM the loop ends nowhere: discover people (Goldfox) → audience → email → pixel → form-fill via webhook → … nothing. People who engage live nowhere. CRM is where they land.
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:
- 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 from how the contact entered. Filter with --origin manual (added by hand) vs --origin goldfox (from outreach).
Event timeline — reserved kinds
created | promoted | stage_changedemail_sent | email_opened | email_clicked | email_repliedpixel_visit | webhook_received | payment
Agents cannot write events directly — the closed enum is intentional. For custom state use mydatabaseapi (KV) keyed on the contact id; the curated timeline stays authoritative.
Engagement kinds bump `last_engagement_at`: email_*, pixel_visit, webhook_received. Admin kinds (created, promoted, stage_changed) don't — promoting a lead isn't engagement.
Auto-ingest
Today (v1):
- Webhook: set per endpoint via
crm_email_path, a JSON dot-path. Default"email"ingests{"email":"x@y.com"}. For Stripe, setdata.object.customer_email; for GitHub,sender.email. Empty string disables ingest.
Coming next (backend wiring in progress):
- Email: every
myapi email message sendwritesemail_sent; opens/clicks fireemail_opened/email_clicked - Pixel:
identifycalls with an email writepixel_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).
Missing lead? Check `myapi webhook deliveries` before concluding it never arrived. A deadlock under concurrent ingest could drop the contact after the form returned success (fixed 2026-07-27). Raw payloads are always stored, so the delivery is there even when the contact isn't.
Soft delete + restore
myapi crm contacts delete <id> sets deleted_at but keeps 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 contact promoted from Goldfox carries a goldfox_person_id. In v2 the GET response will embed the row as goldfox_person; today it is null. Goldfox-only fields are not yet searchable.
Search filter — re-engagement semantics
--max-last-engagement-days N returns contacts last engaged more than N days ago, and intentionally includes contacts with no engagement at all (promoted-but-never-emailed Goldfox leads) — the natural targets of a re-engagement campaign. --min-last-engagement-days N is its complement: engaged within N days. --company-id <id> narrows to one company. To separate "never tried" from "tried and went cold," layer --origin goldfox.
Failure modes
404 CONTACT_NOT_FOUND/COMPANY_NOT_FOUND409on duplicate email/domain in the same org400 INVALID_STAGEif you pass a value outside the enum400 GOLDFOX_PERSON_NOT_FOUNDon promote with an unknown id
<!-- llm:end -->
Commands
<!-- generated:start -->
Contacts
| Command | What it does | |
|---|---|---|
myapi crm contacts list [--limit N] [--offset N] | List all contacts (newest engagement first) | |
myapi crm contacts search [--stage ...] [--origin ...] [--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 promote | |
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. --domain filters search, --name sets display name | |
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 -->
# Discover → promote → engage workflowmyapi 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 emailmyapi 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 fieldsmyapi crm contacts create alice@acme.com \--first-name Alice --stage warm \--custom-json '{"intro_via":"riccardo","topic":"video editing"}'# Update stage as the deal progresses — emits a stage_changed eventmyapi crm contacts update <id> --stage qualifiedmyapi crm contacts update <id> --stage customer# See the full engagement timelinemyapi crm contacts events <id># What landed in CRM from this Stripe webhook?myapi crm contacts search --origin webhook --json \| jq '.contacts[] | {email, last_engagement_at}'
End-to-end recipe — configure a webhook that auto-creates CRM contacts
# Endpoint that ingests Stripe customer.created eventsWH=$(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 --origin webhook --email "$STRIPE_CUSTOMER_EMAIL"myapi crm contacts events <id> --kind webhook_received
<!-- llm:end -->
Notes
- Paginate with `--limit` + `--offset`.
totalis the true match count
and the response carries has_more; branch on has_more rather than doing arithmetic against total. (Both were broken until 2026-07-28. Cached guidance saying the CRM cannot paginate is stale.)
- Reserved event kinds — no custom events in v1. If an agent needs custom state per contact, use
myapi databasekeyed by contact id. The curated timeline stays the authoritative engagement record. - `external_id` on an event payload is the backend's idempotency key — a duplicate of the action's natural id (
goldfox_person_id,delivery_id). Read the semantic field instead; legacymessage_idrows hold the same value. - Only `webhook_received` fires today. Email and pixel auto-ingest are coming; the CLI surface will not change.
- Free in v1. Metered later if usage warrants.
HTTP (from deployed code)
<!-- http:start --> <!-- generated by npm run canonical-sync — do not edit -->
base https://api.myapihq.compath POST /crm/orgs/{org_id}/contactsauth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)reply { "success": true, "data": …, "error": null, "meta": {…} }
- Per-slot host — do not assume one host serves every slot.
- Org id goes in the PATH — there is no
X-Org-Idheader.
<!-- http:end -->
Run myapi crm --help or myapi crm <namespace> --help for inline reference.