Match a product to contacts
Two ways to find who to pitch a product to:
- Prospects (no AI on our side): get every emailable contact with a short digest of their timeline, and let your own model judge fit. Nothing is metered.
- Match (our AI): the AI scores each contact against the product. This is a metered AI action.
Both return contact names, emails and timeline content, so they need read:contacts as well as the offerings scope. Product IDs in the path accept the agentlyleads ID or your externalId.
GET /api/v1/products/{id}/prospects
Requires read:offerings and read:contacts. limit is 1 to 200 (default 50).
{
"offeringId": "cmq2offr0alk34567890",
"offering": { "name": "Lick salt", "category": "Livestock", "description": "10 kg blocks", "price": 12, "unit": "block", "sku": "LS-10" },
"prospects": [
{
"contactId": "cmq2cont0alk34567890",
"name": "Ann Lee",
"title": "Purchasing",
"company": "Lee Farms",
"email": "ann@leefarms.com",
"status": "ACTIVE",
"category": "Farm",
"timelineDigest": "[2026-08-02] EMAIL INBOUND: Asked about bulk mineral blocks for 200 head."
}
],
"skippedNoEmail": 4,
"skippedSuppressed": 1,
"totalEligible": 1
}POST /api/v1/products/{id}/match
Requires write:offerings and read:contacts. Scores the product against each emailable, non-suppressed contact from what their timeline says they buy.
POST /api/v1/products/cmq2offr0alk34567890/match HTTP/1.1
Host: agentlyleads.com
Authorization: Bearer alk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "maxContacts": 50 }| Field | Notes |
|---|---|
contactIds | Only judge these contacts (up to 200). IDs outside your workspace are ignored |
maxContacts | Judge at most this many (1 to 200, default 200) |
The AI judges one contact per call to the model, so a run is metered by the number of contacts judged. Use maxContacts or contactIds to keep it small.
Response (201)
{
"match": {
"id": "cmq2mtch0alk34567890",
"status": "REVIEW",
"offering": { "id": "cmq2offr0alk34567890", "name": "Lick salt", "sku": "LS-10", "category": "Livestock" },
"campaignId": null,
"candidates": [
{
"id": "cmq2cand0alk34567890",
"contact": { "id": "cmq2cont0alk34567890", "name": "Ann Lee", "email": "ann@leefarms.com", "title": "Purchasing", "company": "Lee Farms" },
"fitScore": 84,
"tier": "STRONG",
"reason": "Runs a 200-head herd and asked about mineral blocks in August.",
"angle": "Bulk pricing for the herd size she mentioned.",
"decision": "INCLUDED",
"emailMessageId": null
}
],
"createdAt": "2026-09-27T15:00:00.000Z",
"updatedAt": "2026-09-27T15:00:10.000Z"
},
"judged": 1,
"skippedNoEmail": 4,
"skippedSuppressed": 1
}Tiers: STRONG is 70 and above, POSSIBLE 40 to 69, WEAK below 40. Candidates scoring 60 or more start INCLUDED.
GET /api/v1/outreach-matches/{id}
Requires read:offerings and read:contacts. Returns { "match": { ... } } in the shape above.
PATCH /api/v1/outreach-matches/{id}/candidates/{candidateId}
Requires write:offerings. Sets whether a candidate gets a draft.
{ "decision": "EXCLUDED" }decision is INCLUDED, EXCLUDED or PENDING. Returns { "candidate": { "id", "matchId", "decision" } }.
POST /api/v1/outreach-matches/{id}/drafts
Requires write:offerings and write:emails. The AI writes one personalized email per INCLUDED candidate, grounded in the contact's timeline and the product.
Nothing is sent. Each email is saved as a draft under an Outreach: <product name> campaign and waits for a person to approve it in the app, or for POST /api/v1/emails/{id}/send with the send:emails scope. Candidates that already have a draft are skipped, so a repeat only fills gaps. asUserEmail in the body picks the workspace member the drafts are written as (the workspace owner by default).
{ "matchId": "cmq2mtch0alk34567890", "drafted": 12, "skipped": 0, "campaignId": "cmq2camp0alk34567890" }Billing and errors
match and drafts are metered AI actions, the same as running them in the app. If the workspace has its own AI key for these features, they run on it and are not counted.
| Status | When |
|---|---|
402 | The workspace has used its AI actions for the month |
403 | A required scope is missing (the message names it) |
404 | No product, match or candidate with that ID in your workspace |
502 | The AI provider failed, or rejected the workspace's own key |