Manage your partner CRM from code

You read, update, and sync your partner CRM from a workflow, an app action, or an agent.

Who can use it

The workspace sponsor must be an active partner. Every member's code then acts on that partner's CRM: it can read and edit every lead, and import from Orbitype on the partner's credits. It works in workflows, agents, and app actions. Open the partner portal for the same pipeline. Methods return { error } instead of throwing.

Read leads

  • partner.crm.list(filters) returns { leads, total }.
  • partner.crm.get(id) returns one lead with its timeline. The timeline shows your own activity. Activity by an earlier partner owner is left out.
  • partner.crm.lookup({ email, linkedinUrl, website }) checks a person before you add them.
  • partner.crm.forecast(filters) returns pipeline totals.
  • partner.crm.dealTotals(deal) returns the math for one deal.
async ({ partner }) => {
  const { leads } = await partner.crm.list({ status: "New" })
  return leads
}

Create and import

  • partner.crm.create({ profile, deal }) needs an email or a LinkedIn URL.
  • partner.crm.import(rows) takes up to 500 rows.

A blocked account comes back as { error } (403). A lead another partner owns comes back as { error } (409).

Move leads and edit deals

  • partner.crm.update({ id, profile, deal, status })
  • partner.crm.setStatus(id, status, fadedReason)
  • partner.crm.updateDeal(id, deal)

Statuses, in order: New, First email, First call, Deploy Pilot / Trial Sign up, Provide Feedback, Implement Changes, Analyze Data, Meet with other stakeholders, Design full scale and scope, Leadership approval, IT department approval, Legal Approval, Budget Approval, Signed Contract, Money Received, Faded Out.

A lead still in New is released 7 days after it was assigned. After that, each move to a stage the lead has not reached before gives 83 more days, 90 days in total. Moving back and forward again does not renew the window. Each open lead has releaseAt.

Log activity

  • partner.crm.addActivity({ id, type, title, body }) — type is note, call, or email.
  • partner.crm.addNote(id, body)
async ({ partner }) => {
  await partner.crm.setStatus(leadId, "First email")
  await partner.crm.addNote(leadId, "Sent the intro mail")
}

Import from an ICP

  • partner.crm.icps() returns the three ICPs and creditsLeft.
  • partner.crm.importIcp(icpId) fills New leads up to 100. 80 already in New imports 20.

The ICPs are saas-founder, agency, and enterprise. Each contact costs the normal full-contact unlock price, up to 18 lead credits. The batch stops when the credits run out. You can import once per run. A new lead must leave New within 7 days or it goes back to the pool.

async ({ partner }) => {
  return await partner.crm.importIcp("saas-founder")
}

Sync with another CRM

partner.crm.changes({ since, cursor }) returns leads changed after since, plus removed and nextCursor. partner.crm.allChangesSince(since) walks every page.

partner.crm.upsert({ match, profile, deal, status }) updates your lead or creates one. Match priority is id, then externalId, then email, then linkedinUrl.

async ({ partner }) => {
  const { leads, removed } = await partner.crm.allChangesSince("2026-01-01T00:00:00.000Z")
  for (const lead of leads) {
    await partner.crm.upsert({
      match: { externalId: lead.profile.externalId ?? lead.id },
      profile: lead.profile,
      status: lead.status,
    })
  }
  return { pulled: leads.length, removed }
}

Errors and limits

Wrapper methods return { error }. The API is throttled. Each ICP import spends credits, so check creditsLeft from partner.crm.icps() before you import in a loop.