From cd6beafe905a078bf3d1e2f33d94466209db85e8 Mon Sep 17 00:00:00 2001
From: weilun <1083432533@qq.com>
Date: Mon, 15 Jun 2026 14:51:26 +0800
Subject: [PATCH] docs: clarify credit and async API behavior
---
api-reference/contacts/unlock-task.mdx | 9 ++
api-reference/contacts/unlock.mdx | 7 +
api-reference/events/detail.mdx | 12 +-
api-reference/events/rank.mdx | 4 +-
api-reference/exhibitors/list.mdx | 11 +-
.../exhibitors/search-by-company-name.mdx | 11 ++
api-reference/exhibitors/search-events.mdx | 8 +-
.../personnel/events-by-linkedin.mdx | 11 ++
...actions-apply-recommended-events-paged.mdx | 8 +
changelog.mdx | 6 +
concepts/credits-and-access.mdx | 14 ++
guides/production-readiness.mdx | 11 +-
guides/quickstart.mdx | 18 +--
guides/unlock-contact-emails.mdx | 7 +-
llms-full.txt | 137 +++++++++++++++---
15 files changed, 234 insertions(+), 40 deletions(-)
diff --git a/api-reference/contacts/unlock-task.mdx b/api-reference/contacts/unlock-task.mdx
index e37e2cf..4eeb498 100644
--- a/api-reference/contacts/unlock-task.mdx
+++ b/api-reference/contacts/unlock-task.mdx
@@ -79,6 +79,15 @@ Item status:
- `unlocked`
- `failed`
+## Result interpretation
+
+Treat each `items[]` entry independently:
+
+- Count a contact as delivered only when item `status` is `unlocked` and `email` is present.
+- Surface `failed` items separately with `errorCode` when provided.
+- A task can reach a terminal state while individual items have different outcomes.
+- Refresh credit balance and contact/personnel records after terminal states when your UI needs billing or access-state reconciliation.
+
## Polling guidance
Use a backoff schedule instead of polling aggressively:
diff --git a/api-reference/contacts/unlock.mdx b/api-reference/contacts/unlock.mdx
index 43a03dd..0232f63 100644
--- a/api-reference/contacts/unlock.mdx
+++ b/api-reference/contacts/unlock.mdx
@@ -17,6 +17,10 @@ This endpoint is intentionally asynchronous because email unlock can involve mul
For best user experience, show the number of locked contacts and available credit balance before creating the unlock task.
+
+ A `201 Created` response means the unlock task was accepted. It does not guarantee that every submitted personnel record will return an email. Always inspect item-level task results before counting a contact as enriched.
+
+
## Endpoint
`POST /external/contacts/unlock`
@@ -78,12 +82,14 @@ Use backoff instead of a tight polling loop. A practical pattern is:
2. Poll every few seconds for short jobs.
3. Increase the interval if the job remains in progress.
4. Stop polling when the task completes or fails.
+5. Re-fetch personnel or contact records if your UI needs the latest `email` and `contactUnlockStatus` values.
## Credit behavior
Contact email unlock currently costs `15` credits per chargeable contact.
- Already unlocked contacts are not charged again.
+- Failed or ineligible items may not produce an email. Treat the task result and refreshed balance as the source of truth for actual delivery and billing.
- The API rejects batches larger than `100` personnel IDs.
- Insufficient balance returns `402 Payment Required`.
- Retry behavior should be tied to the task state, not only the create response.
@@ -98,4 +104,5 @@ Contact email unlock currently costs `15` credits per chargeable contact.
## Notes
- This endpoint creates an asynchronous unlock job. Poll [Get contact unlock task](/api-reference/contacts/unlock-task) with `task_id`.
+- Do not create duplicate unlock tasks while a prior task for the same selected contacts is still pending or processing.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
diff --git a/api-reference/events/detail.mdx b/api-reference/events/detail.mdx
index f6acf7a..6603d65 100644
--- a/api-reference/events/detail.mdx
+++ b/api-reference/events/detail.mdx
@@ -102,12 +102,22 @@ curl "https://platform.lensmor.com/external/events/139574" \
| `url` | Source or official event URL when available. |
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country`, `latitude`, `longitude` | Location metadata. Coordinates are string values (e.g. `"36.1313238"`) or `null`. |
-| `attendeeCount`, `exhibitorCount`, `personnelCount` | Known scale and coverage fields when available. |
+| `attendeeCount`, `exhibitorCount`, `personnelCount` | Known summary scale and coverage fields when available. These fields can be `null` or differ from current event-scoped list totals. |
| `priceLower`, `priceUpper` | Price range as string values (e.g. `"1295"`) or `null`. |
| `eventType`, `eventTypes`, `categories`, `topics`, `topicsCount` | Classification metadata. `categories` is an array of objects with `id`, `code`, `name`, `description`, `confidence` fields. |
| `verified`, `future`, `historic`, `historicEvent` | Status flags and historical relationship metadata. |
| `image`, `dataSource` | Media and source metadata. |
+## Count fields
+
+Treat `exhibitorCount` and `personnelCount` on event detail as summary metadata for display and planning. They are not the authoritative count for the current caller's access state, filters, or page visibility.
+
+When you need current coverage numbers, call the event-scoped list endpoint and use its pagination metadata:
+
+- [List event exhibitors](/api-reference/exhibitors/list) for exhibitor totals.
+- [List event personnel](/api-reference/personnel/list) for personnel totals.
+- `semantics.counts` when present for visible, locked, and matched access context.
+
## Error responses
- `401 Unauthorized`
- `404 Not Found`
diff --git a/api-reference/events/rank.mdx b/api-reference/events/rank.mdx
index 0dcce97..35ed97c 100644
--- a/api-reference/events/rank.mdx
+++ b/api-reference/events/rank.mdx
@@ -79,7 +79,9 @@ curl -X POST "https://platform.lensmor.com/external/events/rank" \
- Preserve the original submitted event IDs in your client if you need to reconcile ranked results with local records.
- Display `rank` and `match_score` together when showing ordered recommendations.
-- If `reasons` is empty, use event metadata and match score rather than showing a blank explanation block.
+- Treat the endpoint as shortlist ordering, not a complete recommendation report by itself.
+- Live responses can return low or zero `match_score` values and empty `reasons`. Combine returned order with event metadata, matched counts, and follow-up detail or list calls before making a final business recommendation.
+- If `reasons` is empty, use event metadata and evidence from follow-up calls rather than showing a blank explanation block.
## Error responses
- `400 Bad Request`
diff --git a/api-reference/exhibitors/list.mdx b/api-reference/exhibitors/list.mdx
index 5d39810..2c44a3e 100644
--- a/api-reference/exhibitors/list.mdx
+++ b/api-reference/exhibitors/list.mdx
@@ -15,7 +15,7 @@ Common use cases:
- build an event exhibitor directory
- filter exhibitors by industry, category, geography, or company keyword
-- request a small nested personnel sample with `personnelLimit`
+- request a best-effort nested personnel sample with `personnelLimit`
- decide whether an event is worth unlocking for full coverage
## Endpoint
@@ -40,7 +40,7 @@ See [Authentication](/authentication)
| `jobTitle` | No | string[] | Personnel job-title filter. Repeated query parameters are supported. |
| `managementLevel` | No | string[] | Personnel management-level filter, such as `vp` or `c_suite`. |
| `department` | No | string[] | Personnel department filter, such as `marketing` or `sales`. |
-| `personnelLimit` | No | integer | Maximum nested personnel rows per exhibitor. Use `0` or omit for none. |
+| `personnelLimit` | No | integer | Maximum nested personnel rows per exhibitor when available. Use `0` or omit for none. Treat nested personnel as best-effort; use `GET /external/personnel/list` with `event_id` and `exhibitor_id` when contacts are required. |
## Request example
```bash
@@ -153,6 +153,12 @@ curl "https://platform.lensmor.com/external/exhibitors/list?event_id=139574&indu
| `recommendationProcessingFeature` | Which recommendation feature is processing. `"none"` when idle. |
| `semantics` | Preview/full access metadata for the selected event. |
+## Personnel samples
+
+`personnelLimit` asks the API to include a small personnel sample for each exhibitor when the service has a suitable sample for the current event, filters, and access state. It is not a guarantee that every exhibitor item will contain nested personnel rows.
+
+When your workflow needs buyer contacts for a specific company, call [Personnel list](/api-reference/personnel/list) with both `event_id` and `exhibitor_id`. Treat that event-scoped personnel response as the source of truth for people coverage and contact unlock state.
+
## Access semantics
When an event is locked, this endpoint can return a preview slice instead of full results. Use `semantics` to explain the state to users:
@@ -174,3 +180,4 @@ When an event is locked, this endpoint can return a preview slice instead of ful
- `matched_event_ids` reflects the requested event scope.
- `techStacks` is always returned as an array; when no data is available, the API returns `techStacks: []`.
- Locked events can return preview results. Use `semantics.unlock` to decide whether to call [Unlock event](/api-reference/events/unlock).
+- After event unlock, request the target page again and confirm `semantics.accessMode` moved to `full` before assuming all matching exhibitors are available.
diff --git a/api-reference/exhibitors/search-by-company-name.mdx b/api-reference/exhibitors/search-by-company-name.mdx
index 4d5f669..0df56ee 100644
--- a/api-reference/exhibitors/search-by-company-name.mdx
+++ b/api-reference/exhibitors/search-by-company-name.mdx
@@ -19,6 +19,10 @@ Choose this endpoint when:
Use [Exhibitor event search](/api-reference/exhibitors/search-events) instead when the desired output is events related to a company name.
+
+ This endpoint consumes `50` credits per successful search attempt. Check [Credits balance](/api-reference/credits/balance) and confirm with the user before calling it in a credit-safe workflow.
+
+
## Endpoint
`POST /external/exhibitors/search-by-company-name`
@@ -97,6 +101,12 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company
| `buyingSignals` | Full latest-batch buying-signal objects (see [Exhibitors list](/api-reference/exhibitors/list) for the object shape). `[]` when none. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
+## Credit behavior
+
+This endpoint consumes `50` credits per successful search attempt. If the account cannot spend the required credits, the API returns `402 Payment Required`.
+
+Refresh [Credits balance](/api-reference/credits/balance) after successful searches when your UI or audit log needs final credit reconciliation.
+
## Matching behavior
This endpoint is precision-first. It is designed to avoid surprising broad matches for short or ambiguous company names. If no company passes matching rules, the API returns a successful empty paginated response.
@@ -104,6 +114,7 @@ This endpoint is precision-first. It is designed to avoid surprising broad match
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
+- `402 Payment Required`
- `429 Too Many Requests`
## Notes
diff --git a/api-reference/exhibitors/search-events.mdx b/api-reference/exhibitors/search-events.mdx
index ac2f2b3..7bfc7cd 100644
--- a/api-reference/exhibitors/search-events.mdx
+++ b/api-reference/exhibitors/search-events.mdx
@@ -19,7 +19,7 @@ This endpoint returns events, not exhibitor records. It is useful for:
- starting an event workflow without first calling exhibitor search
- This endpoint can consume credits. Check [Credits balance](/api-reference/credits/balance) before running it in bulk.
+ This endpoint can consume credits. Check [Credits balance](/api-reference/credits/balance) before running it in bulk, and use a confirmation step when a user-facing workflow will spend credits.
## Endpoint
@@ -98,15 +98,17 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
| `id`, `eventId` | Event identifiers returned with each event. |
| `name`, `nickname`, `description`, `url` | Event display and source fields. |
| `dateStart`, `dateEnd` | Event date range. |
-| `venue`, `city`, `region`, `country` | Event location fields. |
+| `venue`, `city`, `region`, `country` | Event location fields. Values can be `null` or empty strings when the event source does not provide normalized location metadata. |
| `attendeeCount`, `exhibitorCount`, `personnelCount` | Event scale and coverage fields when available. |
-| `matchedExhibitors` | Matched company records that connect the input to the returned event. |
+| `matchedExhibitors` | Matched company records that connect the input to the returned event. Treat this as the primary evidence that the company-name input matched the returned event. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Credit behavior
This endpoint currently consumes `50` credits per successful search attempt. If the account cannot spend the required credits, the API returns `402 Payment Required`.
+Refresh [Credits balance](/api-reference/credits/balance) after successful searches when your UI or audit log needs final credit reconciliation. Use the returned event count, page count, and `matchedExhibitors` as the main value evidence for the search result.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
diff --git a/api-reference/personnel/events-by-linkedin.mdx b/api-reference/personnel/events-by-linkedin.mdx
index 0631cc8..b27d30d 100644
--- a/api-reference/personnel/events-by-linkedin.mdx
+++ b/api-reference/personnel/events-by-linkedin.mdx
@@ -100,6 +100,17 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
Send the full LinkedIn URL and URL-encode it in the query string. The API performs normalized matching internally, but clients should avoid sending partial names or non-LinkedIn URLs.
+## Latency and fallback
+
+LinkedIn URL matching can take longer than direct Lensmor ID lookups because the API normalizes the URL and resolves the person before listing events.
+
+Recommended production behavior:
+
+- Use a request timeout that keeps the user workflow responsive.
+- If you already have `personnel.id`, prefer [Personnel events](/api-reference/personnel/events) with `personnel_id`.
+- If URL lookup times out or returns no match, keep the workflow usable by continuing with event-scoped personnel search, a selected `personnel_id`, or a retry action.
+- Do not block a whole lead-prioritization workflow on one slow LinkedIn URL lookup. Mark that person as unresolved and continue ranking the rest of the batch.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
diff --git a/api-reference/profile-matching/actions-apply-recommended-events-paged.mdx b/api-reference/profile-matching/actions-apply-recommended-events-paged.mdx
index ad955a4..5ca30ae 100644
--- a/api-reference/profile-matching/actions-apply-recommended-events-paged.mdx
+++ b/api-reference/profile-matching/actions-apply-recommended-events-paged.mdx
@@ -217,6 +217,14 @@ Each event item includes the full event detail shape:
`profile_version`, `active_result_version`, and `is_stale` help clients decide whether to refresh recommendations after profile inputs change. For most UI integrations, show the current results and re-run the endpoint when the user edits the company URL, audience description, filters, or geography.
+## Integration guidance
+
+- This endpoint is synchronous but can take several seconds for broad profiles or large result sets. Show a loading state and keep user input editable.
+- `relevanceReason` can be `null`. Do not require it before displaying ranked recommendations.
+- Use `match_score`, `rank`, matched counts, event dates, geography, categories, and follow-up evidence together. Avoid presenting score differences as exact mathematical certainty.
+- For the top events, follow up with [Event detail](/api-reference/events/detail), then preview [List event exhibitors](/api-reference/exhibitors/list) or [List event personnel](/api-reference/personnel/list) before asking the user to unlock an event.
+- If event summary counts are `null`, use the event-scoped list endpoint's `total` and `semantics` fields for the current access and filter context.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
diff --git a/changelog.mdx b/changelog.mdx
index f32f05a..d5c1600 100644
--- a/changelog.mdx
+++ b/changelog.mdx
@@ -5,6 +5,12 @@ description: "Versioned Lensmor API documentation updates, newly documented endp
Track documentation updates, newly documented API capabilities, and behavior clarifications that may affect integrations.
+## Unreleased
+
+### Changed
+
+- Clarified live API edge cases for best-effort nested personnel samples, event summary counts, LinkedIn URL lookup fallback, shortlist ranking, exhibitor event search billing, and item-level contact unlock outcomes.
+
## v0.21.0
Released June 12, 2026.
diff --git a/concepts/credits-and-access.mdx b/concepts/credits-and-access.mdx
index 195cccd..5d2ade1 100644
--- a/concepts/credits-and-access.mdx
+++ b/concepts/credits-and-access.mdx
@@ -34,11 +34,25 @@ Unlocking an event grants full access to event-scoped exhibitor and personnel re
| --- | --- | --- |
| Event unlock | `2000` credits | Unlocks full event access when contacts are available and the event is not already unlocked. |
| Contact email unlock | `15` credits per chargeable contact | Batch unlock creates an asynchronous task. Already unlocked contacts are not charged again. |
+| Exhibitor company search | `50` credits | `POST /external/exhibitors/search-by-company-name` finds exhibitor company records from a company name. |
| Exhibitor event search | `50` credits | `POST /external/exhibitors/search-events` reverse-lookups events from a company name. |
| LinkedIn activity unlock | No email-unlock charge | `POST /external/personnel/unlock-linkedin-activity` unlocks or starts analysis for LinkedIn activity. It does not unlock contact emails. |
Prices can change by plan or product policy. Treat the API response and your commercial agreement as the source of truth for billing.
+## Final billing reconciliation
+
+Precheck responses, endpoint docs, and UI estimates help users decide whether to proceed, but they are not final billing records. After a paid action, refresh [Credits balance](/api-reference/credits/balance) and the affected resource.
+
+This is especially important when:
+
+- an event or contact was already unlocked
+- an asynchronous contact unlock task completes with mixed item-level outcomes
+- a submitted contact is ineligible, failed, or does not produce an email
+- a workflow is retried after a timeout
+
+For contact unlocks, count delivered enrichment only when an item-level result is unlocked and contains an email. Keep failed or unresolved items visible separately from successful unlocks.
+
## Credit-safe integration pattern
Use a confirmation step for operations that can spend credits:
diff --git a/guides/production-readiness.mdx b/guides/production-readiness.mdx
index 9b03122..fcf9a84 100644
--- a/guides/production-readiness.mdx
+++ b/guides/production-readiness.mdx
@@ -25,6 +25,9 @@ Production integrations should handle authentication, pagination, credit-aware a
Store `task_id` from contact unlock responses so polling can continue after page refreshes, worker restarts, or network failures.
+
+ Treat a terminal task as complete for the job, then inspect each item result before counting delivered emails or failed contacts.
+
Respect `Retry-After` for `429 Too Many Requests` and use `X-RateLimit-*` headers for proactive throttling.
@@ -51,15 +54,17 @@ For credit-consuming actions, use a two-step confirmation pattern:
4. Ask the user to confirm.
5. Execute the API call.
6. Refresh balance and result state.
+7. Reconcile final billing against the post-action balance and task or endpoint response.
-This pattern is especially important for [Unlock event](/api-reference/events/unlock), [Unlock contact emails](/api-reference/contacts/unlock), and [Exhibitor event search](/api-reference/exhibitors/search-events).
+This pattern is especially important for [Unlock event](/api-reference/events/unlock), [Unlock contact emails](/api-reference/contacts/unlock), [Exhibitor company search](/api-reference/exhibitors/search-by-company-name), and [Exhibitor event search](/api-reference/exhibitors/search-events).
## Credit-consuming actions
| Action | Endpoint | Current behavior |
| --- | --- | --- |
| Event unlock | `POST /external/events/:id/unlock` | Charges `2000` credits when the event was not already unlocked. Repeating the same unlock is idempotent and returns `creditsUsed: 0`. |
-| Contact email unlock | `POST /external/contacts/unlock` | Starts an async task and charges `15` credits per chargeable contact. |
+| Contact email unlock | `POST /external/contacts/unlock` | Starts an async task and charges `15` credits per chargeable contact. A completed task can contain mixed item-level outcomes. |
+| Exhibitor company search | `POST /external/exhibitors/search-by-company-name` | Charges `50` credits per successful company search attempt. |
| Exhibitor event search | `POST /external/exhibitors/search-events` | Charges `50` credits per successful search attempt. |
Read-only list, profile, search, and precheck endpoints do not spend credits by themselves.
@@ -74,6 +79,8 @@ Create task -> wait 3s -> poll -> wait 5s -> poll -> wait 10s -> poll
Stop polling when the task is completed or failed. If the user leaves the page, store the task ID and resume later.
+For completed contact unlock tasks, inspect item-level results before updating CRM fields. Count only records that include an unlocked email, surface item-level failures separately, and refresh credit balance after terminal states when users need a spend audit.
+
## Logging and support
Log enough context to investigate issues without storing sensitive data:
diff --git a/guides/quickstart.mdx b/guides/quickstart.mdx
index 7fe102e..de702e9 100644
--- a/guides/quickstart.mdx
+++ b/guides/quickstart.mdx
@@ -39,15 +39,15 @@ The response includes subscription credits, gift credits, the total balance, and
## Choose the right starting point
-| User goal | First endpoint |
-| --- | --- |
-| Search for events by keyword, country, city, or date | `GET /external/events/list` |
-| Rank events from a company profile or audience description | `POST /external/profile-matching/actions/apply-recommended-events/paged` |
-| Browse companies inside a selected event | `GET /external/exhibitors/list` |
-| Browse people inside a selected event | `GET /external/personnel/list` |
-| Find exhibitor records from a company name | `POST /external/exhibitors/search-by-company-name` |
-| Find events associated with a company name | `POST /external/exhibitors/search-events` |
-| Unlock selected contact emails | `POST /external/contacts/unlock` |
+| User goal | First endpoint | Credit note |
+| --- | --- | --- |
+| Search for events by keyword, country, city, or date | `GET /external/events/list` | Read-only |
+| Rank events from a company profile or audience description | `POST /external/profile-matching/actions/apply-recommended-events/paged` | Read-only |
+| Browse companies inside a selected event | `GET /external/exhibitors/list` | Read-only unless the user unlocks event access |
+| Browse people inside a selected event | `GET /external/personnel/list` | Read-only unless the user unlocks event access or emails |
+| Find exhibitor records from a company name | `POST /external/exhibitors/search-by-company-name` | Can consume `50` credits |
+| Find events associated with a company name | `POST /external/exhibitors/search-events` | Can consume `50` credits |
+| Unlock selected contact emails | `POST /external/contacts/unlock` | Can consume `15` credits per chargeable contact |
## 2. Search the event catalog
diff --git a/guides/unlock-contact-emails.mdx b/guides/unlock-contact-emails.mdx
index 622a51f..fa2e22b 100644
--- a/guides/unlock-contact-emails.mdx
+++ b/guides/unlock-contact-emails.mdx
@@ -75,12 +75,14 @@ Poll with backoff rather than a tight loop. A practical pattern is to wait a few
Your integration should account for these outcomes:
- `accepted` or in-progress state: continue polling.
-- completed state: read the unlocked contact data from the task result.
+- completed state: inspect item-level results. Count a contact as delivered only when its item status is unlocked and an email is present.
- failed state: show the task error and decide whether the user should retry.
- `404 Not Found`: the task ID is invalid or not visible to the API key owner.
See [Get contact unlock task](/api-reference/contacts/unlock-task) for the exact response shape.
+Completed tasks can contain mixed item outcomes. Show failed, ineligible, or unresolved contacts separately instead of folding them into the unlocked count.
+
## Credit behavior
Contact email unlock currently costs `15` credits per chargeable contact.
@@ -96,7 +98,8 @@ Contact email unlock currently costs `15` credits per chargeable contact.
- Check `GET /external/credits/balance` before large batches.
- Store the task ID and make polling resumable.
- Back off on `429 Too Many Requests`.
-- Re-fetch personnel or contact records after completion if your UI needs the latest unlock status.
+- Re-fetch `GET /external/credits/balance` after terminal task states when you need final credit reconciliation.
+- Re-fetch personnel or contact records after completion if your UI needs the latest unlock status or email visibility.
## Related endpoints
diff --git a/llms-full.txt b/llms-full.txt
index f1c8f0e..e8921be 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -201,6 +201,12 @@ Source: /changelog
Track documentation updates, newly documented API capabilities, and behavior clarifications that may affect integrations.
+## Unreleased
+
+### Changed
+
+- Clarified live API edge cases for best-effort nested personnel samples, event summary counts, LinkedIn URL lookup fallback, shortlist ranking, exhibitor event search billing, and item-level contact unlock outcomes.
+
## v0.21.0
Released June 12, 2026.
@@ -279,15 +285,15 @@ The response includes subscription credits, gift credits, the total balance, and
## Choose the right starting point
-| User goal | First endpoint |
-| --- | --- |
-| Search for events by keyword, country, city, or date | `GET /external/events/list` |
-| Rank events from a company profile or audience description | `POST /external/profile-matching/actions/apply-recommended-events/paged` |
-| Browse companies inside a selected event | `GET /external/exhibitors/list` |
-| Browse people inside a selected event | `GET /external/personnel/list` |
-| Find exhibitor records from a company name | `POST /external/exhibitors/search-by-company-name` |
-| Find events associated with a company name | `POST /external/exhibitors/search-events` |
-| Unlock selected contact emails | `POST /external/contacts/unlock` |
+| User goal | First endpoint | Credit note |
+| --- | --- | --- |
+| Search for events by keyword, country, city, or date | `GET /external/events/list` | Read-only |
+| Rank events from a company profile or audience description | `POST /external/profile-matching/actions/apply-recommended-events/paged` | Read-only |
+| Browse companies inside a selected event | `GET /external/exhibitors/list` | Read-only unless the user unlocks event access |
+| Browse people inside a selected event | `GET /external/personnel/list` | Read-only unless the user unlocks event access or emails |
+| Find exhibitor records from a company name | `POST /external/exhibitors/search-by-company-name` | Can consume `50` credits |
+| Find events associated with a company name | `POST /external/exhibitors/search-events` | Can consume `50` credits |
+| Unlock selected contact emails | `POST /external/contacts/unlock` | Can consume `15` credits per chargeable contact |
## 2. Search the event catalog
@@ -669,12 +675,14 @@ Poll with backoff rather than a tight loop. A practical pattern is to wait a few
Your integration should account for these outcomes:
- `accepted` or in-progress state: continue polling.
-- completed state: read the unlocked contact data from the task result.
+- completed state: inspect item-level results. Count a contact as delivered only when its item status is unlocked and an email is present.
- failed state: show the task error and decide whether the user should retry.
- `404 Not Found`: the task ID is invalid or not visible to the API key owner.
See [Get contact unlock task](/api-reference/contacts/unlock-task) for the exact response shape.
+Completed tasks can contain mixed item outcomes. Show failed, ineligible, or unresolved contacts separately instead of folding them into the unlocked count.
+
## Credit behavior
Contact email unlock currently costs `15` credits per chargeable contact.
@@ -690,7 +698,8 @@ Contact email unlock currently costs `15` credits per chargeable contact.
- Check `GET /external/credits/balance` before large batches.
- Store the task ID and make polling resumable.
- Back off on `429 Too Many Requests`.
-- Re-fetch personnel or contact records after completion if your UI needs the latest unlock status.
+- Re-fetch `GET /external/credits/balance` after terminal task states when you need final credit reconciliation.
+- Re-fetch personnel or contact records after completion if your UI needs the latest unlock status or email visibility.
## Related endpoints
@@ -737,6 +746,9 @@ Production integrations should handle authentication, pagination, credit-aware a
Store `task_id` from contact unlock responses so polling can continue after page refreshes, worker restarts, or network failures.
+
+ Treat a terminal task as complete for the job, then inspect each item result before counting delivered emails or failed contacts.
+
Respect `Retry-After` for `429 Too Many Requests` and use `X-RateLimit-*` headers for proactive throttling.
@@ -763,15 +775,17 @@ For credit-consuming actions, use a two-step confirmation pattern:
4. Ask the user to confirm.
5. Execute the API call.
6. Refresh balance and result state.
+7. Reconcile final billing against the post-action balance and task or endpoint response.
-This pattern is especially important for [Unlock event](/api-reference/events/unlock), [Unlock contact emails](/api-reference/contacts/unlock), and [Exhibitor event search](/api-reference/exhibitors/search-events).
+This pattern is especially important for [Unlock event](/api-reference/events/unlock), [Unlock contact emails](/api-reference/contacts/unlock), [Exhibitor company search](/api-reference/exhibitors/search-by-company-name), and [Exhibitor event search](/api-reference/exhibitors/search-events).
## Credit-consuming actions
| Action | Endpoint | Current behavior |
| --- | --- | --- |
| Event unlock | `POST /external/events/:id/unlock` | Charges `2000` credits when the event was not already unlocked. Repeating the same unlock is idempotent and returns `creditsUsed: 0`. |
-| Contact email unlock | `POST /external/contacts/unlock` | Starts an async task and charges `15` credits per chargeable contact. |
+| Contact email unlock | `POST /external/contacts/unlock` | Starts an async task and charges `15` credits per chargeable contact. A completed task can contain mixed item-level outcomes. |
+| Exhibitor company search | `POST /external/exhibitors/search-by-company-name` | Charges `50` credits per successful company search attempt. |
| Exhibitor event search | `POST /external/exhibitors/search-events` | Charges `50` credits per successful search attempt. |
Read-only list, profile, search, and precheck endpoints do not spend credits by themselves.
@@ -786,6 +800,8 @@ Create task -> wait 3s -> poll -> wait 5s -> poll -> wait 10s -> poll
Stop polling when the task is completed or failed. If the user leaves the page, store the task ID and resume later.
+For completed contact unlock tasks, inspect item-level results before updating CRM fields. Count only records that include an unlocked email, surface item-level failures separately, and refresh credit balance after terminal states when users need a spend audit.
+
## Logging and support
Log enough context to investigate issues without storing sensitive data:
@@ -1203,12 +1219,22 @@ curl "https://platform.lensmor.com/external/events/139574" \
| `url` | Source or official event URL when available. |
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country`, `latitude`, `longitude` | Location metadata. Coordinates are string values (e.g. `"36.1313238"`) or `null`. |
-| `attendeeCount`, `exhibitorCount`, `personnelCount` | Known scale and coverage fields when available. |
+| `attendeeCount`, `exhibitorCount`, `personnelCount` | Known summary scale and coverage fields when available. These fields can be `null` or differ from current event-scoped list totals. |
| `priceLower`, `priceUpper` | Price range as string values (e.g. `"1295"`) or `null`. |
| `eventType`, `eventTypes`, `categories`, `topics`, `topicsCount` | Classification metadata. `categories` is an array of objects with `id`, `code`, `name`, `description`, `confidence` fields. |
| `verified`, `future`, `historic`, `historicEvent` | Status flags and historical relationship metadata. |
| `image`, `dataSource` | Media and source metadata. |
+## Count fields
+
+Treat `exhibitorCount` and `personnelCount` on event detail as summary metadata for display and planning. They are not the authoritative count for the current caller's access state, filters, or page visibility.
+
+When you need current coverage numbers, call the event-scoped list endpoint and use its pagination metadata:
+
+- [List event exhibitors](/api-reference/exhibitors/list) for exhibitor totals.
+- [List event personnel](/api-reference/personnel/list) for personnel totals.
+- `semantics.counts` when present for visible, locked, and matched access context.
+
## Error responses
- `401 Unauthorized`
- `404 Not Found`
@@ -1492,7 +1518,9 @@ curl -X POST "https://platform.lensmor.com/external/events/rank" \
- Preserve the original submitted event IDs in your client if you need to reconcile ranked results with local records.
- Display `rank` and `match_score` together when showing ordered recommendations.
-- If `reasons` is empty, use event metadata and match score rather than showing a blank explanation block.
+- Treat the endpoint as shortlist ordering, not a complete recommendation report by itself.
+- Live responses can return low or zero `match_score` values and empty `reasons`. Combine returned order with event metadata, matched counts, and follow-up detail or list calls before making a final business recommendation.
+- If `reasons` is empty, use event metadata and evidence from follow-up calls rather than showing a blank explanation block.
## Error responses
- `400 Bad Request`
@@ -1630,7 +1658,7 @@ Common use cases:
- build an event exhibitor directory
- filter exhibitors by industry, category, geography, or company keyword
-- request a small nested personnel sample with `personnelLimit`
+- request a best-effort nested personnel sample with `personnelLimit`
- decide whether an event is worth unlocking for full coverage
## Endpoint
@@ -1655,7 +1683,7 @@ See [Authentication](/authentication)
| `jobTitle` | No | string[] | Personnel job-title filter. Repeated query parameters are supported. |
| `managementLevel` | No | string[] | Personnel management-level filter, such as `vp` or `c_suite`. |
| `department` | No | string[] | Personnel department filter, such as `marketing` or `sales`. |
-| `personnelLimit` | No | integer | Maximum nested personnel rows per exhibitor. Use `0` or omit for none. |
+| `personnelLimit` | No | integer | Maximum nested personnel rows per exhibitor when available. Use `0` or omit for none. Treat nested personnel as best-effort; use `GET /external/personnel/list` with `event_id` and `exhibitor_id` when contacts are required. |
## Request example
```bash
@@ -1768,6 +1796,12 @@ curl "https://platform.lensmor.com/external/exhibitors/list?event_id=139574&indu
| `recommendationProcessingFeature` | Which recommendation feature is processing. `"none"` when idle. |
| `semantics` | Preview/full access metadata for the selected event. |
+## Personnel samples
+
+`personnelLimit` asks the API to include a small personnel sample for each exhibitor when the service has a suitable sample for the current event, filters, and access state. It is not a guarantee that every exhibitor item will contain nested personnel rows.
+
+When your workflow needs buyer contacts for a specific company, call [Personnel list](/api-reference/personnel/list) with both `event_id` and `exhibitor_id`. Treat that event-scoped personnel response as the source of truth for people coverage and contact unlock state.
+
## Access semantics
When an event is locked, this endpoint can return a preview slice instead of full results. Use `semantics` to explain the state to users:
@@ -1789,6 +1823,7 @@ When an event is locked, this endpoint can return a preview slice instead of ful
- `matched_event_ids` reflects the requested event scope.
- `techStacks` is always returned as an array; when no data is available, the API returns `techStacks: []`.
- Locked events can return preview results. Use `semantics.unlock` to decide whether to call [Unlock event](/api-reference/events/unlock).
+- After event unlock, request the target page again and confirm `semantics.accessMode` moved to `full` before assuming all matching exhibitors are available.
---
@@ -1970,6 +2005,10 @@ Choose this endpoint when:
Use [Exhibitor event search](/api-reference/exhibitors/search-events) instead when the desired output is events related to a company name.
+
+ This endpoint consumes `50` credits per successful search attempt. Check [Credits balance](/api-reference/credits/balance) and confirm with the user before calling it in a credit-safe workflow.
+
+
## Endpoint
`POST /external/exhibitors/search-by-company-name`
@@ -2048,6 +2087,12 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company
| `buyingSignals` | Full latest-batch buying-signal objects (see [Exhibitors list](/api-reference/exhibitors/list) for the object shape). `[]` when none. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
+## Credit behavior
+
+This endpoint consumes `50` credits per successful search attempt. If the account cannot spend the required credits, the API returns `402 Payment Required`.
+
+Refresh [Credits balance](/api-reference/credits/balance) after successful searches when your UI or audit log needs final credit reconciliation.
+
## Matching behavior
This endpoint is precision-first. It is designed to avoid surprising broad matches for short or ambiguous company names. If no company passes matching rules, the API returns a successful empty paginated response.
@@ -2055,6 +2100,7 @@ This endpoint is precision-first. It is designed to avoid surprising broad match
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
+- `402 Payment Required`
- `429 Too Many Requests`
## Notes
@@ -2084,7 +2130,7 @@ This endpoint returns events, not exhibitor records. It is useful for:
- starting an event workflow without first calling exhibitor search
- This endpoint can consume credits. Check [Credits balance](/api-reference/credits/balance) before running it in bulk.
+ This endpoint can consume credits. Check [Credits balance](/api-reference/credits/balance) before running it in bulk, and use a confirmation step when a user-facing workflow will spend credits.
## Endpoint
@@ -2163,15 +2209,17 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
| `id`, `eventId` | Event identifiers returned with each event. |
| `name`, `nickname`, `description`, `url` | Event display and source fields. |
| `dateStart`, `dateEnd` | Event date range. |
-| `venue`, `city`, `region`, `country` | Event location fields. |
+| `venue`, `city`, `region`, `country` | Event location fields. Values can be `null` or empty strings when the event source does not provide normalized location metadata. |
| `attendeeCount`, `exhibitorCount`, `personnelCount` | Event scale and coverage fields when available. |
-| `matchedExhibitors` | Matched company records that connect the input to the returned event. |
+| `matchedExhibitors` | Matched company records that connect the input to the returned event. Treat this as the primary evidence that the company-name input matched the returned event. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Credit behavior
This endpoint currently consumes `50` credits per successful search attempt. If the account cannot spend the required credits, the API returns `402 Payment Required`.
+Refresh [Credits balance](/api-reference/credits/balance) after successful searches when your UI or audit log needs final credit reconciliation. Use the returned event count, page count, and `matchedExhibitors` as the main value evidence for the search result.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
@@ -2783,6 +2831,17 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
Send the full LinkedIn URL and URL-encode it in the query string. The API performs normalized matching internally, but clients should avoid sending partial names or non-LinkedIn URLs.
+## Latency and fallback
+
+LinkedIn URL matching can take longer than direct Lensmor ID lookups because the API normalizes the URL and resolves the person before listing events.
+
+Recommended production behavior:
+
+- Use a request timeout that keeps the user workflow responsive.
+- If you already have `personnel.id`, prefer [Personnel events](/api-reference/personnel/events) with `personnel_id`.
+- If URL lookup times out or returns no match, keep the workflow usable by continuing with event-scoped personnel search, a selected `personnel_id`, or a retry action.
+- Do not block a whole lead-prioritization workflow on one slow LinkedIn URL lookup. Mark that person as unresolved and continue ranking the rest of the batch.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
@@ -3006,6 +3065,10 @@ This endpoint is intentionally asynchronous because email unlock can involve mul
For best user experience, show the number of locked contacts and available credit balance before creating the unlock task.
+
+ A `201 Created` response means the unlock task was accepted. It does not guarantee that every submitted personnel record will return an email. Always inspect item-level task results before counting a contact as enriched.
+
+
## Endpoint
`POST /external/contacts/unlock`
@@ -3067,12 +3130,14 @@ Use backoff instead of a tight polling loop. A practical pattern is:
2. Poll every few seconds for short jobs.
3. Increase the interval if the job remains in progress.
4. Stop polling when the task completes or fails.
+5. Re-fetch personnel or contact records if your UI needs the latest `email` and `contactUnlockStatus` values.
## Credit behavior
Contact email unlock currently costs `15` credits per chargeable contact.
- Already unlocked contacts are not charged again.
+- Failed or ineligible items may not produce an email. Treat the task result and refreshed balance as the source of truth for actual delivery and billing.
- The API rejects batches larger than `100` personnel IDs.
- Insufficient balance returns `402 Payment Required`.
- Retry behavior should be tied to the task state, not only the create response.
@@ -3087,6 +3152,7 @@ Contact email unlock currently costs `15` credits per chargeable contact.
## Notes
- This endpoint creates an asynchronous unlock job. Poll [Get contact unlock task](/api-reference/contacts/unlock-task) with `task_id`.
+- Do not create duplicate unlock tasks while a prior task for the same selected contacts is still pending or processing.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
---
@@ -3171,6 +3237,15 @@ Item status:
- `unlocked`
- `failed`
+## Result interpretation
+
+Treat each `items[]` entry independently:
+
+- Count a contact as delivered only when item `status` is `unlocked` and `email` is present.
+- Surface `failed` items separately with `errorCode` when provided.
+- A task can reach a terminal state while individual items have different outcomes.
+- Refresh credit balance and contact/personnel records after terminal states when your UI needs billing or access-state reconciliation.
+
## Polling guidance
Use a backoff schedule instead of polling aggressively:
@@ -3410,6 +3485,14 @@ Each event item includes the full event detail shape:
`profile_version`, `active_result_version`, and `is_stale` help clients decide whether to refresh recommendations after profile inputs change. For most UI integrations, show the current results and re-run the endpoint when the user edits the company URL, audience description, filters, or geography.
+## Integration guidance
+
+- This endpoint is synchronous but can take several seconds for broad profiles or large result sets. Show a loading state and keep user input editable.
+- `relevanceReason` can be `null`. Do not require it before displaying ranked recommendations.
+- Use `match_score`, `rank`, matched counts, event dates, geography, categories, and follow-up evidence together. Avoid presenting score differences as exact mathematical certainty.
+- For the top events, follow up with [Event detail](/api-reference/events/detail), then preview [List event exhibitors](/api-reference/exhibitors/list) or [List event personnel](/api-reference/personnel/list) before asking the user to unlock an event.
+- If event summary counts are `null`, use the event-scoped list endpoint's `total` and `semantics` fields for the current access and filter context.
+
## Error responses
- `400 Bad Request`
- `401 Unauthorized`
@@ -3774,11 +3857,25 @@ Unlocking an event grants full access to event-scoped exhibitor and personnel re
| --- | --- | --- |
| Event unlock | `2000` credits | Unlocks full event access when contacts are available and the event is not already unlocked. |
| Contact email unlock | `15` credits per chargeable contact | Batch unlock creates an asynchronous task. Already unlocked contacts are not charged again. |
+| Exhibitor company search | `50` credits | `POST /external/exhibitors/search-by-company-name` finds exhibitor company records from a company name. |
| Exhibitor event search | `50` credits | `POST /external/exhibitors/search-events` reverse-lookups events from a company name. |
| LinkedIn activity unlock | No email-unlock charge | `POST /external/personnel/unlock-linkedin-activity` unlocks or starts analysis for LinkedIn activity. It does not unlock contact emails. |
Prices can change by plan or product policy. Treat the API response and your commercial agreement as the source of truth for billing.
+## Final billing reconciliation
+
+Precheck responses, endpoint docs, and UI estimates help users decide whether to proceed, but they are not final billing records. After a paid action, refresh [Credits balance](/api-reference/credits/balance) and the affected resource.
+
+This is especially important when:
+
+- an event or contact was already unlocked
+- an asynchronous contact unlock task completes with mixed item-level outcomes
+- a submitted contact is ineligible, failed, or does not produce an email
+- a workflow is retried after a timeout
+
+For contact unlocks, count delivered enrichment only when an item-level result is unlocked and contains an email. Keep failed or unresolved items visible separately from successful unlocks.
+
## Credit-safe integration pattern
Use a confirmation step for operations that can spend credits: