diff --git a/README.md b/README.md
index 68682db..eb6131c 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ pnpm dlx mintlify dev
## Changelog
-See `changelog.mdx` for versioned documentation updates. The current documentation version is `v0.23.1`.
+See `changelog.mdx` for versioned documentation updates. The current documentation version is `v0.24.0`.
---
diff --git a/api-reference-backup/actions/precheck.mdx b/api-reference-backup/actions/precheck.mdx
index b3d95e9..52a6890 100644
--- a/api-reference-backup/actions/precheck.mdx
+++ b/api-reference-backup/actions/precheck.mdx
@@ -20,7 +20,7 @@ See [Authentication](/authentication)
## Request body
| Name | Required | Type | Notes |
| --- | --- | --- | --- |
-| `action_type` | Yes | string | Supported values include `query_event_personnel`, `unlock_event_contacts`, `unlock_contact_emails`, `search_exhibitor_events`, and `others`. |
+| `action_type` | Yes | string | Supported customer actions include `query_event_personnel`, `unlock_event_contacts`, `unlock_event_visitors`, `unlock_event_full_access`, `unlock_contact_emails`, `unlock_contact_phones`, `search_exhibitor_events`, and `others`. |
| `params` | Yes | object | Action-specific inputs, such as `{ "event_id": "26855" }`. |
| `call_source` | No | string | Source label such as `api` or `agent`. |
| `locale` | No | string | Optional locale, such as `en-US`. |
@@ -74,8 +74,33 @@ curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
| `biz_code` | Billing/business code used by the execution path when applicable. |
| `detail` | Action-specific metadata. Shape varies by `action_type`. |
+## Visitor unlock example
+
+```json
+{
+ "ok": true,
+ "action_type": "unlock_event_visitors",
+ "allowed": true,
+ "should_charge": true,
+ "credits": 3000,
+ "reason_code": "ok",
+ "biz_code": "Visitor Unlock - API",
+ "detail": {
+ "event_id": "26855",
+ "event_unlocked": true,
+ "visitor_unlocked": false,
+ "visitor_data_available": true,
+ "visitor_access_credits": 3000,
+ "available_balance": 8000
+ }
+}
+```
+
## Notes
- Precheck is read-only. It does not unlock records, create tasks, or consume credits.
- `query_event_personnel` is useful before calling [Personnel list](/api-reference/personnel/list-event-personnel).
+- `unlock_event_visitors` requires base event access first. It can report `3000` expected credits, `state_conflict` when the event is still locked, `no_contacts_available` when visitor data is unavailable, or `forbidden` when the account has no active subscription.
+- `unlock_event_full_access` calculates only the missing access layers: `2000` for base event access plus `3000` for visitor access. It can therefore report `0`, `2000`, `3000`, or `5000` credits.
+- For full-access precheck, `detail` includes `event_unlocked`, `visitor_unlocked`, `visitor_data_available`, `event_access_credits`, `visitor_access_credits`, and `full_access_credits`.
- Once a list endpoint returns `semantics`, treat that response as the authoritative explanation for the actual list result.
- Precheck decisions can change as credits, event access, or underlying data changes. Recheck before executing a paid action.
diff --git a/api-reference-backup/contacts/search.mdx b/api-reference-backup/contacts/search.mdx
index b3c5ccc..b23156e 100644
--- a/api-reference-backup/contacts/search.mdx
+++ b/api-reference-backup/contacts/search.mdx
@@ -62,11 +62,14 @@ curl "https://platform.lensmor.com/external/contacts/search?company_name=Acme&pe
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane",
"companyName": "Acme",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": null,
- "linkedinActivityStatus": null
+ "linkedinActivityStatus": null,
+ "eventCount": 3
}
],
"total": 1,
@@ -87,9 +90,12 @@ curl "https://platform.lensmor.com/external/contacts/search?company_name=Acme&pe
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | LinkedIn profile URL when available. |
| `companyName` | Matched company name. |
-| `sourceType` | Source context for the contact record. |
+| `sourceType` | Normalized source labels for the contact record. Values can include `exhibitor`, `social`, and `visitors`. |
| `email` | Email if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Contact access state for this API key owner. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `eventCount` | Number of associated events when available. |
| `linkedinActivity` | Always `null` on this endpoint. Only [Personnel list](/api-reference/personnel/list-event-personnel) populates LinkedIn activity data. |
| `linkedinActivityStatus` | Always `null` on this endpoint. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
diff --git a/api-reference-backup/contacts/unlock-phone.mdx b/api-reference-backup/contacts/unlock-phone.mdx
index 0a955bd..e3d1952 100644
--- a/api-reference-backup/contacts/unlock-phone.mdx
+++ b/api-reference-backup/contacts/unlock-phone.mdx
@@ -1,10 +1,10 @@
---
title: "Unlock contact phone numbers"
-description: "Start an asynchronous phone number unlock workflow for one or more personnel records in a Lensmor event."
+description: "Submit a phone unlock request and receive either an accepted asynchronous task or an immediate no-work success result."
openapi: "openapi.json POST /external/contacts/unlock-phone"
---
-Start an asynchronous job to unlock phone numbers for one or more personnel records.
+Submit a request to unlock phone numbers for one or more personnel records. Chargeable work creates an asynchronous task; a request with no chargeable work completes immediately without a task.
Use this endpoint after finding contacts from personnel or contact search results when you need direct phone numbers rather than email addresses.
@@ -12,25 +12,32 @@ Use this endpoint after finding contacts from personnel or contact search result
Use `POST /external/contacts/unlock-phone` when your integration needs verified phone numbers for outbound calling or SMS workflows.
-This endpoint is asynchronous. The create call returns a task identifier; poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) until the job reaches a terminal state.
+When the response has `status: "accepted"`, the create call returns a task identifier. Poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) until that job reaches a terminal state. When the response has `status: "success"`, no task was created and there is nothing to poll.
Phone unlock is a separate workflow from email unlock. Use [Unlock contact emails](/api-reference/contacts/unlock-contact-emails) for email addresses.
- A `201 Created` response means the task was accepted. Always inspect item-level results before counting a contact phone as delivered.
+ A `201 Created` response means the request was processed. `status: "accepted"` means a task was created; `status: "success"` means there was no chargeable work to enqueue. Neither state guarantees that every submitted personnel record will return a phone number.
## Credit behavior
-Phone unlock costs credits per chargeable contact.
+Phone unlock costs `150` credits per chargeable contact.
- Already unlocked contacts are not charged again.
- Insufficient balance returns `402 Payment Required`.
- The API rejects batches larger than 100 personnel IDs.
+If there is no chargeable work to enqueue, the endpoint can return `status: "success"` without `task_id` or `job_id`. The response can also include `skipped_personnel_ids` and `skipped_detail` so the caller can explain invalid, missing, unlinked, already unlocked, or currently unlocking records.
+
+## Response handling
+
+- For `status: "accepted"`, use `task_id` (or its `job_id` alias) to poll the phone unlock task.
+- For `status: "success"`, do not poll. Inspect `skipped_personnel_ids` and `skipped_detail` to explain why no task was needed.
+
## Notes
-- This endpoint creates an asynchronous unlock job. Poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) with `task_id`.
+- Only poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) when `status` is `accepted` and `task_id` is present.
- Do not create duplicate unlock tasks while a prior task is still pending or processing.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
diff --git a/api-reference-backup/contacts/unlock.mdx b/api-reference-backup/contacts/unlock.mdx
index 74dbbe9..f915cf6 100644
--- a/api-reference-backup/contacts/unlock.mdx
+++ b/api-reference-backup/contacts/unlock.mdx
@@ -19,7 +19,7 @@ This endpoint is intentionally asynchronous because email unlock can involve mul
- 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.
+ A `201 Created` response means the request was processed. `status: "accepted"` means a task was created; `status: "success"` means there was no chargeable work to enqueue. Neither state guarantees that every submitted personnel record will return an email.
## Endpoint
@@ -64,9 +64,11 @@ curl -X POST "https://platform.lensmor.com/external/contacts/unlock" \
| Field | Description |
| --- | --- |
-| `status` | Initial task acceptance state. Usually `accepted` when the task was created. |
-| `task_id` | Identifier used to poll task status. Store this value. |
-| `job_id` | Alias for the background job identifier. It currently matches `task_id`. |
+| `status` | `accepted` when an asynchronous task was created, or `success` when there was no chargeable work to enqueue. |
+| `task_id` | Identifier used to poll task status. Present only when `status` is `accepted`. |
+| `job_id` | Alias for `task_id`. Present only when `status` is `accepted`. |
+| `skipped_personnel_ids` | Personnel IDs omitted from the unlock task, when applicable. |
+| `skipped_detail` | Optional grouped reasons for skipped IDs, including invalid, missing, unlinked, already unlocked, or currently unlocking records. |
## Polling pattern
diff --git a/api-reference-backup/credits/balance.mdx b/api-reference-backup/credits/balance.mdx
index fb2d335..ce50f23 100644
--- a/api-reference-backup/credits/balance.mdx
+++ b/api-reference-backup/credits/balance.mdx
@@ -6,13 +6,15 @@ openapi: "openapi.json GET /external/credits/balance"
Get the current credit balance for the API key owner.
-Use this endpoint before credit-consuming workflows such as event unlock, contact email unlock, or exhibitor event search.
+Use this endpoint before credit-consuming workflows such as event unlock, visitor unlock, contact email unlock, or exhibitor event search.
## When to use this endpoint
Call `GET /external/credits/balance` before any workflow that can spend credits:
- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access)
+- [Unlock full event access](/api-reference/events/unlock-full-event-access)
- [Unlock contact emails](/api-reference/contacts/unlock-contact-emails)
- [Search exhibitor events](/api-reference/exhibitors/search-events-by-exhibitor-company-name)
@@ -36,10 +38,13 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Response example
```json
{
- "totalAmount": 35050,
- "totalBalance": 30050,
+ "totalAmount": 45050,
+ "totalBalance": 38050,
"subscriptionAmount": 35000,
"subscriptionBalance": 30000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
"giftAmount": 50,
"giftBalance": 50,
"resetAt": 1714521600000
@@ -50,10 +55,13 @@ curl "https://platform.lensmor.com/external/credits/balance" \
| Field | Description |
| --- | --- |
-| `totalAmount` | Total credits ever granted across active subscription and gift buckets. |
+| `totalAmount` | Total credits granted across active subscription, add-on, and gift buckets. |
| `totalBalance` | Total currently available credits. Use this for simple "can this user spend credits?" checks. |
| `subscriptionAmount` | Subscription credits granted for the active credit period. |
-| `subscriptionBalance` | Remaining subscription credits. These are consumed before gift credits. |
+| `subscriptionBalance` | Remaining subscription credits. |
+| `addonAmount` | Add-on credits granted in currently active add-on buckets. |
+| `addonBalance` | Remaining add-on credits. |
+| `addonExpireAt` | Latest active add-on credit expiration timestamp in milliseconds, or `null`. |
| `giftAmount` | Gift credits granted to the user. |
| `giftBalance` | Remaining gift credits. |
| `resetAt` | Earliest active subscription credit expiration timestamp in milliseconds, or `null`. |
@@ -61,9 +69,9 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Integration guidance
- Use `totalBalance` for high-level balance checks.
-- Use bucket-specific fields when your UI needs to explain subscription vs. gift credit usage.
+- Use bucket-specific fields when your UI needs to explain subscription, add-on, and gift credit usage.
- Refresh the balance after credit-consuming operations because `balanceAfter` may differ from a previously cached value.
-- Treat `resetAt` as nullable. Some accounts may only have gift credits or no active subscription credit expiration.
+- Treat `resetAt` and `addonExpireAt` as nullable. Some accounts may not have an active expiring bucket of that type.
## Error responses
- `401 Unauthorized`
@@ -71,5 +79,5 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Notes
- `resetAt` is a timestamp in milliseconds for the earliest active subscription credit expiration, or `null`.
-- Subscription credits are consumed before gift credits.
+- `addonExpireAt` is a timestamp in milliseconds for the latest active add-on credit expiration, or `null`.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
diff --git a/api-reference-backup/events/brief.mdx b/api-reference-backup/events/brief.mdx
index 5a5d148..92dc453 100644
--- a/api-reference-backup/events/brief.mdx
+++ b/api-reference-backup/events/brief.mdx
@@ -51,8 +51,9 @@ curl "https://platform.lensmor.com/external/events/brief?event_id=139574" \
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
},
"summary": {
"attendeeCount": 40000,
diff --git a/api-reference-backup/events/detail.mdx b/api-reference-backup/events/detail.mdx
index c485d8c..3e5ff0f 100644
--- a/api-reference-backup/events/detail.mdx
+++ b/api-reference-backup/events/detail.mdx
@@ -61,6 +61,7 @@ curl "https://platform.lensmor.com/external/events/139574" \
"priceLower": "0",
"priceUpper": "0",
"eventType": "In-person",
+ "sponsorMatchStarred": 0,
"categories": [
{
"id": 11,
@@ -107,7 +108,9 @@ curl "https://platform.lensmor.com/external/events/139574" \
| `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. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
## Count fields
@@ -128,6 +131,6 @@ When you need current coverage numbers, call the event-scoped list endpoint and
- For event identifiers, see [Identifiers](/concepts/identifiers). In the current API version, `id` and `eventId` return the same value.
- Some fields can be `null` when the underlying source does not provide that data.
- String-typed numeric fields (`latitude`, `longitude`, `priceLower`, `priceUpper`) require client-side parsing when used for calculations.
-- `dataSource` indicates the data origin (e.g. `"Lensmor"`, `"local_import"`). Do not hard-code checks against specific values.
+- `dataSource` is always `"Lensmor"` for public event records.
- `categories` is an array of objects. Each object contains `id` (integer), `code` (string), `name` (string), `description` (string or null), and `confidence` (string).
- This endpoint does not unlock event-scoped exhibitor or personnel access by itself.
diff --git a/api-reference-backup/events/fit-score.mdx b/api-reference-backup/events/fit-score.mdx
index 3af66a1..a9c62f6 100644
--- a/api-reference-backup/events/fit-score.mdx
+++ b/api-reference-backup/events/fit-score.mdx
@@ -53,8 +53,9 @@ curl -X POST "https://platform.lensmor.com/external/events/fit-score" \
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
},
"score": 8.1,
"recommendation": "recommended",
diff --git a/api-reference-backup/events/list.mdx b/api-reference-backup/events/list.mdx
index f46b4ca..d5d12c7 100644
--- a/api-reference-backup/events/list.mdx
+++ b/api-reference-backup/events/list.mdx
@@ -81,6 +81,7 @@ curl "https://platform.lensmor.com/external/events/list?event_type=conference_le
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"image": "https://example.com/ces-image.png",
"dataSource": "Lensmor"
}
@@ -107,7 +108,8 @@ curl "https://platform.lensmor.com/external/events/list?event_type=conference_le
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `dataSource` | Source category for the returned record (e.g. `"Lensmor"`, `"local_import"`). Do not hard-code checks against specific values. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
diff --git a/api-reference-backup/events/unlock-event-visitor-access.mdx b/api-reference-backup/events/unlock-event-visitor-access.mdx
new file mode 100644
index 0000000..95614fa
--- /dev/null
+++ b/api-reference-backup/events/unlock-event-visitor-access.mdx
@@ -0,0 +1,129 @@
+---
+title: "Unlock event visitor access"
+description: "Unlock visitor records for an event after base event access is available, with precheck, subscription, credit, and idempotency guidance."
+openapi: "openapi.json POST /external/events/{id}/visitors/unlock"
+---
+
+Unlock the visitor data layer for an event that already has base event access.
+
+Use this endpoint when the caller needs visitor records in addition to the exhibitor and personnel coverage unlocked by [Unlock event](/api-reference/events/unlock-event-access).
+
+## Prerequisites
+
+Before executing this paid action:
+
+1. Confirm base event access is already unlocked.
+2. Call [Actions precheck](/api-reference/actions/precheck-an-external-action) with `action_type: "unlock_event_visitors"` and the selected `event_id`.
+3. Continue only when precheck returns `allowed: true`.
+
+Visitor access requires an active subscription, available visitor data for the event, and enough credits.
+
+
+ The first successful visitor unlock consumes `3000` credits. Precheck is read-only and should be used before the paid call.
+
+
+## Endpoint
+
+`POST /external/events/:id/visitors/unlock`
+
+## Authentication
+
+See [Authentication](/authentication).
+
+## Success status code
+
+`201 Created`
+
+## Path parameters
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `id` | Yes | string | Supports the event `id` or `eventId` returned by event responses. |
+
+## Headers
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
+## Precheck example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_visitors",
+ "call_source": "api",
+ "params": {
+ "event_id": "26855"
+ }
+ }'
+```
+
+## Request example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/events/26855/visitors/unlock" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "x-call-source: api"
+```
+
+## Response example
+
+```json
+{
+ "success": true,
+ "alreadyUnlocked": false,
+ "creditsSpent": 3000,
+ "balanceAfter": {
+ "totalAmount": 45050,
+ "totalBalance": 35050,
+ "subscriptionAmount": 35000,
+ "subscriptionBalance": 27000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
+ "giftAmount": 50,
+ "giftBalance": 50,
+ "resetAt": 1785542400000
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+}
+```
+
+## Response fields
+
+| Field | Description |
+| --- | --- |
+| `success` | Whether the request completed successfully. |
+| `alreadyUnlocked` | `true` when visitor access was already available before this request. |
+| `creditsSpent` | Credits charged by this request. It is `3000` for a first successful unlock and `0` for an idempotent repeat. |
+| `balanceAfter` | Full credit-balance snapshot after a paid unlock, or `null` when no credits were charged. |
+| `event` | Minimal event identity for confirmation and audit logs. |
+
+## Idempotency
+
+Repeating the unlock for the same account and event does not charge again. An idempotent repeat returns `alreadyUnlocked: true`, `creditsSpent: 0`, and `balanceAfter: null`.
+
+## Error responses
+
+- `400 Bad Request` — no active subscription, base event access is still locked, or no visitor data is available.
+- `401 Unauthorized`
+- `402 Payment Required`
+- `404 Not Found`
+- `409 Conflict` — another credit operation is in progress.
+- `429 Too Many Requests`
+
+The corresponding `errorKey` can include `NO_ACTIVE_SUBSCRIPTION`, `EVENT_NOT_UNLOCKED`, `VISITOR_DATA_NOT_AVAILABLE`, or `CREDIT_OPERATION_IN_PROGRESS`.
+
+## Related endpoints
+
+- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock full event access](/api-reference/events/unlock-full-event-access)
+- [Credits balance](/api-reference/credits/get-credits-balance)
+- [Actions precheck](/api-reference/actions/precheck-an-external-action)
diff --git a/api-reference-backup/events/unlock-full-event-access.mdx b/api-reference-backup/events/unlock-full-event-access.mdx
new file mode 100644
index 0000000..8d7bfc0
--- /dev/null
+++ b/api-reference-backup/events/unlock-full-event-access.mdx
@@ -0,0 +1,129 @@
+---
+title: "Unlock full event access"
+description: "Atomically unlock base event and visitor access, charging only for missing access layers and returning the exact access changes."
+openapi: "openapi.json POST /external/events/{id}/full-access/unlock"
+---
+
+Atomically unlock base event access and visitor access for one event.
+
+This endpoint is useful when the user wants all available event data in one confirmed action. The API charges only for access layers that are not already unlocked.
+
+## Credit matrix
+
+| State before the call | Result | Credits |
+| --- | --- | --- |
+| Event locked, visitor locked, visitor data available | Unlock both layers | `5000` |
+| Event unlocked, visitor locked, visitor data available | Unlock visitor layer | `3000` |
+| Event locked, visitor already unlocked | Unlock base event layer | `2000` |
+| Both layers already unlocked | No new unlock | `0` |
+| Event locked, visitor data unavailable | Direct execution can unlock the event and set `visitorSkipped: true` | `2000` |
+
+Base event access costs `2000` credits. Visitor access costs `3000` credits and requires an active subscription when visitor data is available.
+
+
+ Call [Actions precheck](/api-reference/actions/precheck-an-external-action) with `action_type: "unlock_event_full_access"` before execution. Current precheck returns `no_contacts_available` when visitor data is unavailable; use [Unlock event](/api-reference/events/unlock-event-access) when only base event coverage is needed.
+
+
+## Endpoint
+
+`POST /external/events/:id/full-access/unlock`
+
+## Authentication
+
+See [Authentication](/authentication).
+
+## Success status code
+
+`201 Created`
+
+## Path parameters
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `id` | Yes | string | Supports the event `id` or `eventId` returned by event responses. |
+
+## Headers
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
+## Precheck example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_full_access",
+ "call_source": "api",
+ "params": {
+ "event_id": "26855"
+ }
+ }'
+```
+
+## Request example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/events/26855/full-access/unlock" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "x-call-source: api"
+```
+
+## Response example
+
+```json
+{
+ "success": true,
+ "alreadyUnlocked": false,
+ "eventUnlocked": true,
+ "visitorUnlocked": true,
+ "visitorSkipped": false,
+ "totalCreditsUsed": 5000,
+ "balanceAfter": {
+ "subscriptionBalance": 25000,
+ "permanentBalance": 50,
+ "totalBalance": 33050,
+ "unlimited": false
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+}
+```
+
+## Response fields
+
+| Field | Description |
+| --- | --- |
+| `success` | Whether the atomic unlock completed successfully. |
+| `alreadyUnlocked` | `true` only when both access layers were already unlocked before this request. |
+| `eventUnlocked` | Whether this request newly unlocked base event access. |
+| `visitorUnlocked` | Whether this request newly unlocked visitor access. |
+| `visitorSkipped` | Whether visitor access was skipped because no visitor data was available. |
+| `totalCreditsUsed` | Total credits charged by this request: `0`, `2000`, `3000`, or `5000`. |
+| `balanceAfter` | Remaining balance after a paid unlock, or `null` when both layers were already unlocked. |
+| `event` | Minimal event identity for confirmation and audit logs. |
+
+## Atomicity and idempotency
+
+The base event and visitor unlocks run in one transaction. If the paid operation fails, neither access layer should be partially committed. Repeating the call charges only for any layer that is still missing.
+
+## Error responses
+
+- `400 Bad Request` — no chargeable base-event contacts or no active subscription for an available visitor layer.
+- `401 Unauthorized`
+- `402 Payment Required`
+- `404 Not Found`
+- `409 Conflict` — another credit operation is in progress.
+- `429 Too Many Requests`
+
+## Related endpoints
+
+- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access)
+- [Credits balance](/api-reference/credits/get-credits-balance)
+- [Actions precheck](/api-reference/actions/precheck-an-external-action)
diff --git a/api-reference-backup/events/unlock.mdx b/api-reference-backup/events/unlock.mdx
index cb8f723..664d67f 100644
--- a/api-reference-backup/events/unlock.mdx
+++ b/api-reference-backup/events/unlock.mdx
@@ -1,10 +1,10 @@
---
title: "Unlock event"
-description: "Unlock full Lensmor event access with credits so integrations can retrieve complete exhibitor and personnel coverage beyond preview limits."
+description: "Unlock base Lensmor event access with credits so integrations can retrieve complete exhibitor and personnel coverage beyond preview limits."
openapi: "openapi.json POST /external/events/{id}/unlock"
---
-Unlock full access to an event's exhibitor and personnel results.
+Unlock base event access for complete exhibitor and personnel results. This endpoint does not unlock the separate visitor data layer.
Use this endpoint when preview list responses indicate that more event-scoped records require an event unlock.
@@ -74,7 +74,7 @@ curl -X POST "https://platform.lensmor.com/external/events/139574/unlock" \
| Field | Description |
| --- | --- |
| `success` | Whether the unlock request completed successfully. |
-| `alreadyUnlocked` | `true` when the API key owner already had full access to the event. |
+| `alreadyUnlocked` | `true` when the API key owner already had base event access. |
| `creditsUsed` | Number of credits charged by this request. `0` when the event was already unlocked. |
| `balanceAfter` | Remaining balance snapshot after a paid unlock. `null` when no credits were charged, such as an already-unlocked event. |
| `balanceAfter.subscriptionBalance` | Remaining subscription credits. |
@@ -93,6 +93,8 @@ Your integration can use this behavior to safely retry after a network failure,
After a successful unlock, call event-scoped exhibitor or personnel list endpoints again. The list response should move from preview semantics to full-access semantics for that event.
+To add visitor records, call [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access) after base access, or use [Unlock full event access](/api-reference/events/unlock-full-event-access) to unlock missing layers atomically.
+
## Error responses
- `401 Unauthorized`
- `402 Payment Required`
diff --git a/api-reference-backup/exhibitors/events.mdx b/api-reference-backup/exhibitors/events.mdx
index 7173de3..3d911b9 100644
--- a/api-reference-backup/exhibitors/events.mdx
+++ b/api-reference-backup/exhibitors/events.mdx
@@ -54,8 +54,9 @@ curl "https://platform.lensmor.com/external/exhibitors/events?exhibitor_id=456&p
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 12,
@@ -76,7 +77,9 @@ curl "https://platform.lensmor.com/external/exhibitors/events?exhibitor_id=456&p
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `image`, `dataSource` | Media and source metadata. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
diff --git a/api-reference-backup/exhibitors/search-by-company-name.mdx b/api-reference-backup/exhibitors/search-by-company-name.mdx
index db43e8d..26e11ff 100644
--- a/api-reference-backup/exhibitors/search-by-company-name.mdx
+++ b/api-reference-backup/exhibitors/search-by-company-name.mdx
@@ -21,7 +21,7 @@ Choose this endpoint when:
Use [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) 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/get-credits-balance) and confirm with the user before calling it in a credit-safe workflow.
+ This endpoint consumes `50` credits only when the response contains at least one exhibitor. An empty result does not consume credits. Check [Credits balance](/api-reference/credits/get-credits-balance) and confirm with the user before calling it in a credit-safe workflow.
## Endpoint
@@ -40,11 +40,17 @@ See [Authentication](/authentication)
| `page` | No | integer | Defaults to `1`. |
| `pageSize` | No | integer | Defaults to `20`; maximum `100`. |
+## Headers
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
## Request example
```bash
curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company-name" \
-H "Authorization: Bearer $LENSMOR_API_KEY" \
-H "Content-Type: application/json" \
+ -H "x-call-source: api" \
-d '{"company_name":"Acme","page":1,"pageSize":20}'
```
@@ -104,7 +110,7 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company
## 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`.
+This endpoint consumes `50` credits after the company-name matcher finds at least one exhibitor. A successful empty response consumes `0` credits. If a non-empty search cannot spend the required credits, the API returns `402 Payment Required` instead of returning the matched records.
Refresh [Credits balance](/api-reference/credits/get-credits-balance) after successful searches when your UI or audit log needs final credit reconciliation.
@@ -120,5 +126,5 @@ This endpoint is precision-first. It is designed to avoid surprising broad match
## Notes
- This endpoint uses `POST` because company name matching may evolve to accept additional body parameters. It does not create a persistent resource.
-- No matches return an empty paginated success response rather than `404 Not Found`.
+- No matches return an empty paginated success response rather than `404 Not Found`, and do not consume credits.
- If you need event results for a company name, use `POST /external/exhibitors/search-events`.
diff --git a/api-reference-backup/exhibitors/search-events.mdx b/api-reference-backup/exhibitors/search-events.mdx
index b5dfb62..5b4ccda 100644
--- a/api-reference-backup/exhibitors/search-events.mdx
+++ b/api-reference-backup/exhibitors/search-events.mdx
@@ -36,6 +36,7 @@ See [Authentication](/authentication)
| Name | Required | Type | Notes |
| --- | --- | --- | --- |
| `company_name` | Yes | string | Company name input. Incomplete names are allowed, but matching stays precision-first. |
+| `sponsor_match_starred` | No | integer | `1` returns starred Sponsor Match events, `0` returns non-starred events; omit to include both. |
| `page` | No | integer | Defaults to `1`. |
| `pageSize` | No | integer | Defaults to `20`; maximum `100`. |
@@ -50,7 +51,7 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
-H "Authorization: Bearer $LENSMOR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-call-source: api" \
- -d '{"company_name":"Acme","page":1,"pageSize":20}'
+ -d '{"company_name":"Acme","sponsor_match_starred":1,"page":1,"pageSize":20}'
```
## Response example
@@ -70,11 +71,10 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
"city": "Las Vegas",
"region": "Nevada",
"country": "United States",
- "attendeeCount": null,
"exhibitorCount": 100,
- "personnelCount": 50,
+ "sponsorMatchStarred": 1,
"image": null,
- "dataSource": null,
+ "dataSource": "Lensmor",
"matchedExhibitors": [
{
"id": "456",
@@ -100,13 +100,15 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
| `name`, `nickname`, `description`, `url` | Event display and source fields. |
| `dateStart`, `dateEnd` | Event date range. |
| `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. |
+| `exhibitorCount` | Exhibitor coverage count when available. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | Always `Lensmor` for public event records. |
| `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`.
+This endpoint consumes `50` credits after the company-name matcher finds at least one event. A successful empty response consumes `0` credits. If a non-empty search cannot spend the required credits, the API returns `402 Payment Required` instead of returning the matched records.
Refresh [Credits balance](/api-reference/credits/get-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.
@@ -118,8 +120,8 @@ Refresh [Credits balance](/api-reference/credits/get-credits-balance) after succ
## Notes
- This endpoint is separate from `GET /external/exhibitors/events`, which requires a known `exhibitor_id`.
-- This endpoint currently consumes `50` credits per successful search attempt. See [Credits and access](/concepts/credits-and-access).
+- This endpoint consumes `50` credits only for a non-empty result. See [Credits and access](/concepts/credits-and-access).
- Compact-normalized queries shorter than `3` characters use exact-only matching after case, whitespace, and connector-punctuation normalization.
- Longer queries use strict token or prefix admission instead of permissive `contains` matching.
-- No matches return an empty paginated success response rather than `404 Not Found`.
+- No matches return an empty paginated success response rather than `404 Not Found`, and do not consume credits.
- `matchedExhibitors` contains only the admitted matched exhibitors that actually participate in the returned event, and event results are returned in deterministic recency order.
diff --git a/api-reference-backup/openapi.json b/api-reference-backup/openapi.json
index dd14ecf..e2295dc 100644
--- a/api-reference-backup/openapi.json
+++ b/api-reference-backup/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Lensmor API",
- "version": "0.23.1",
+ "version": "0.24.0",
"description": "Lensmor Event Intelligence API for event discovery, exhibitor research, personnel lookup, credits, and profile matching.",
"license": {
"name": "Lensmor Terms of Service",
@@ -56,7 +56,7 @@
"Credits"
],
"summary": "Get credits balance",
- "description": "Return subscription, gift, and total credits for the authenticated API key owner without spending credits.",
+ "description": "Return subscription, add-on, gift, and total credits for the authenticated API key owner without spending credits.",
"operationId": "getExternalCreditsBalance",
"responses": {
"200": {
@@ -67,10 +67,13 @@
"$ref": "#/components/schemas/CreditBalance"
},
"example": {
- "totalAmount": 35050,
- "totalBalance": 30050,
+ "totalAmount": 45050,
+ "totalBalance": 38050,
"subscriptionAmount": 35000,
"subscriptionBalance": 30000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
"giftAmount": 50,
"giftBalance": 50,
"resetAt": 1714521600000
@@ -251,6 +254,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"image": "https://example.com/ces-image.png",
"dataSource": "Lensmor"
}
@@ -316,6 +320,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"dataSource": "Lensmor"
}
}
@@ -401,7 +406,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"summary": {
"attendeeCount": 40000,
@@ -458,7 +464,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"score": 8.1,
"recommendation": "recommended",
@@ -583,7 +590,7 @@
"Events"
],
"summary": "Unlock event access",
- "description": "Unlock full exhibitor and personnel access for an event; a first unlock can consume 2,000 credits and repeated unlocks are idempotent.",
+ "description": "Unlock base event access for complete exhibitor and personnel results. A first unlock can consume 2,000 credits, repeated unlocks are idempotent, and the separate visitor data layer is not included.",
"operationId": "unlockEvent",
"parameters": [
{
@@ -635,6 +642,143 @@
}
}
},
+ "/external/events/{id}/visitors/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock event visitor access",
+ "description": "Unlock visitor records for an event that already has base event access. The first successful visitor unlock consumes 3,000 credits, requires an active subscription and available visitor data, and repeated unlocks are idempotent.",
+ "operationId": "unlockEventVisitors",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Visitor access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventVisitorUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "creditsSpent": 3000,
+ "balanceAfter": {
+ "totalAmount": 45050,
+ "totalBalance": 35050,
+ "subscriptionAmount": 35000,
+ "subscriptionBalance": 27000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
+ "giftAmount": 50,
+ "giftBalance": 50,
+ "resetAt": 1785542400000
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
+ "/external/events/{id}/full-access/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock full event access",
+ "description": "Atomically unlock base event access and visitor access, charging only for missing access layers: 2,000 credits for base event access and 3,000 credits for visitor access, up to 5,000 credits total.",
+ "operationId": "unlockEventFullAccess",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Full event access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventFullAccessUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "eventUnlocked": true,
+ "visitorUnlocked": true,
+ "visitorSkipped": false,
+ "totalCreditsUsed": 5000,
+ "balanceAfter": {
+ "subscriptionBalance": 25000,
+ "permanentBalance": 50,
+ "totalBalance": 33050,
+ "unlimited": false
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
"/external/exhibitors/list": {
"get": {
"tags": [
@@ -838,8 +982,13 @@
"Exhibitors"
],
"summary": "Search exhibitors by company name",
- "description": "Run a precision-first exhibitor lookup by company name; a successful search attempt can consume 50 credits.",
+ "description": "Run a precision-first exhibitor lookup by company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchExhibitorsByCompanyName",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
"requestBody": {
"$ref": "#/components/requestBodies/CompanyNameBody"
},
@@ -880,6 +1029,9 @@
"401": {
"$ref": "#/components/responses/Unauthorized"
},
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
"429": {
"$ref": "#/components/responses/RateLimited"
}
@@ -892,7 +1044,7 @@
"Exhibitors"
],
"summary": "Search events by exhibitor company name",
- "description": "Find events associated with a company name; a successful reverse event search can consume 50 credits.",
+ "description": "Find events associated with a company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchEventsByExhibitorCompanyName",
"parameters": [
{
@@ -900,7 +1052,7 @@
}
],
"requestBody": {
- "$ref": "#/components/requestBodies/CompanyNameBody"
+ "$ref": "#/components/requestBodies/SearchEventsByCompanyNameBody"
},
"responses": {
"201": {
@@ -908,7 +1060,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/EventPage"
+ "$ref": "#/components/schemas/EventReverseSearchPage"
},
"example": {
"items": [
@@ -919,7 +1071,15 @@
"dateStart": "2026-03-15",
"dateEnd": "2026-03-18",
"city": "Las Vegas",
- "country": "United States"
+ "country": "United States",
+ "sponsorMatchStarred": 1,
+ "dataSource": "Lensmor",
+ "matchedExhibitors": [
+ {
+ "id": "456",
+ "companyName": "Acme Retail Systems"
+ }
+ ]
}
],
"total": 1,
@@ -1135,7 +1295,12 @@
"title": "VP of Retail Technology",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
],
"total": 42,
@@ -1193,7 +1358,12 @@
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
}
}
@@ -1317,7 +1487,10 @@
"fullName": "Jane Smith",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
},
"events": {
"items": [
@@ -1473,9 +1646,12 @@
"title": "VP Marketing",
"companyName": "Acme",
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": null,
- "linkedinActivityStatus": null
+ "linkedinActivityStatus": null,
+ "eventCount": 3
}
],
"total": 1,
@@ -1550,11 +1726,11 @@
},
"responses": {
"201": {
- "description": "Unlock task accepted",
+ "description": "Unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1814,7 +1990,7 @@
"Contacts"
],
"summary": "Unlock contact phone numbers",
- "description": "Start an asynchronous phone unlock task that consumes credits per chargeable contact.",
+ "description": "Start an asynchronous phone unlock task that consumes 150 credits per chargeable contact.",
"operationId": "unlockContactPhones",
"parameters": [
{
@@ -1839,11 +2015,11 @@
},
"responses": {
"201": {
- "description": "Phone unlock task accepted",
+ "description": "Phone unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1955,6 +2131,10 @@
"email",
"linkedin_message"
],
+ "linkedin_message_types": [
+ "inmail",
+ "connection_note"
+ ],
"outreach_purpose": "Book a product demo"
}
}
@@ -2043,7 +2223,16 @@
],
"messages": {
"email": "Hi Jane, I would like to compare notes before Retail Technology Expo.",
- "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?"
+ "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?",
+ "linkedin": {
+ "inmail": {
+ "subject": "Retail Technology Expo",
+ "body": "Hi Jane, I would like to compare notes before the event."
+ },
+ "connection_note": {
+ "body": "Hi Jane, would be glad to connect before the event."
+ }
+ }
},
"outreach_purpose": "Book a product demo",
"tone": "professional",
@@ -2273,6 +2462,51 @@
}
}
},
+ "SearchEventsByCompanyNameBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "company_name"
+ ],
+ "properties": {
+ "company_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "sponsor_match_starred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Filter to events whose Sponsor Match starred state is 0 or 1. Omit the field to include both states."
+ },
+ "page": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "pageSize": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20
+ }
+ }
+ },
+ "example": {
+ "company_name": "Acme",
+ "sponsor_match_starred": 1,
+ "page": 1,
+ "pageSize": 20
+ }
+ }
+ }
+ },
"ProfileMatchingBody": {
"required": true,
"content": {
@@ -2459,6 +2693,9 @@
"totalBalance",
"subscriptionAmount",
"subscriptionBalance",
+ "addonAmount",
+ "addonBalance",
+ "addonExpireAt",
"giftAmount",
"giftBalance",
"resetAt"
@@ -2476,6 +2713,18 @@
"subscriptionBalance": {
"type": "number"
},
+ "addonAmount": {
+ "type": "number"
+ },
+ "addonBalance": {
+ "type": "number"
+ },
+ "addonExpireAt": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
"giftAmount": {
"type": "number"
},
@@ -2572,6 +2821,14 @@
"null"
]
},
+ "sponsorMatchStarred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Sponsor Match starred state returned by Lensmor."
+ },
"image": {
"type": [
"string",
@@ -2579,10 +2836,8 @@
]
},
"dataSource": {
- "type": [
- "string",
- "null"
- ]
+ "type": "string",
+ "const": "Lensmor"
}
},
"additionalProperties": true
@@ -2714,6 +2969,79 @@
},
"additionalProperties": true
},
+ "MatchedExhibitorSummary": {
+ "type": "object",
+ "required": [
+ "id",
+ "companyName"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "companyName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventReverseSearchItem": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventItem"
+ },
+ {
+ "type": "object",
+ "required": [
+ "matchedExhibitors"
+ ],
+ "properties": {
+ "matchedExhibitors": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MatchedExhibitorSummary"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "EventReverseSearchPage": {
+ "type": "object",
+ "required": [
+ "items",
+ "total",
+ "page",
+ "pageSize",
+ "totalPages",
+ "hasMore"
+ ],
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/EventReverseSearchItem"
+ }
+ },
+ "total": {
+ "type": "integer"
+ },
+ "page": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "totalPages": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
"EventFitScore": {
"type": "object",
"required": [
@@ -3065,6 +3393,131 @@
}
}
},
+ "EventUnlockIdentity": {
+ "type": "object",
+ "required": [
+ "id",
+ "eventId",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "eventId": {
+ "type": "string"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventVisitorUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "creditsSpent",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "creditsSpent": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreditBalance"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
+ "EventFullAccessUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "eventUnlocked",
+ "visitorUnlocked",
+ "visitorSkipped",
+ "totalCreditsUsed",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "eventUnlocked": {
+ "type": "boolean"
+ },
+ "visitorUnlocked": {
+ "type": "boolean"
+ },
+ "visitorSkipped": {
+ "type": "boolean"
+ },
+ "totalCreditsUsed": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscriptionBalance",
+ "permanentBalance",
+ "totalBalance",
+ "unlimited"
+ ],
+ "properties": {
+ "subscriptionBalance": {
+ "type": "number"
+ },
+ "permanentBalance": {
+ "type": "number"
+ },
+ "totalBalance": {
+ "type": "number"
+ },
+ "unlimited": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
"ExhibitorBase": {
"type": "object",
"required": [
@@ -3438,12 +3891,24 @@
]
},
"sourceType": {
+ "type": "array",
+ "description": "Normalized Lensmor source labels for this person. Possible values are exhibitor, social, and visitors.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "exhibitor",
+ "social",
+ "visitors"
+ ]
+ }
+ },
+ "email": {
"type": [
"string",
"null"
]
},
- "email": {
+ "phone": {
"type": [
"string",
"null"
@@ -3452,6 +3917,18 @@
"contactUnlockStatus": {
"type": "string"
},
+ "phoneUnlockStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "ready",
+ "processing",
+ "failed",
+ null
+ ]
+ },
"linkedinActivity": {
"type": [
"object",
@@ -3477,6 +3954,26 @@
"string",
"null"
]
+ },
+ "eventCount": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "outreachMessageStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Current outreach generation state when this endpoint includes outreach metadata."
+ },
+ "outreachMessageChannels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Generated or requested outreach channels when outreach metadata is present."
}
}
},
@@ -3611,25 +4108,90 @@
},
"additionalProperties": true
},
- "ContactUnlockAccepted": {
+ "ContactUnlockSubmission": {
"type": "object",
"required": [
- "status",
- "task_id",
- "job_id"
+ "status"
],
"properties": {
"status": {
"type": "string",
- "const": "accepted"
+ "enum": [
+ "accepted",
+ "success"
+ ],
+ "description": "accepted when an asynchronous task was created; success when there was no chargeable work to enqueue."
},
"task_id": {
- "type": "string"
+ "type": "string",
+ "description": "TaskCenter identifier. Present when status is accepted."
},
"job_id": {
- "type": "string"
+ "type": "string",
+ "description": "Alias of task_id. Present when status is accepted."
+ },
+ "skipped_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "skipped_detail": {
+ "type": "object",
+ "properties": {
+ "invalid_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "missing_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlinked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "already_unlocked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlocking_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
- }
+ },
+ "oneOf": [
+ {
+ "properties": {
+ "status": {
+ "const": "accepted"
+ }
+ },
+ "required": [
+ "task_id",
+ "job_id"
+ ]
+ },
+ {
+ "properties": {
+ "status": {
+ "const": "success"
+ }
+ }
+ }
+ ]
},
"ContactUnlockTask": {
"type": "object",
@@ -3733,7 +4295,10 @@
"enum": [
"query_event_personnel",
"unlock_event_contacts",
+ "unlock_event_visitors",
+ "unlock_event_full_access",
"unlock_contact_emails",
+ "unlock_contact_phones",
"search_exhibitor_events",
"others"
]
@@ -3991,6 +4556,18 @@
]
}
},
+ "linkedin_message_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "inmail",
+ "connection_note",
+ "connected_message"
+ ]
+ },
+ "description": "LinkedIn message variants to generate when linkedin_message is requested."
+ },
"outreach_purpose": {
"type": "string"
}
@@ -4074,6 +4651,38 @@
"string",
"null"
]
+ },
+ "linkedin": {
+ "type": "object",
+ "properties": {
+ "inmail": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connection_note": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connected_message": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ }
+ }
}
}
},
diff --git a/api-reference-backup/personnel/events-by-linkedin.mdx b/api-reference-backup/personnel/events-by-linkedin.mdx
index afd5246..9decb6c 100644
--- a/api-reference-backup/personnel/events-by-linkedin.mdx
+++ b/api-reference-backup/personnel/events-by-linkedin.mdx
@@ -52,9 +52,12 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
"seniorityLevel": "vp",
"linkedinUrl": "https://www.linkedin.com/in/jane-smith/",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
},
"events": {
"items": [
@@ -72,8 +75,9 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 1,
@@ -92,9 +96,14 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
| `personnel` | Matched person record using the standard contact-style response shape. |
| `personnel.id` | Personnel identifier for profile and email unlock workflows. |
| `personnel.email` | Email if already unlocked for the caller; otherwise `null`. |
+| `personnel.phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `personnel.contactUnlockStatus` | Current contact access state. |
+| `personnel.phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `personnel.eventCount` | Number of associated events. |
| `events` | Paginated event results connected to the matched person. |
| `events.items[].eventId` | Public event identifier for event-scoped follow-up calls. |
+| `events.items[].sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `events.items[].dataSource` | Always `Lensmor` for public event records. |
| `events.total`, `events.page`, `events.pageSize`, `events.totalPages`, `events.hasMore` | Pagination metadata. |
## URL normalization
diff --git a/api-reference-backup/personnel/events.mdx b/api-reference-backup/personnel/events.mdx
index adb72bb..8db9733 100644
--- a/api-reference-backup/personnel/events.mdx
+++ b/api-reference-backup/personnel/events.mdx
@@ -54,8 +54,9 @@ curl "https://platform.lensmor.com/external/personnel/events?personnel_id=789&pa
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 5,
@@ -76,7 +77,9 @@ curl "https://platform.lensmor.com/external/personnel/events?personnel_id=789&pa
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `image`, `dataSource` | Media and source metadata. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
diff --git a/api-reference-backup/personnel/generate-outreach-message.mdx b/api-reference-backup/personnel/generate-outreach-message.mdx
index 4ac29ee..b7ca639 100644
--- a/api-reference-backup/personnel/generate-outreach-message.mdx
+++ b/api-reference-backup/personnel/generate-outreach-message.mdx
@@ -19,9 +19,29 @@ Use `POST /external/personnel/generate-outreach-message` when you want to:
This endpoint is asynchronous. Each personnel item may create a background task. Use [Get outreach message detail](/api-reference/personnel/get-outreach-message-detail) to retrieve the generated content once processing completes.
- Channels must be one or more of: `email`, `linkedin_message`. You can request both simultaneously.
+ Channels must be one or more of: `email`, `linkedin_message`. When requesting LinkedIn output, use `linkedin_message_types` to choose `inmail`, `connection_note`, and/or `connected_message`.
+## Request body
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `personnel_ids` | Yes | string[] | Maximum `50` personnel IDs. |
+| `event_id` | Yes | string | Supports external `eventId` or internal event ID. |
+| `channels` | Yes | string[] | One or both of `email`, `linkedin_message`. |
+| `linkedin_message_types` | No | string[] | LinkedIn variants: `inmail`, `connection_note`, `connected_message`. |
+| `outreach_purpose` | No | string | Purpose or offer context for personalization. |
+
+```json
+{
+ "personnel_ids": ["789"],
+ "event_id": "139574",
+ "channels": ["email", "linkedin_message"],
+ "linkedin_message_types": ["inmail", "connection_note"],
+ "outreach_purpose": "Invite the contact to a product demo"
+}
+```
+
## Credit behavior
Outreach message generation consumes credits per personnel record processed.
diff --git a/api-reference-backup/personnel/list.mdx b/api-reference-backup/personnel/list.mdx
index 105c6dd..db3550f 100644
--- a/api-reference-backup/personnel/list.mdx
+++ b/api-reference-backup/personnel/list.mdx
@@ -64,15 +64,20 @@ curl "https://platform.lensmor.com/external/personnel/list?event_id=139574&depar
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": {
"activityLevel": "ACTIVE",
"isActive": true,
"latestPostAt": "1747123200000"
},
- "linkedinActivityStatus": "ready"
+ "linkedinActivityStatus": "ready",
+ "eventCount": 5,
+ "outreachMessageStatus": "ready",
+ "outreachMessageChannels": ["email", "linkedin_message"]
}
],
"total": 3,
@@ -116,11 +121,16 @@ curl "https://platform.lensmor.com/external/personnel/list?event_id=139574&depar
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | LinkedIn profile URL when available. |
| `companyName` | Associated company or exhibitor name. |
-| `sourceType` | Source context for the person record. |
+| `sourceType` | Normalized source labels for the person record. Values can include `exhibitor`, `social`, and `visitors`; an empty array means no supported source label is available. |
| `email` | Email address if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Contact access state, such as `locked` or unlocked states. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
| `linkedinActivity` | LinkedIn activity object (`activityLevel` `ACTIVE`/`WARM`/`DORMANT`/`""`, `isActive`, `latestPostAt` unix ms or `null`) when the caller has unlocked it and analysis is complete; otherwise `null`. Independent of `contactUnlockStatus`. |
| `linkedinActivityStatus` | `ready` (data available), `processing` (unlock/analysis running), or `null` (not unlocked / no record). |
+| `eventCount` | Number of events associated with the personnel record. |
+| `outreachMessageStatus` | Current outreach generation status when a record exists. |
+| `outreachMessageChannels` | Channels associated with the latest outreach record. |
| `semantics` | Access metadata for preview/full event coverage. |
## Access semantics
diff --git a/api-reference-backup/personnel/outreach.mdx b/api-reference-backup/personnel/outreach.mdx
index 38bf674..03a71b4 100644
--- a/api-reference-backup/personnel/outreach.mdx
+++ b/api-reference-backup/personnel/outreach.mdx
@@ -18,4 +18,6 @@ Use `GET /external/personnel/outreach` when:
## Notes
- Returns the most recent outreach generation result for the given personnel and event combination.
+- `messages.linkedin` can contain separate `inmail`, `connection_note`, and `connected_message` objects. InMail can include `subject` and `body`; the other variants contain `body`.
+- `messages.linkedin_message` remains available as the legacy single-message field when present.
- Re-generating outreach for the same personnel + event will overwrite previous content.
diff --git a/api-reference-backup/personnel/profile.mdx b/api-reference-backup/personnel/profile.mdx
index f10cd25..ecf4999 100644
--- a/api-reference-backup/personnel/profile.mdx
+++ b/api-reference-backup/personnel/profile.mdx
@@ -44,9 +44,12 @@ curl "https://platform.lensmor.com/external/personnel/profile?personnel_id=789"
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
}
```
@@ -59,9 +62,12 @@ curl "https://platform.lensmor.com/external/personnel/profile?personnel_id=789"
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | Public LinkedIn profile URL when available. |
| `companyName` | Associated company or exhibitor. |
-| `sourceType` | Source context, such as exhibitor-derived data. |
+| `sourceType` | Normalized source labels such as `exhibitor`, `social`, and `visitors`. |
| `email` | Email address if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Current contact access state for this API key owner. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `eventCount` | Number of associated events. |
## Email access
diff --git a/api-reference-backup/profile-matching/actions-apply-recommended-events-paged.mdx b/api-reference-backup/profile-matching/actions-apply-recommended-events-paged.mdx
index f30255a..f25d88c 100644
--- a/api-reference-backup/profile-matching/actions-apply-recommended-events-paged.mdx
+++ b/api-reference-backup/profile-matching/actions-apply-recommended-events-paged.mdx
@@ -98,6 +98,7 @@ curl -X POST "https://platform.lensmor.com/external/profile-matching/actions/app
"priceUpper": "2395",
"eventType": "conference_led",
"quality": "Standard",
+ "sponsorMatchStarred": 0,
"sourceTags": [],
"categories": [
{
@@ -189,7 +190,8 @@ Each event item includes the full event detail shape:
| `verified`, `future`, `historic` | integer | Status flags (0 or 1). |
| `historicEvent` | string \| null | Name of the recurring event series. |
| `image` | string \| null | Event image URL. |
-| `dataSource` | string | Source label (e.g. `"Lensmor"`, `"local_import"`). |
+| `sponsorMatchStarred` | integer | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | string | Always `Lensmor` for public event records. |
| `exhibitorCount`, `personnelCount` | integer \| null | Known counts when available. |
| `eventTypes` | array | Event type metadata. |
| `createTime`, `updateTime` | string | Unix timestamps in milliseconds as strings. |
diff --git a/api-reference/openapi.json b/api-reference/openapi.json
index dd14ecf..e2295dc 100644
--- a/api-reference/openapi.json
+++ b/api-reference/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Lensmor API",
- "version": "0.23.1",
+ "version": "0.24.0",
"description": "Lensmor Event Intelligence API for event discovery, exhibitor research, personnel lookup, credits, and profile matching.",
"license": {
"name": "Lensmor Terms of Service",
@@ -56,7 +56,7 @@
"Credits"
],
"summary": "Get credits balance",
- "description": "Return subscription, gift, and total credits for the authenticated API key owner without spending credits.",
+ "description": "Return subscription, add-on, gift, and total credits for the authenticated API key owner without spending credits.",
"operationId": "getExternalCreditsBalance",
"responses": {
"200": {
@@ -67,10 +67,13 @@
"$ref": "#/components/schemas/CreditBalance"
},
"example": {
- "totalAmount": 35050,
- "totalBalance": 30050,
+ "totalAmount": 45050,
+ "totalBalance": 38050,
"subscriptionAmount": 35000,
"subscriptionBalance": 30000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
"giftAmount": 50,
"giftBalance": 50,
"resetAt": 1714521600000
@@ -251,6 +254,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"image": "https://example.com/ces-image.png",
"dataSource": "Lensmor"
}
@@ -316,6 +320,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"dataSource": "Lensmor"
}
}
@@ -401,7 +406,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"summary": {
"attendeeCount": 40000,
@@ -458,7 +464,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"score": 8.1,
"recommendation": "recommended",
@@ -583,7 +590,7 @@
"Events"
],
"summary": "Unlock event access",
- "description": "Unlock full exhibitor and personnel access for an event; a first unlock can consume 2,000 credits and repeated unlocks are idempotent.",
+ "description": "Unlock base event access for complete exhibitor and personnel results. A first unlock can consume 2,000 credits, repeated unlocks are idempotent, and the separate visitor data layer is not included.",
"operationId": "unlockEvent",
"parameters": [
{
@@ -635,6 +642,143 @@
}
}
},
+ "/external/events/{id}/visitors/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock event visitor access",
+ "description": "Unlock visitor records for an event that already has base event access. The first successful visitor unlock consumes 3,000 credits, requires an active subscription and available visitor data, and repeated unlocks are idempotent.",
+ "operationId": "unlockEventVisitors",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Visitor access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventVisitorUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "creditsSpent": 3000,
+ "balanceAfter": {
+ "totalAmount": 45050,
+ "totalBalance": 35050,
+ "subscriptionAmount": 35000,
+ "subscriptionBalance": 27000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
+ "giftAmount": 50,
+ "giftBalance": 50,
+ "resetAt": 1785542400000
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
+ "/external/events/{id}/full-access/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock full event access",
+ "description": "Atomically unlock base event access and visitor access, charging only for missing access layers: 2,000 credits for base event access and 3,000 credits for visitor access, up to 5,000 credits total.",
+ "operationId": "unlockEventFullAccess",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Full event access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventFullAccessUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "eventUnlocked": true,
+ "visitorUnlocked": true,
+ "visitorSkipped": false,
+ "totalCreditsUsed": 5000,
+ "balanceAfter": {
+ "subscriptionBalance": 25000,
+ "permanentBalance": 50,
+ "totalBalance": 33050,
+ "unlimited": false
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
"/external/exhibitors/list": {
"get": {
"tags": [
@@ -838,8 +982,13 @@
"Exhibitors"
],
"summary": "Search exhibitors by company name",
- "description": "Run a precision-first exhibitor lookup by company name; a successful search attempt can consume 50 credits.",
+ "description": "Run a precision-first exhibitor lookup by company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchExhibitorsByCompanyName",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
"requestBody": {
"$ref": "#/components/requestBodies/CompanyNameBody"
},
@@ -880,6 +1029,9 @@
"401": {
"$ref": "#/components/responses/Unauthorized"
},
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
"429": {
"$ref": "#/components/responses/RateLimited"
}
@@ -892,7 +1044,7 @@
"Exhibitors"
],
"summary": "Search events by exhibitor company name",
- "description": "Find events associated with a company name; a successful reverse event search can consume 50 credits.",
+ "description": "Find events associated with a company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchEventsByExhibitorCompanyName",
"parameters": [
{
@@ -900,7 +1052,7 @@
}
],
"requestBody": {
- "$ref": "#/components/requestBodies/CompanyNameBody"
+ "$ref": "#/components/requestBodies/SearchEventsByCompanyNameBody"
},
"responses": {
"201": {
@@ -908,7 +1060,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/EventPage"
+ "$ref": "#/components/schemas/EventReverseSearchPage"
},
"example": {
"items": [
@@ -919,7 +1071,15 @@
"dateStart": "2026-03-15",
"dateEnd": "2026-03-18",
"city": "Las Vegas",
- "country": "United States"
+ "country": "United States",
+ "sponsorMatchStarred": 1,
+ "dataSource": "Lensmor",
+ "matchedExhibitors": [
+ {
+ "id": "456",
+ "companyName": "Acme Retail Systems"
+ }
+ ]
}
],
"total": 1,
@@ -1135,7 +1295,12 @@
"title": "VP of Retail Technology",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
],
"total": 42,
@@ -1193,7 +1358,12 @@
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
}
}
@@ -1317,7 +1487,10 @@
"fullName": "Jane Smith",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
},
"events": {
"items": [
@@ -1473,9 +1646,12 @@
"title": "VP Marketing",
"companyName": "Acme",
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": null,
- "linkedinActivityStatus": null
+ "linkedinActivityStatus": null,
+ "eventCount": 3
}
],
"total": 1,
@@ -1550,11 +1726,11 @@
},
"responses": {
"201": {
- "description": "Unlock task accepted",
+ "description": "Unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1814,7 +1990,7 @@
"Contacts"
],
"summary": "Unlock contact phone numbers",
- "description": "Start an asynchronous phone unlock task that consumes credits per chargeable contact.",
+ "description": "Start an asynchronous phone unlock task that consumes 150 credits per chargeable contact.",
"operationId": "unlockContactPhones",
"parameters": [
{
@@ -1839,11 +2015,11 @@
},
"responses": {
"201": {
- "description": "Phone unlock task accepted",
+ "description": "Phone unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1955,6 +2131,10 @@
"email",
"linkedin_message"
],
+ "linkedin_message_types": [
+ "inmail",
+ "connection_note"
+ ],
"outreach_purpose": "Book a product demo"
}
}
@@ -2043,7 +2223,16 @@
],
"messages": {
"email": "Hi Jane, I would like to compare notes before Retail Technology Expo.",
- "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?"
+ "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?",
+ "linkedin": {
+ "inmail": {
+ "subject": "Retail Technology Expo",
+ "body": "Hi Jane, I would like to compare notes before the event."
+ },
+ "connection_note": {
+ "body": "Hi Jane, would be glad to connect before the event."
+ }
+ }
},
"outreach_purpose": "Book a product demo",
"tone": "professional",
@@ -2273,6 +2462,51 @@
}
}
},
+ "SearchEventsByCompanyNameBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "company_name"
+ ],
+ "properties": {
+ "company_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "sponsor_match_starred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Filter to events whose Sponsor Match starred state is 0 or 1. Omit the field to include both states."
+ },
+ "page": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "pageSize": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20
+ }
+ }
+ },
+ "example": {
+ "company_name": "Acme",
+ "sponsor_match_starred": 1,
+ "page": 1,
+ "pageSize": 20
+ }
+ }
+ }
+ },
"ProfileMatchingBody": {
"required": true,
"content": {
@@ -2459,6 +2693,9 @@
"totalBalance",
"subscriptionAmount",
"subscriptionBalance",
+ "addonAmount",
+ "addonBalance",
+ "addonExpireAt",
"giftAmount",
"giftBalance",
"resetAt"
@@ -2476,6 +2713,18 @@
"subscriptionBalance": {
"type": "number"
},
+ "addonAmount": {
+ "type": "number"
+ },
+ "addonBalance": {
+ "type": "number"
+ },
+ "addonExpireAt": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
"giftAmount": {
"type": "number"
},
@@ -2572,6 +2821,14 @@
"null"
]
},
+ "sponsorMatchStarred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Sponsor Match starred state returned by Lensmor."
+ },
"image": {
"type": [
"string",
@@ -2579,10 +2836,8 @@
]
},
"dataSource": {
- "type": [
- "string",
- "null"
- ]
+ "type": "string",
+ "const": "Lensmor"
}
},
"additionalProperties": true
@@ -2714,6 +2969,79 @@
},
"additionalProperties": true
},
+ "MatchedExhibitorSummary": {
+ "type": "object",
+ "required": [
+ "id",
+ "companyName"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "companyName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventReverseSearchItem": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventItem"
+ },
+ {
+ "type": "object",
+ "required": [
+ "matchedExhibitors"
+ ],
+ "properties": {
+ "matchedExhibitors": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MatchedExhibitorSummary"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "EventReverseSearchPage": {
+ "type": "object",
+ "required": [
+ "items",
+ "total",
+ "page",
+ "pageSize",
+ "totalPages",
+ "hasMore"
+ ],
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/EventReverseSearchItem"
+ }
+ },
+ "total": {
+ "type": "integer"
+ },
+ "page": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "totalPages": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
"EventFitScore": {
"type": "object",
"required": [
@@ -3065,6 +3393,131 @@
}
}
},
+ "EventUnlockIdentity": {
+ "type": "object",
+ "required": [
+ "id",
+ "eventId",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "eventId": {
+ "type": "string"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventVisitorUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "creditsSpent",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "creditsSpent": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreditBalance"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
+ "EventFullAccessUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "eventUnlocked",
+ "visitorUnlocked",
+ "visitorSkipped",
+ "totalCreditsUsed",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "eventUnlocked": {
+ "type": "boolean"
+ },
+ "visitorUnlocked": {
+ "type": "boolean"
+ },
+ "visitorSkipped": {
+ "type": "boolean"
+ },
+ "totalCreditsUsed": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscriptionBalance",
+ "permanentBalance",
+ "totalBalance",
+ "unlimited"
+ ],
+ "properties": {
+ "subscriptionBalance": {
+ "type": "number"
+ },
+ "permanentBalance": {
+ "type": "number"
+ },
+ "totalBalance": {
+ "type": "number"
+ },
+ "unlimited": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
"ExhibitorBase": {
"type": "object",
"required": [
@@ -3438,12 +3891,24 @@
]
},
"sourceType": {
+ "type": "array",
+ "description": "Normalized Lensmor source labels for this person. Possible values are exhibitor, social, and visitors.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "exhibitor",
+ "social",
+ "visitors"
+ ]
+ }
+ },
+ "email": {
"type": [
"string",
"null"
]
},
- "email": {
+ "phone": {
"type": [
"string",
"null"
@@ -3452,6 +3917,18 @@
"contactUnlockStatus": {
"type": "string"
},
+ "phoneUnlockStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "ready",
+ "processing",
+ "failed",
+ null
+ ]
+ },
"linkedinActivity": {
"type": [
"object",
@@ -3477,6 +3954,26 @@
"string",
"null"
]
+ },
+ "eventCount": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "outreachMessageStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Current outreach generation state when this endpoint includes outreach metadata."
+ },
+ "outreachMessageChannels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Generated or requested outreach channels when outreach metadata is present."
}
}
},
@@ -3611,25 +4108,90 @@
},
"additionalProperties": true
},
- "ContactUnlockAccepted": {
+ "ContactUnlockSubmission": {
"type": "object",
"required": [
- "status",
- "task_id",
- "job_id"
+ "status"
],
"properties": {
"status": {
"type": "string",
- "const": "accepted"
+ "enum": [
+ "accepted",
+ "success"
+ ],
+ "description": "accepted when an asynchronous task was created; success when there was no chargeable work to enqueue."
},
"task_id": {
- "type": "string"
+ "type": "string",
+ "description": "TaskCenter identifier. Present when status is accepted."
},
"job_id": {
- "type": "string"
+ "type": "string",
+ "description": "Alias of task_id. Present when status is accepted."
+ },
+ "skipped_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "skipped_detail": {
+ "type": "object",
+ "properties": {
+ "invalid_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "missing_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlinked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "already_unlocked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlocking_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
- }
+ },
+ "oneOf": [
+ {
+ "properties": {
+ "status": {
+ "const": "accepted"
+ }
+ },
+ "required": [
+ "task_id",
+ "job_id"
+ ]
+ },
+ {
+ "properties": {
+ "status": {
+ "const": "success"
+ }
+ }
+ }
+ ]
},
"ContactUnlockTask": {
"type": "object",
@@ -3733,7 +4295,10 @@
"enum": [
"query_event_personnel",
"unlock_event_contacts",
+ "unlock_event_visitors",
+ "unlock_event_full_access",
"unlock_contact_emails",
+ "unlock_contact_phones",
"search_exhibitor_events",
"others"
]
@@ -3991,6 +4556,18 @@
]
}
},
+ "linkedin_message_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "inmail",
+ "connection_note",
+ "connected_message"
+ ]
+ },
+ "description": "LinkedIn message variants to generate when linkedin_message is requested."
+ },
"outreach_purpose": {
"type": "string"
}
@@ -4074,6 +4651,38 @@
"string",
"null"
]
+ },
+ "linkedin": {
+ "type": "object",
+ "properties": {
+ "inmail": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connection_note": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connected_message": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ }
+ }
}
}
},
diff --git a/authentication.mdx b/authentication.mdx
index 8eae93e..f694f29 100644
--- a/authentication.mdx
+++ b/authentication.mdx
@@ -74,7 +74,7 @@ Lensmor does not currently provide a separate sandbox environment. All API calls
To test your integration safely:
- Use read-only endpoints (`events/list`, `events/detail`, `exhibitors/list`, `personnel/list`, `credits/balance`) freely — they do not consume credits.
-- Avoid calling credit-consuming endpoints (`events/:id/unlock`, `contacts/unlock`, `exhibitors/search-events`) until you are ready to use real data.
+- Avoid calling credit-consuming endpoints (`events/:id/unlock`, `events/:id/visitors/unlock`, `events/:id/full-access/unlock`, `contacts/unlock`, and paid exhibitor company lookups) until you are ready to use real data.
- Check your shared SaaS/API balance with `GET /external/credits/balance` before running paid workflows.
- API usage and Lensmor SaaS usage draw from the same credit pool for the subscribed user account.
diff --git a/changelog.mdx b/changelog.mdx
index 196e8f5..7660d83 100644
--- a/changelog.mdx
+++ b/changelog.mdx
@@ -5,6 +5,31 @@ description: "Versioned Lensmor API documentation updates, newly documented endp
Track documentation updates, newly documented API capabilities, and behavior clarifications that may affect integrations.
+## v0.24.0
+
+Released July 24, 2026.
+
+### Added
+
+- Added `POST /external/events/{id}/visitors/unlock` for the `3000`-credit visitor data layer after base event access is unlocked.
+- Added `POST /external/events/{id}/full-access/unlock` for atomic base event plus visitor access, with dynamic `0`–`5000` credit usage.
+- Added `unlock_event_visitors` and `unlock_event_full_access` action prechecks with access-state, visitor-availability, subscription, and expected-credit details.
+- Added `addonAmount`, `addonBalance`, and `addonExpireAt` to the credit-balance contract.
+
+### Changed
+
+- Changed personnel and contact `sourceType` from a nullable string to a normalized array containing `exhibitor`, `social`, and/or `visitors`.
+- Clarified that exhibitor company search and exhibitor event search consume `50` credits only when the result is non-empty; empty results consume no credits.
+- Documented phone unlock at `150` credits per chargeable contact.
+- Added `sponsor_match_starred` to the exhibitor event-search request and documented `sponsorMatchStarred` plus the canonical `Lensmor` event data source.
+- Added `linkedin_message_types` and typed LinkedIn InMail, connection-note, and connected-message response variants.
+
+### Fixed
+
+- Added phone, phone-unlock, event-count, and outreach status fields to the shared contact response contract.
+- Updated email and phone unlock submission responses to cover both asynchronous `accepted` and no-work `success` outcomes, including skipped personnel details.
+- Updated OpenAPI metadata to version `0.24.0`.
+
## v0.23.1
Released July 19, 2026.
diff --git a/concepts/credits-and-access.mdx b/concepts/credits-and-access.mdx
index 9aa8a90..5cf8a72 100644
--- a/concepts/credits-and-access.mdx
+++ b/concepts/credits-and-access.mdx
@@ -3,7 +3,7 @@ title: "Credits and access"
description: "Learn how Lensmor credits, locked event access, preview limits, unlock guidance, and credit-safe integration patterns work across the API."
---
-Some Lensmor API operations are credit-aware. Read-only discovery endpoints are available with a valid API key, while unlock and reverse-lookup workflows may consume credits.
+Some Lensmor API operations are credit-aware. Most read-only discovery endpoints are available with a valid API key, while unlock and paid company-name lookup workflows may consume credits.
API credits are shared with the Lensmor SaaS app. The API key belongs to a subscribed Lensmor user account, and API usage draws from the same credit pool as product usage in the app.
@@ -13,9 +13,11 @@ Use `GET /external/credits/balance` to inspect the API key owner's current balan
The balance is split into:
- `subscriptionBalance` — credits from the active subscription period.
+- `addonBalance` — credits from active one-time add-on purchases.
- `giftBalance` — non-expiring gift credits.
- `totalBalance` — total currently available credits.
- `resetAt` — the earliest active subscription credit expiration timestamp in milliseconds, or `null`.
+- `addonExpireAt` — the latest active add-on credit expiration timestamp in milliseconds, or `null`.
## Preview and full access
Event-scoped exhibitor and personnel lists can return preview results before the event is unlocked.
@@ -27,15 +29,18 @@ When an event is locked, list responses may include `semantics` metadata describ
- whether more results require unlocking the event
- which unlock action and credit amount applies
-Unlocking an event grants full access to event-scoped exhibitor and personnel results for that event.
+Base event unlock grants full access to event-scoped exhibitor and personnel results. Visitor access is a separate data layer that can be unlocked after base event access or together through the full-access endpoint.
## Credit-consuming operations
| Operation | Typical cost | Notes |
| --- | --- | --- |
-| Event unlock | `2000` credits | Unlocks full event access when contacts are available and the event is not already unlocked. |
+| Base event unlock | `2000` credits | Unlocks full exhibitor and personnel access when contacts are available and the event is not already unlocked. |
+| Event visitor unlock | `3000` credits | Requires base event access, an active subscription, and available visitor data. Repeated unlocks are free. |
+| Full event access | `0`–`5000` credits | Atomically unlocks missing base and visitor layers: `2000` for base event access plus `3000` for visitor access. |
| 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. |
+| Contact phone unlock | `150` credits per chargeable contact | Batch unlock creates an asynchronous task. Already unlocked contacts are not charged again. |
+| Exhibitor company search | `50` credits for a non-empty result | `POST /external/exhibitors/search-by-company-name` charges only when at least one exhibitor is returned. Empty results are free. |
+| Exhibitor event search | `50` credits for a non-empty result | `POST /external/exhibitors/search-events` charges only when at least one event is returned. Empty results are free. |
| 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.
@@ -65,7 +70,7 @@ Use a confirmation step for operations that can spend credits:
Call `GET /external/credits/balance` and show the current total balance.
- Display the known cost from this page, endpoint docs, or `semantics.unlock`.
+ Call precheck for access-dependent actions and display the returned cost, or use the known cost from this page or `semantics.unlock`.
Call the unlock or reverse-lookup endpoint only after the user confirms.
@@ -85,7 +90,7 @@ Supported values are:
- `api` — default customer API usage.
- `agent` — agent-driven usage.
-If omitted or unrecognized, the API records the call as `api`. This header is currently used by event unlock, contact unlock, phone unlock, exhibitor event search, LinkedIn activity unlock, and outreach message generation workflows.
+If omitted or unrecognized, the API records the call as `api`. This header is currently used by base event unlock, visitor unlock, full event access, contact unlock, phone unlock, exhibitor company search, exhibitor event search, LinkedIn activity unlock, and outreach message generation workflows.
## Handling `402 Payment Required`
@@ -105,7 +110,10 @@ Recommended behavior:
Check available subscription and gift credits.
- Unlock full event-scoped coverage.
+ Unlock base event-scoped exhibitor and personnel coverage.
+
+
+ Unlock missing base and visitor access layers in one atomic call.
Start an asynchronous email unlock task.
diff --git a/docs.json b/docs.json
index 206f9e0..9dcba6e 100644
--- a/docs.json
+++ b/docs.json
@@ -28,7 +28,6 @@
}
}
},
- "openapi": "openapi.json",
"navigation": {
"tabs": [
{
@@ -88,7 +87,9 @@
"GET /external/events/brief",
"POST /external/events/fit-score",
"POST /external/events/rank",
- "POST /external/events/{id}/unlock"
+ "POST /external/events/{id}/unlock",
+ "POST /external/events/{id}/visitors/unlock",
+ "POST /external/events/{id}/full-access/unlock"
]
},
{
diff --git a/docs/endpoint-inventory/external-api-endpoints.md b/docs/endpoint-inventory/external-api-endpoints.md
index 59ed2ed..b585408 100644
--- a/docs/endpoint-inventory/external-api-endpoints.md
+++ b/docs/endpoint-inventory/external-api-endpoints.md
@@ -16,6 +16,8 @@
| POST | /external/events/fit-score | `src/modules/external-api/controllers/external-events.controller.ts` | Yes | `201 Created` | Yes | Profile-dependent score |
| POST | /external/events/rank | `src/modules/external-api/controllers/external-events.controller.ts` | Yes | `201 Created` | Yes | Rank a supplied event set |
| POST | /external/events/:id/unlock | `src/modules/external-api/controllers/external-events.controller.ts` | Yes | `201 Created` | Yes | Credit-consuming event unlock |
+| POST | /external/events/:id/visitors/unlock | `src/modules/external-api/controllers/external-events.controller.ts` | Yes | `201 Created` | Yes | Credit-consuming visitor access unlock |
+| POST | /external/events/:id/full-access/unlock | `src/modules/external-api/controllers/external-events.controller.ts` | Yes | `201 Created` | Yes | Atomic base event plus visitor access unlock |
| GET | /external/exhibitors/list | `src/modules/external-api/controllers/external-exhibitors.controller.ts` | Yes | `200 OK` | Yes | Event-scoped exhibitor list with preview semantics |
| POST | /external/exhibitors/search | `src/modules/external-api/controllers/external-exhibitors.controller.ts` | Yes | `201 Created` | Yes | Heuristic exhibitor search by company URL/audience |
| POST | /external/exhibitors/search-by-company-name | `src/modules/external-api/controllers/external-exhibitors.controller.ts` | Yes | `201 Created` | Yes | Precision-first exhibitor lookup |
@@ -55,7 +57,7 @@
- External rate limiting can emit `429 RATE_LIMIT_EXCEEDED` with `Retry-After`
## Verification notes
-- Included exactly 28 customer-facing routes in Mintlify and OpenAPI
+- Included exactly 30 customer-facing routes in Mintlify and OpenAPI
- Confirmed Agent Files, Agent-only Integrations, Debug, and deprecated profile-matching route are intentionally excluded
- Confirmed customer-facing auth docs use Business `sk_...` examples
- Confirmed `/external/*` exceptions are replied with only `code`, `message`, `errorKey`, and `traceId`
diff --git a/docs/reconciliation/external-api-pm-reconciliation.md b/docs/reconciliation/external-api-pm-reconciliation.md
index f042d71..0121309 100644
--- a/docs/reconciliation/external-api-pm-reconciliation.md
+++ b/docs/reconciliation/external-api-pm-reconciliation.md
@@ -6,13 +6,13 @@
| Auth token prefix | Active auth example is `Authorization: Bearer sk_your_api_key` | PM revision should use the same active wording | Needs PM sync | Current customer-facing contract wording must use Business `sk_...` keys |
| Shared error body | External errors use `{ code, message, errorKey, traceId }` with real HTTP status codes | Same shared error body and status semantics are documented in Chinese | Aligned | `/external/*` final error body remains the external contract |
| Shared pagination contract | Common envelope fields are `page`, `pageSize`, `total`, `totalPages`, and `hasMore` | Same semantics are summarized in Chinese | Aligned | Route-specific request naming and item fields remain endpoint-specific |
-| Customer-facing endpoint inventory | 28 published endpoints across credits, actions, events, exhibitors, personnel, contacts, and profile matching | PM revision needs to mirror this latest customer-facing set | Needs PM sync | Agent-only, integration bridge, debug, and deprecated routes are intentionally excluded |
-| Events endpoints | 6 endpoint pages include list, detail, brief, fit-score, rank, and unlock | PM revision needs the same event unlock/access semantics | Needs PM sync | Response examples and status codes now track `API-Doc` |
+| Customer-facing endpoint inventory | 30 published endpoints across credits, actions, events, exhibitors, personnel, contacts, and profile matching | PM revision needs to mirror this latest customer-facing set | Needs PM sync | Agent-only, integration bridge, debug, and deprecated routes are intentionally excluded |
+| Events endpoints | 8 endpoint pages include list, detail, brief, fit-score, rank, base event unlock, visitor unlock, and atomic full-access unlock | PM revision needs the same layered event unlock/access semantics | Needs PM sync | Visitor access costs `3000`; full access charges only missing layers up to `5000` |
| Exhibitors endpoints | 6 endpoint pages define list, search, search-by-company-name, search-events, profile, and related-events behavior | PM revision needs the new company-name lookup page | Needs PM sync | `search-events` is documented as credit-consuming |
| Personnel endpoints | 7 endpoint pages define list, profile, related-events, LinkedIn URL related-events, LinkedIn activity unlock, outreach generation, and outreach detail behavior | PM revision needs the latest LinkedIn activity and outreach contract | Needs PM sync | Lightweight public personnel/contact shape includes unlock status, activity status, and outreach status when available |
| Contacts endpoints | Search, email unlock, email unlock polling, phone unlock, and phone unlock polling are now documented | PM revision needs contact email and phone unlock polling | Needs PM sync | Email and phone are returned only after the matching unlock workflow succeeds |
| Credits endpoint | `GET /external/credits/balance` is now documented | PM revision needs credit balance semantics | Needs PM sync | This supports customer preflight checks |
-| Actions endpoint | `POST /external/actions/precheck` is now documented | PM revision needs precheck semantics | Needs PM sync | Precheck is read-only and should be used before access/credit-sensitive workflows |
+| Actions endpoint | `POST /external/actions/precheck` is now documented | PM revision needs precheck semantics | Needs PM sync | Public action types now include visitor and full-access unlock prechecks |
| Profile Matching endpoints | Current recommended contract is `POST /external/profile-matching/actions/apply-recommended-events/paged` plus `GET /external/profile-matching/recommendations/exhibitors` | PM revision needs the new actions path | Needs PM sync | The older `POST /external/profile-matching/recommendations/events/paged` route is deprecated and excluded from public navigation |
| Excluded routes | Agent files, Agent-only `/external/integrations/*`, debug reset, and deprecated profile matching are excluded from Mintlify | PM revision should not promote these as customer-facing APIs | Aligned principle | They may remain internal implementation notes only |
| Intentional differences | English Mintlify prose, navigation, and cross-links | Chinese PM prose and PM-facing framing | Intentional wording difference only | This is not a contract mismatch |
@@ -25,6 +25,6 @@
## Review notes
-- API-Doc now carries endpoint-level success status codes, request sections, response examples, error responses, and notes for the 28 customer-facing endpoints.
+- API-Doc now carries endpoint-level success status codes, request sections, response examples, error responses, and notes for the 30 customer-facing endpoints.
- This artifact now tracks the API-Doc-first state; PM revision should be synced after this documentation update.
- Future discrepancies should be treated as contract mismatches only when method/path, status code, request fields, response structure, or caller-visible note semantics diverge.
diff --git a/docs/response-sources/events.md b/docs/response-sources/events.md
index 0660cb1..42ccad0 100644
--- a/docs/response-sources/events.md
+++ b/docs/response-sources/events.md
@@ -28,3 +28,23 @@
- Success status source: Nest default for `@Get()` -> `200 OK`
- Response example source: `src/modules/external-api/services/external-events.service.ts#getEventBrief` and `src/modules/external-api/mappers/external-event-response.mapper.ts`
- Ambiguity note: `topCategories` is currently an empty array in the service output.
+
+## POST /external/events/:id/unlock
+- Method/path source: `src/modules/external-api/controllers/external-events.controller.ts`
+- Success status source: Nest default for `@Post()` with no `@HttpCode` override -> `201 Created`
+- Response example source: `src/modules/external-api/services/external-events.service.ts#unlockEvent`
+- Billing source: `CREDIT_PRICES[CreditBizType.EVENT_UNLOCK]` -> `2000`
+
+## POST /external/events/:id/visitors/unlock
+- Method/path source: `src/modules/external-api/controllers/external-events.controller.ts`
+- Success status source: Nest default for `@Post()` with no `@HttpCode` override -> `201 Created`
+- Response example source: `src/modules/external-api/services/external-events.service.ts#unlockVisitor` and `src/modules/unlock/unlock.service.ts#unlockVisitor`
+- Billing source: `CREDIT_PRICES[CreditBizType.VISITOR_UNLOCK]` -> `3000`
+- Contract note: Base event access, active subscription, and available visitor data are required for a first unlock. Repeated unlocks return `creditsSpent: 0`.
+
+## POST /external/events/:id/full-access/unlock
+- Method/path source: `src/modules/external-api/controllers/external-events.controller.ts`
+- Success status source: Nest default for `@Post()` with no `@HttpCode` override -> `201 Created`
+- Response example source: `src/modules/external-api/services/external-events.service.ts#unlockFullAccess` and `src/modules/unlock/unlock.service.ts#unlockFullAccess`
+- Billing source: missing base event access costs `2000`; missing visitor access costs `3000`; both missing costs `5000`
+- Contract note: The operation is atomic and reports `eventUnlocked`, `visitorUnlocked`, `visitorSkipped`, and `totalCreditsUsed`.
diff --git a/docs/response-sources/personnel-and-contacts.md b/docs/response-sources/personnel-and-contacts.md
index cc21b42..67fda3b 100644
--- a/docs/response-sources/personnel-and-contacts.md
+++ b/docs/response-sources/personnel-and-contacts.md
@@ -6,6 +6,7 @@
- Success status source: Nest default for `@Get()` -> `200 OK`
- Response example source: `src/modules/external-api/services/external-personnel.service.ts#list` and `src/modules/external-api/mappers/external-contact-response.mapper.ts`
- Ambiguity note: Returned people use the contact-style public shape.
+- Contract note: `sourceType` is a normalized string array. Supported labels are `exhibitor`, `social`, and `visitors`.
## GET /external/personnel/profile
- Method/path source: `src/modules/external-api/controllers/external-personnel.controller.ts`
@@ -13,6 +14,7 @@
- Success status source: Nest default for `@Get()` -> `200 OK`
- Response example source: `src/modules/external-api/services/external-personnel.service.ts#profile` and `src/modules/external-api/mappers/external-contact-response.mapper.ts`
- Ambiguity note: The response is intentionally lightweight and does not expose email fields.
+- Contract note: `sourceType` uses the same normalized array shape as personnel list and contact search.
## GET /external/personnel/events
- Method/path source: `src/modules/external-api/controllers/external-personnel.controller.ts`
@@ -27,3 +29,4 @@
- Success status source: e2e in `test/e2e/external-contacts.e2e-spec.ts` asserts `200`
- Response example source: `src/modules/external-api/services/external-contacts.service.ts`, `src/modules/personnel/personnel.service.ts#searchExternalContacts`, and `src/modules/external-api/mappers/external-contact-response.mapper.ts`
- Ambiguity note: Email is intentionally omitted from the external response contract.
+- Contract note: `sourceType` is an array and can combine multiple labels for the same person.
diff --git a/guides/find-and-unlock-event.mdx b/guides/find-and-unlock-event.mdx
index e9890e1..88b86fb 100644
--- a/guides/find-and-unlock-event.mdx
+++ b/guides/find-and-unlock-event.mdx
@@ -1,11 +1,11 @@
---
title: "Find and unlock an event"
-description: "Find a relevant trade show, inspect preview exhibitor and personnel data, unlock full event access, and continue with enriched event coverage."
+description: "Find a relevant trade show, inspect preview data, and unlock base event, visitor, or atomic full access with credit-safe prechecks."
---
-Use this guide when your integration needs to move from discovery to full event-level coverage.
+Use this guide when your integration needs to move from discovery to base event coverage, visitor access, or both.
-Event unlocks are intentionally explicit. Lensmor lets you preview available exhibitor and personnel data first, then spend credits only when the event is worth deeper analysis.
+Event unlocks are intentionally explicit. Lensmor lets you preview exhibitor and personnel data first, then choose the access layer the user actually needs.
## Recommended workflow
@@ -19,8 +19,8 @@ Event unlocks are intentionally explicit. Lensmor lets you preview available exh
Call event-scoped list endpoints. If the event is locked, read the `semantics` object to understand preview limits.
-
- Call `POST /external/events/:id/unlock` when `semantics.unlock` indicates more records are available and the event is actionable.
+
+ Use base event unlock for exhibitor and personnel coverage, visitor unlock after base access, or full-access unlock for both layers. Precheck before each paid action.
@@ -95,7 +95,7 @@ Locked events can return a response like this:
Use `semantics` to decide whether to unlock. Do not infer full access from pagination alone.
-## 4. Unlock the event
+## 4. Unlock base event access
```bash
curl -X POST "https://platform.lensmor.com/external/events/139574/unlock" \
@@ -126,12 +126,38 @@ The response tells you whether credits were used:
If `alreadyUnlocked` is `true`, the event was already available and `creditsUsed` is `0`.
+## 5. Add visitor access or unlock both layers
+
+Visitor access is separate from base event access:
+
+- `POST /external/events/:id/visitors/unlock` costs `3000` credits and requires base event access, an active subscription, and visitor data.
+- `POST /external/events/:id/full-access/unlock` atomically charges only for missing layers: `2000` for base event access plus `3000` for visitor access.
+
+Precheck the intended action before execution:
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_full_access",
+ "call_source": "api",
+ "params": {
+ "event_id": "139574"
+ }
+ }'
+```
+
+Continue only when `allowed` is `true`, show the returned `credits` to the user, and refresh the balance after execution. If full-access precheck returns `no_contacts_available` because visitor data is unavailable, use base event unlock when exhibitor and personnel coverage is still needed.
+
## Common mistakes
- Unlocking before checking preview results. Preview first to verify the event has relevant records.
- Using only the event name as your local key. Store `id` or `eventId` so follow-up calls are deterministic.
- Treating `402 Payment Required` as a generic failure. It usually means your integration needs to pause, notify the user, or ask them to add credits.
- Ignoring `semantics`. It tells you whether a list is in preview mode or has full access.
+- Calling visitor unlock before base event access. Precheck returns `state_conflict` in that state.
+- Treating base event access and visitor access as the same layer. They have separate access records and credit costs.
## Related endpoints
@@ -140,7 +166,13 @@ If `alreadyUnlocked` is `true`, the event was already available and `creditsUsed
Browse events with filters and pagination.
- Spend credits to access full event-scoped coverage.
+ Spend credits to access full exhibitor and personnel coverage.
+
+
+ Add visitor records after base event access.
+
+
+ Atomically unlock missing base and visitor layers.
Fetch exhibitors for a selected event.
diff --git a/guides/production-readiness.mdx b/guides/production-readiness.mdx
index a32ab7e..45f858a 100644
--- a/guides/production-readiness.mdx
+++ b/guides/production-readiness.mdx
@@ -17,7 +17,7 @@ Production integrations should handle authentication, pagination, credit-aware a
Always pass `page` and `pageSize` for list endpoints. Do not assume all records fit in the first response.
- Use `GET /external/credits/balance` before event unlocks, email unlocks, or exhibitor event searches.
+ Use `GET /external/credits/balance` before base event, visitor, full-access, contact, or paid exhibitor company workflows.
Use `POST /external/actions/precheck` before workflows where you need to know whether an action is allowed, whether it should charge credits, or which unlock path applies.
@@ -56,18 +56,21 @@ For credit-consuming actions, use a two-step confirmation pattern:
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-event-access), [Unlock contact emails](/api-reference/contacts/unlock-contact-emails), [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name), and [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name).
+This pattern is especially important for [Unlock event](/api-reference/events/unlock-event-access), [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access), [Unlock full event access](/api-reference/events/unlock-full-event-access), [Unlock contact emails](/api-reference/contacts/unlock-contact-emails), and the paid exhibitor company lookup endpoints.
## 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`. |
+| Event visitor unlock | `POST /external/events/:id/visitors/unlock` | Charges `3000` credits when visitor access is still locked. Requires base event access, visitor data, and an active subscription. |
+| Full event access | `POST /external/events/:id/full-access/unlock` | Atomically charges only missing access layers: `2000` for base event access plus `3000` for visitor access. |
| 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. |
+| Contact phone unlock | `POST /external/contacts/unlock-phone` | Starts an async task and charges `150` credits per chargeable contact. |
+| Exhibitor company search | `POST /external/exhibitors/search-by-company-name` | Charges `50` credits only when the result is non-empty. |
+| Exhibitor event search | `POST /external/exhibitors/search-events` | Charges `50` credits only when the result is non-empty. |
-Read-only list, profile, search, and precheck endpoints do not spend credits by themselves.
+Other read-only list, profile, heuristic-search, and precheck endpoints do not spend credits by themselves. Treat the two company-name lookup endpoints above as explicit paid exceptions.
## Rate-limit friendly polling
diff --git a/guides/quickstart.mdx b/guides/quickstart.mdx
index 1ae500a..e5afa87 100644
--- a/guides/quickstart.mdx
+++ b/guides/quickstart.mdx
@@ -10,7 +10,7 @@ The Lensmor API is designed around a typical event-intelligence workflow:
1. Start with a company profile, buyer profile, keyword, or target market.
2. Discover relevant trade shows and event records.
3. Inspect exhibitors and people connected to those events.
-4. Unlock full access or contact emails only when the data is actionable.
+4. Unlock base event access, visitor access, full event access, or contact fields only when the data is actionable.
## Before you begin
@@ -45,6 +45,9 @@ The response includes subscription credits, gift credits, the total balance, and
| 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 |
+| Unlock complete exhibitor and personnel coverage | `POST /external/events/:id/unlock` | `2000` credits when base event access is still locked |
+| Unlock visitor records after base event access | `POST /external/events/:id/visitors/unlock` | `3000` credits when visitor access is still locked |
+| Unlock missing base and visitor layers together | `POST /external/events/:id/full-access/unlock` | Dynamic cost from `0` to `5000` credits |
| 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 |
@@ -86,6 +89,8 @@ curl "https://platform.lensmor.com/external/exhibitors/list?event_id=139574&page
When the response contains `semantics.accessMode: "preview"`, read `semantics.counts.visibleTotal` and `semantics.unlock.requiredForMoreResults` before calling an unlock endpoint. These fields tell you whether more data is available and which action unlocks it.
+For visitor or combined full access, call `POST /external/actions/precheck` with `unlock_event_visitors` or `unlock_event_full_access` before execution. Precheck is read-only and returns current access state, eligibility, and expected credits.
+
## 5. Handle errors and limits
Production integrations should handle these responses explicitly:
@@ -101,7 +106,7 @@ See [Error conventions](/concepts/errors), [Credits and access](/concepts/credit
- Learn how preview access works and when to spend credits on full event coverage.
+ Learn how preview, base event, visitor, and atomic full access work.
Start an email unlock task and poll until the result is ready.
diff --git a/index.mdx b/index.mdx
index 5336e7d..94f5391 100644
--- a/index.mdx
+++ b/index.mdx
@@ -7,7 +7,7 @@ Build event intelligence workflows with the Lensmor API. Use it to discover trad
Use this documentation to authenticate, make your first request, and integrate Lensmor event, exhibitor, personnel, contact, and recommendation resources into your own workflows.
-Current documentation version: `v0.23.1`
+Current documentation version: `v0.24.0`
## Base URL
`https://platform.lensmor.com`
@@ -34,7 +34,7 @@ Authorization: Bearer sk_your_api_key
Match target accounts or buyer profiles against exhibitor data and related event presence.
- Move from preview results to full exhibitor and personnel access for a selected event.
+ Move from preview results to base event coverage, visitor access, or both in one atomic action.
Start asynchronous email unlock jobs and poll task status until contact data is ready.
@@ -48,8 +48,9 @@ Authorization: Bearer sk_your_api_key
| A keyword, geography, or date range | [Events list](/api-reference/events/list-events) | Broad event catalog search. |
| A company website or buyer profile | [Apply profile and get recommended events](/api-reference/profile-matching/apply-profile-and-get-recommended-events) | Returns ranked events from profile context. |
| A known event ID | [Exhibitors list](/api-reference/exhibitors/list-event-exhibitors) or [Personnel list](/api-reference/personnel/list-event-personnel) | Shows event-scoped companies and people, with preview/full access semantics. |
-| A company name and you want companies | [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name) | Precision-first company lookup. |
-| A company name and you want events | [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) | Finds events associated with that company. This can consume credits. |
+| An event that needs both base and visitor access | [Unlock full event access](/api-reference/events/unlock-full-event-access) | Prechecks and atomically unlocks missing access layers for up to `5000` credits. |
+| A company name and you want companies | [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name) | Precision-first company lookup. Non-empty results cost `50` credits. |
+| A company name and you want events | [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) | Finds events associated with that company. Non-empty results cost `50` credits. |
| Personnel IDs and you need emails | [Unlock contact emails](/api-reference/contacts/unlock-contact-emails) | Starts an async email unlock task. |
## Recommended integration path
@@ -59,7 +60,7 @@ Authorization: Bearer sk_your_api_key
Create a user API key and send it as `Authorization: Bearer sk_your_api_key`.
- Call `GET /external/credits/balance` before event unlocks, email unlocks, or reverse event searches.
+ Call `GET /external/credits/balance` and the relevant action precheck before event, visitor, email, or reverse-search workflows.
Use Events, Exhibitors, Personnel, Contacts, or Profile Matching endpoints depending on your workflow.
@@ -80,7 +81,7 @@ If the request succeeds, continue with the [Quickstart guide](/guides/quickstart
## Available resources
- **Credits** — inspect the API key owner's current credit balance.
-- **Events** — browse, score, rank, and inspect event details.
+- **Events** — browse, score, rank, inspect event details, and unlock base or visitor access.
- **Exhibitors** — list exhibitors, search by company context, inspect profiles, and fetch related events.
- **Personnel** — list people, inspect profiles, and fetch related events.
- **Contacts** — search contacts, unlock emails, and poll unlock tasks.
diff --git a/llms-full.txt b/llms-full.txt
index d24b506..8cd05fa 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -11,7 +11,7 @@ Build event intelligence workflows with the Lensmor API. Use it to discover trad
Use this documentation to authenticate, make your first request, and integrate Lensmor event, exhibitor, personnel, contact, and recommendation resources into your own workflows.
-Current documentation version: `v0.23.1`
+Current documentation version: `v0.24.0`
## Base URL
`https://platform.lensmor.com`
@@ -38,7 +38,7 @@ Authorization: Bearer sk_your_api_key
Match target accounts or buyer profiles against exhibitor data and related event presence.
- Move from preview results to full exhibitor and personnel access for a selected event.
+ Move from preview results to base event coverage, visitor access, or both in one atomic action.
Start asynchronous email unlock jobs and poll task status until contact data is ready.
@@ -52,8 +52,9 @@ Authorization: Bearer sk_your_api_key
| A keyword, geography, or date range | [Events list](/api-reference/events/list-events) | Broad event catalog search. |
| A company website or buyer profile | [Apply profile and get recommended events](/api-reference/profile-matching/apply-profile-and-get-recommended-events) | Returns ranked events from profile context. |
| A known event ID | [Exhibitors list](/api-reference/exhibitors/list-event-exhibitors) or [Personnel list](/api-reference/personnel/list-event-personnel) | Shows event-scoped companies and people, with preview/full access semantics. |
-| A company name and you want companies | [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name) | Precision-first company lookup. |
-| A company name and you want events | [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) | Finds events associated with that company. This can consume credits. |
+| An event that needs both base and visitor access | [Unlock full event access](/api-reference/events/unlock-full-event-access) | Prechecks and atomically unlocks missing access layers for up to `5000` credits. |
+| A company name and you want companies | [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name) | Precision-first company lookup. Non-empty results cost `50` credits. |
+| A company name and you want events | [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) | Finds events associated with that company. Non-empty results cost `50` credits. |
| Personnel IDs and you need emails | [Unlock contact emails](/api-reference/contacts/unlock-contact-emails) | Starts an async email unlock task. |
## Recommended integration path
@@ -63,7 +64,7 @@ Authorization: Bearer sk_your_api_key
Create a user API key and send it as `Authorization: Bearer sk_your_api_key`.
- Call `GET /external/credits/balance` before event unlocks, email unlocks, or reverse event searches.
+ Call `GET /external/credits/balance` and the relevant action precheck before event, visitor, email, or reverse-search workflows.
Use Events, Exhibitors, Personnel, Contacts, or Profile Matching endpoints depending on your workflow.
@@ -84,7 +85,7 @@ If the request succeeds, continue with the [Quickstart guide](/guides/quickstart
## Available resources
- **Credits** — inspect the API key owner's current credit balance.
-- **Events** — browse, score, rank, and inspect event details.
+- **Events** — browse, score, rank, inspect event details, and unlock base or visitor access.
- **Exhibitors** — list exhibitors, search by company context, inspect profiles, and fetch related events.
- **Personnel** — list people, inspect profiles, and fetch related events.
- **Contacts** — search contacts, unlock emails, and poll unlock tasks.
@@ -181,7 +182,7 @@ Lensmor does not currently provide a separate sandbox environment. All API calls
To test your integration safely:
- Use read-only endpoints (`events/list`, `events/detail`, `exhibitors/list`, `personnel/list`, `credits/balance`) freely — they do not consume credits.
-- Avoid calling credit-consuming endpoints (`events/:id/unlock`, `contacts/unlock`, `exhibitors/search-events`) until you are ready to use real data.
+- Avoid calling credit-consuming endpoints (`events/:id/unlock`, `events/:id/visitors/unlock`, `events/:id/full-access/unlock`, `contacts/unlock`, and paid exhibitor company lookups) until you are ready to use real data.
- Check your shared SaaS/API balance with `GET /external/credits/balance` before running paid workflows.
- API usage and Lensmor SaaS usage draw from the same credit pool for the subscribed user account.
@@ -201,6 +202,31 @@ Source: /changelog
Track documentation updates, newly documented API capabilities, and behavior clarifications that may affect integrations.
+## v0.24.0
+
+Released July 24, 2026.
+
+### Added
+
+- Added `POST /external/events/{id}/visitors/unlock` for the `3000`-credit visitor data layer after base event access is unlocked.
+- Added `POST /external/events/{id}/full-access/unlock` for atomic base event plus visitor access, with dynamic `0`–`5000` credit usage.
+- Added `unlock_event_visitors` and `unlock_event_full_access` action prechecks with access-state, visitor-availability, subscription, and expected-credit details.
+- Added `addonAmount`, `addonBalance`, and `addonExpireAt` to the credit-balance contract.
+
+### Changed
+
+- Changed personnel and contact `sourceType` from a nullable string to a normalized array containing `exhibitor`, `social`, and/or `visitors`.
+- Clarified that exhibitor company search and exhibitor event search consume `50` credits only when the result is non-empty; empty results consume no credits.
+- Documented phone unlock at `150` credits per chargeable contact.
+- Added `sponsor_match_starred` to the exhibitor event-search request and documented `sponsorMatchStarred` plus the canonical `Lensmor` event data source.
+- Added `linkedin_message_types` and typed LinkedIn InMail, connection-note, and connected-message response variants.
+
+### Fixed
+
+- Added phone, phone-unlock, event-count, and outreach status fields to the shared contact response contract.
+- Updated email and phone unlock submission responses to cover both asynchronous `accepted` and no-work `success` outcomes, including skipped personnel details.
+- Updated OpenAPI metadata to version `0.24.0`.
+
## v0.23.1
Released July 19, 2026.
@@ -293,7 +319,7 @@ The Lensmor API is designed around a typical event-intelligence workflow:
1. Start with a company profile, buyer profile, keyword, or target market.
2. Discover relevant trade shows and event records.
3. Inspect exhibitors and people connected to those events.
-4. Unlock full access or contact emails only when the data is actionable.
+4. Unlock base event access, visitor access, full event access, or contact fields only when the data is actionable.
## Before you begin
@@ -328,6 +354,9 @@ The response includes subscription credits, gift credits, the total balance, and
| 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 |
+| Unlock complete exhibitor and personnel coverage | `POST /external/events/:id/unlock` | `2000` credits when base event access is still locked |
+| Unlock visitor records after base event access | `POST /external/events/:id/visitors/unlock` | `3000` credits when visitor access is still locked |
+| Unlock missing base and visitor layers together | `POST /external/events/:id/full-access/unlock` | Dynamic cost from `0` to `5000` credits |
| 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 |
@@ -369,6 +398,8 @@ curl "https://platform.lensmor.com/external/exhibitors/list?event_id=139574&page
When the response contains `semantics.accessMode: "preview"`, read `semantics.counts.visibleTotal` and `semantics.unlock.requiredForMoreResults` before calling an unlock endpoint. These fields tell you whether more data is available and which action unlocks it.
+For visitor or combined full access, call `POST /external/actions/precheck` with `unlock_event_visitors` or `unlock_event_full_access` before execution. Precheck is read-only and returns current access state, eligibility, and expected credits.
+
## 5. Handle errors and limits
Production integrations should handle these responses explicitly:
@@ -384,7 +415,7 @@ See [Error conventions](/concepts/errors), [Credits and access](/concepts/credit
- Learn how preview access works and when to spend credits on full event coverage.
+ Learn how preview, base event, visitor, and atomic full access work.
Start an email unlock task and poll until the result is ready.
@@ -487,9 +518,9 @@ curl -X POST "https://platform.lensmor.com/external/events/rank" \
Source: /guides/find-and-unlock-event
-Use this guide when your integration needs to move from discovery to full event-level coverage.
+Use this guide when your integration needs to move from discovery to base event coverage, visitor access, or both.
-Event unlocks are intentionally explicit. Lensmor lets you preview available exhibitor and personnel data first, then spend credits only when the event is worth deeper analysis.
+Event unlocks are intentionally explicit. Lensmor lets you preview exhibitor and personnel data first, then choose the access layer the user actually needs.
## Recommended workflow
@@ -503,8 +534,8 @@ Event unlocks are intentionally explicit. Lensmor lets you preview available exh
Call event-scoped list endpoints. If the event is locked, read the `semantics` object to understand preview limits.
-
- Call `POST /external/events/:id/unlock` when `semantics.unlock` indicates more records are available and the event is actionable.
+
+ Use base event unlock for exhibitor and personnel coverage, visitor unlock after base access, or full-access unlock for both layers. Precheck before each paid action.
@@ -579,7 +610,7 @@ Locked events can return a response like this:
Use `semantics` to decide whether to unlock. Do not infer full access from pagination alone.
-## 4. Unlock the event
+## 4. Unlock base event access
```bash
curl -X POST "https://platform.lensmor.com/external/events/139574/unlock" \
@@ -610,12 +641,38 @@ The response tells you whether credits were used:
If `alreadyUnlocked` is `true`, the event was already available and `creditsUsed` is `0`.
+## 5. Add visitor access or unlock both layers
+
+Visitor access is separate from base event access:
+
+- `POST /external/events/:id/visitors/unlock` costs `3000` credits and requires base event access, an active subscription, and visitor data.
+- `POST /external/events/:id/full-access/unlock` atomically charges only for missing layers: `2000` for base event access plus `3000` for visitor access.
+
+Precheck the intended action before execution:
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_full_access",
+ "call_source": "api",
+ "params": {
+ "event_id": "139574"
+ }
+ }'
+```
+
+Continue only when `allowed` is `true`, show the returned `credits` to the user, and refresh the balance after execution. If full-access precheck returns `no_contacts_available` because visitor data is unavailable, use base event unlock when exhibitor and personnel coverage is still needed.
+
## Common mistakes
- Unlocking before checking preview results. Preview first to verify the event has relevant records.
- Using only the event name as your local key. Store `id` or `eventId` so follow-up calls are deterministic.
- Treating `402 Payment Required` as a generic failure. It usually means your integration needs to pause, notify the user, or ask them to add credits.
- Ignoring `semantics`. It tells you whether a list is in preview mode or has full access.
+- Calling visitor unlock before base event access. Precheck returns `state_conflict` in that state.
+- Treating base event access and visitor access as the same layer. They have separate access records and credit costs.
## Related endpoints
@@ -624,7 +681,13 @@ If `alreadyUnlocked` is `true`, the event was already available and `creditsUsed
Browse events with filters and pagination.
- Spend credits to access full event-scoped coverage.
+ Spend credits to access full exhibitor and personnel coverage.
+
+
+ Add visitor records after base event access.
+
+
+ Atomically unlock missing base and visitor layers.
Fetch exhibitors for a selected event.
@@ -775,7 +838,7 @@ Production integrations should handle authentication, pagination, credit-aware a
Always pass `page` and `pageSize` for list endpoints. Do not assume all records fit in the first response.
- Use `GET /external/credits/balance` before event unlocks, email unlocks, or exhibitor event searches.
+ Use `GET /external/credits/balance` before base event, visitor, full-access, contact, or paid exhibitor company workflows.
Use `POST /external/actions/precheck` before workflows where you need to know whether an action is allowed, whether it should charge credits, or which unlock path applies.
@@ -814,18 +877,21 @@ For credit-consuming actions, use a two-step confirmation pattern:
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-event-access), [Unlock contact emails](/api-reference/contacts/unlock-contact-emails), [Exhibitor company search](/api-reference/exhibitors/search-exhibitors-by-company-name), and [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name).
+This pattern is especially important for [Unlock event](/api-reference/events/unlock-event-access), [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access), [Unlock full event access](/api-reference/events/unlock-full-event-access), [Unlock contact emails](/api-reference/contacts/unlock-contact-emails), and the paid exhibitor company lookup endpoints.
## 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`. |
+| Event visitor unlock | `POST /external/events/:id/visitors/unlock` | Charges `3000` credits when visitor access is still locked. Requires base event access, visitor data, and an active subscription. |
+| Full event access | `POST /external/events/:id/full-access/unlock` | Atomically charges only missing access layers: `2000` for base event access plus `3000` for visitor access. |
| 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. |
+| Contact phone unlock | `POST /external/contacts/unlock-phone` | Starts an async task and charges `150` credits per chargeable contact. |
+| Exhibitor company search | `POST /external/exhibitors/search-by-company-name` | Charges `50` credits only when the result is non-empty. |
+| Exhibitor event search | `POST /external/exhibitors/search-events` | Charges `50` credits only when the result is non-empty. |
-Read-only list, profile, search, and precheck endpoints do not spend credits by themselves.
+Other read-only list, profile, heuristic-search, and precheck endpoints do not spend credits by themselves. Treat the two company-name lookup endpoints above as explicit paid exceptions.
## Rate-limit friendly polling
@@ -877,13 +943,15 @@ Source: /api-reference/credits/get-credits-balance
Get the current credit balance for the API key owner.
-Use this endpoint before credit-consuming workflows such as event unlock, contact email unlock, or exhibitor event search.
+Use this endpoint before credit-consuming workflows such as event unlock, visitor unlock, contact email unlock, or exhibitor event search.
## When to use this endpoint
Call `GET /external/credits/balance` before any workflow that can spend credits:
- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access)
+- [Unlock full event access](/api-reference/events/unlock-full-event-access)
- [Unlock contact emails](/api-reference/contacts/unlock-contact-emails)
- [Search exhibitor events](/api-reference/exhibitors/search-events-by-exhibitor-company-name)
@@ -907,10 +975,13 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Response example
```json
{
- "totalAmount": 35050,
- "totalBalance": 30050,
+ "totalAmount": 45050,
+ "totalBalance": 38050,
"subscriptionAmount": 35000,
"subscriptionBalance": 30000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
"giftAmount": 50,
"giftBalance": 50,
"resetAt": 1714521600000
@@ -921,10 +992,13 @@ curl "https://platform.lensmor.com/external/credits/balance" \
| Field | Description |
| --- | --- |
-| `totalAmount` | Total credits ever granted across active subscription and gift buckets. |
+| `totalAmount` | Total credits granted across active subscription, add-on, and gift buckets. |
| `totalBalance` | Total currently available credits. Use this for simple "can this user spend credits?" checks. |
| `subscriptionAmount` | Subscription credits granted for the active credit period. |
-| `subscriptionBalance` | Remaining subscription credits. These are consumed before gift credits. |
+| `subscriptionBalance` | Remaining subscription credits. |
+| `addonAmount` | Add-on credits granted in currently active add-on buckets. |
+| `addonBalance` | Remaining add-on credits. |
+| `addonExpireAt` | Latest active add-on credit expiration timestamp in milliseconds, or `null`. |
| `giftAmount` | Gift credits granted to the user. |
| `giftBalance` | Remaining gift credits. |
| `resetAt` | Earliest active subscription credit expiration timestamp in milliseconds, or `null`. |
@@ -932,9 +1006,9 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Integration guidance
- Use `totalBalance` for high-level balance checks.
-- Use bucket-specific fields when your UI needs to explain subscription vs. gift credit usage.
+- Use bucket-specific fields when your UI needs to explain subscription, add-on, and gift credit usage.
- Refresh the balance after credit-consuming operations because `balanceAfter` may differ from a previously cached value.
-- Treat `resetAt` as nullable. Some accounts may only have gift credits or no active subscription credit expiration.
+- Treat `resetAt` and `addonExpireAt` as nullable. Some accounts may not have an active expiring bucket of that type.
## Error responses
- `401 Unauthorized`
@@ -942,7 +1016,7 @@ curl "https://platform.lensmor.com/external/credits/balance" \
## Notes
- `resetAt` is a timestamp in milliseconds for the earliest active subscription credit expiration, or `null`.
-- Subscription credits are consumed before gift credits.
+- `addonExpireAt` is a timestamp in milliseconds for the latest active add-on credit expiration, or `null`.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
---
@@ -967,7 +1041,7 @@ See [Authentication](/authentication)
## Request body
| Name | Required | Type | Notes |
| --- | --- | --- | --- |
-| `action_type` | Yes | string | Supported values include `query_event_personnel`, `unlock_event_contacts`, `unlock_contact_emails`, `search_exhibitor_events`, and `others`. |
+| `action_type` | Yes | string | Supported customer actions include `query_event_personnel`, `unlock_event_contacts`, `unlock_event_visitors`, `unlock_event_full_access`, `unlock_contact_emails`, `unlock_contact_phones`, `search_exhibitor_events`, and `others`. |
| `params` | Yes | object | Action-specific inputs, such as `{ "event_id": "26855" }`. |
| `call_source` | No | string | Source label such as `api` or `agent`. |
| `locale` | No | string | Optional locale, such as `en-US`. |
@@ -1021,9 +1095,34 @@ curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
| `biz_code` | Billing/business code used by the execution path when applicable. |
| `detail` | Action-specific metadata. Shape varies by `action_type`. |
+## Visitor unlock example
+
+```json
+{
+ "ok": true,
+ "action_type": "unlock_event_visitors",
+ "allowed": true,
+ "should_charge": true,
+ "credits": 3000,
+ "reason_code": "ok",
+ "biz_code": "Visitor Unlock - API",
+ "detail": {
+ "event_id": "26855",
+ "event_unlocked": true,
+ "visitor_unlocked": false,
+ "visitor_data_available": true,
+ "visitor_access_credits": 3000,
+ "available_balance": 8000
+ }
+}
+```
+
## Notes
- Precheck is read-only. It does not unlock records, create tasks, or consume credits.
- `query_event_personnel` is useful before calling [Personnel list](/api-reference/personnel/list-event-personnel).
+- `unlock_event_visitors` requires base event access first. It can report `3000` expected credits, `state_conflict` when the event is still locked, `no_contacts_available` when visitor data is unavailable, or `forbidden` when the account has no active subscription.
+- `unlock_event_full_access` calculates only the missing access layers: `2000` for base event access plus `3000` for visitor access. It can therefore report `0`, `2000`, `3000`, or `5000` credits.
+- For full-access precheck, `detail` includes `event_unlocked`, `visitor_unlocked`, `visitor_data_available`, `event_access_credits`, `visitor_access_credits`, and `full_access_credits`.
- Once a list endpoint returns `semantics`, treat that response as the authoritative explanation for the actual list result.
- Precheck decisions can change as credits, event access, or underlying data changes. Recheck before executing a paid action.
@@ -1110,6 +1209,7 @@ curl "https://platform.lensmor.com/external/events/list?event_type=conference_le
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"image": "https://example.com/ces-image.png",
"dataSource": "Lensmor"
}
@@ -1136,7 +1236,8 @@ curl "https://platform.lensmor.com/external/events/list?event_type=conference_le
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `dataSource` | Source category for the returned record (e.g. `"Lensmor"`, `"local_import"`). Do not hard-code checks against specific values. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
@@ -1214,6 +1315,7 @@ curl "https://platform.lensmor.com/external/events/139574" \
"priceLower": "0",
"priceUpper": "0",
"eventType": "In-person",
+ "sponsorMatchStarred": 0,
"categories": [
{
"id": 11,
@@ -1260,7 +1362,9 @@ curl "https://platform.lensmor.com/external/events/139574" \
| `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. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
## Count fields
@@ -1281,7 +1385,7 @@ When you need current coverage numbers, call the event-scoped list endpoint and
- For event identifiers, see [Identifiers](/concepts/identifiers). In the current API version, `id` and `eventId` return the same value.
- Some fields can be `null` when the underlying source does not provide that data.
- String-typed numeric fields (`latitude`, `longitude`, `priceLower`, `priceUpper`) require client-side parsing when used for calculations.
-- `dataSource` indicates the data origin (e.g. `"Lensmor"`, `"local_import"`). Do not hard-code checks against specific values.
+- `dataSource` is always `"Lensmor"` for public event records.
- `categories` is an array of objects. Each object contains `id` (integer), `code` (string), `name` (string), `description` (string or null), and `confidence` (string).
- This endpoint does not unlock event-scoped exhibitor or personnel access by itself.
@@ -1338,8 +1442,9 @@ curl "https://platform.lensmor.com/external/events/brief?event_id=139574" \
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
},
"summary": {
"attendeeCount": 40000,
@@ -1431,8 +1536,9 @@ curl -X POST "https://platform.lensmor.com/external/events/fit-score" \
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
},
"score": 8.1,
"recommendation": "recommended",
@@ -1578,7 +1684,7 @@ curl -X POST "https://platform.lensmor.com/external/events/rank" \
Source: /api-reference/events/unlock-event-access
-Unlock full access to an event's exhibitor and personnel results.
+Unlock base event access for complete exhibitor and personnel results. This endpoint does not unlock the separate visitor data layer.
Use this endpoint when preview list responses indicate that more event-scoped records require an event unlock.
@@ -1648,7 +1754,7 @@ curl -X POST "https://platform.lensmor.com/external/events/139574/unlock" \
| Field | Description |
| --- | --- |
| `success` | Whether the unlock request completed successfully. |
-| `alreadyUnlocked` | `true` when the API key owner already had full access to the event. |
+| `alreadyUnlocked` | `true` when the API key owner already had base event access. |
| `creditsUsed` | Number of credits charged by this request. `0` when the event was already unlocked. |
| `balanceAfter` | Remaining balance snapshot after a paid unlock. `null` when no credits were charged, such as an already-unlocked event. |
| `balanceAfter.subscriptionBalance` | Remaining subscription credits. |
@@ -1667,6 +1773,8 @@ Your integration can use this behavior to safely retry after a network failure,
After a successful unlock, call event-scoped exhibitor or personnel list endpoints again. The list response should move from preview semantics to full-access semantics for that event.
+To add visitor records, call [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access) after base access, or use [Unlock full event access](/api-reference/events/unlock-full-event-access) to unlock missing layers atomically.
+
## Error responses
- `401 Unauthorized`
- `402 Payment Required`
@@ -1680,6 +1788,266 @@ After a successful unlock, call event-scoped exhibitor or personnel list endpoin
---
+## Unlock event visitor access
+
+Source: /api-reference/events/unlock-event-visitor-access
+
+Unlock the visitor data layer for an event that already has base event access.
+
+Use this endpoint when the caller needs visitor records in addition to the exhibitor and personnel coverage unlocked by [Unlock event](/api-reference/events/unlock-event-access).
+
+## Prerequisites
+
+Before executing this paid action:
+
+1. Confirm base event access is already unlocked.
+2. Call [Actions precheck](/api-reference/actions/precheck-an-external-action) with `action_type: "unlock_event_visitors"` and the selected `event_id`.
+3. Continue only when precheck returns `allowed: true`.
+
+Visitor access requires an active subscription, available visitor data for the event, and enough credits.
+
+
+ The first successful visitor unlock consumes `3000` credits. Precheck is read-only and should be used before the paid call.
+
+
+## Endpoint
+
+`POST /external/events/:id/visitors/unlock`
+
+## Authentication
+
+See [Authentication](/authentication).
+
+## Success status code
+
+`201 Created`
+
+## Path parameters
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `id` | Yes | string | Supports the event `id` or `eventId` returned by event responses. |
+
+## Headers
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
+## Precheck example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_visitors",
+ "call_source": "api",
+ "params": {
+ "event_id": "26855"
+ }
+ }'
+```
+
+## Request example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/events/26855/visitors/unlock" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "x-call-source: api"
+```
+
+## Response example
+
+```json
+{
+ "success": true,
+ "alreadyUnlocked": false,
+ "creditsSpent": 3000,
+ "balanceAfter": {
+ "totalAmount": 45050,
+ "totalBalance": 35050,
+ "subscriptionAmount": 35000,
+ "subscriptionBalance": 27000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
+ "giftAmount": 50,
+ "giftBalance": 50,
+ "resetAt": 1785542400000
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+}
+```
+
+## Response fields
+
+| Field | Description |
+| --- | --- |
+| `success` | Whether the request completed successfully. |
+| `alreadyUnlocked` | `true` when visitor access was already available before this request. |
+| `creditsSpent` | Credits charged by this request. It is `3000` for a first successful unlock and `0` for an idempotent repeat. |
+| `balanceAfter` | Full credit-balance snapshot after a paid unlock, or `null` when no credits were charged. |
+| `event` | Minimal event identity for confirmation and audit logs. |
+
+## Idempotency
+
+Repeating the unlock for the same account and event does not charge again. An idempotent repeat returns `alreadyUnlocked: true`, `creditsSpent: 0`, and `balanceAfter: null`.
+
+## Error responses
+
+- `400 Bad Request` — no active subscription, base event access is still locked, or no visitor data is available.
+- `401 Unauthorized`
+- `402 Payment Required`
+- `404 Not Found`
+- `409 Conflict` — another credit operation is in progress.
+- `429 Too Many Requests`
+
+The corresponding `errorKey` can include `NO_ACTIVE_SUBSCRIPTION`, `EVENT_NOT_UNLOCKED`, `VISITOR_DATA_NOT_AVAILABLE`, or `CREDIT_OPERATION_IN_PROGRESS`.
+
+## Related endpoints
+
+- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock full event access](/api-reference/events/unlock-full-event-access)
+- [Credits balance](/api-reference/credits/get-credits-balance)
+- [Actions precheck](/api-reference/actions/precheck-an-external-action)
+
+---
+
+## Unlock full event access
+
+Source: /api-reference/events/unlock-full-event-access
+
+Atomically unlock base event access and visitor access for one event.
+
+This endpoint is useful when the user wants all available event data in one confirmed action. The API charges only for access layers that are not already unlocked.
+
+## Credit matrix
+
+| State before the call | Result | Credits |
+| --- | --- | --- |
+| Event locked, visitor locked, visitor data available | Unlock both layers | `5000` |
+| Event unlocked, visitor locked, visitor data available | Unlock visitor layer | `3000` |
+| Event locked, visitor already unlocked | Unlock base event layer | `2000` |
+| Both layers already unlocked | No new unlock | `0` |
+| Event locked, visitor data unavailable | Direct execution can unlock the event and set `visitorSkipped: true` | `2000` |
+
+Base event access costs `2000` credits. Visitor access costs `3000` credits and requires an active subscription when visitor data is available.
+
+
+ Call [Actions precheck](/api-reference/actions/precheck-an-external-action) with `action_type: "unlock_event_full_access"` before execution. Current precheck returns `no_contacts_available` when visitor data is unavailable; use [Unlock event](/api-reference/events/unlock-event-access) when only base event coverage is needed.
+
+
+## Endpoint
+
+`POST /external/events/:id/full-access/unlock`
+
+## Authentication
+
+See [Authentication](/authentication).
+
+## Success status code
+
+`201 Created`
+
+## Path parameters
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `id` | Yes | string | Supports the event `id` or `eventId` returned by event responses. |
+
+## Headers
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
+## Precheck example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/actions/precheck" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action_type": "unlock_event_full_access",
+ "call_source": "api",
+ "params": {
+ "event_id": "26855"
+ }
+ }'
+```
+
+## Request example
+
+```bash
+curl -X POST "https://platform.lensmor.com/external/events/26855/full-access/unlock" \
+ -H "Authorization: Bearer $LENSMOR_API_KEY" \
+ -H "x-call-source: api"
+```
+
+## Response example
+
+```json
+{
+ "success": true,
+ "alreadyUnlocked": false,
+ "eventUnlocked": true,
+ "visitorUnlocked": true,
+ "visitorSkipped": false,
+ "totalCreditsUsed": 5000,
+ "balanceAfter": {
+ "subscriptionBalance": 25000,
+ "permanentBalance": 50,
+ "totalBalance": 33050,
+ "unlimited": false
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+}
+```
+
+## Response fields
+
+| Field | Description |
+| --- | --- |
+| `success` | Whether the atomic unlock completed successfully. |
+| `alreadyUnlocked` | `true` only when both access layers were already unlocked before this request. |
+| `eventUnlocked` | Whether this request newly unlocked base event access. |
+| `visitorUnlocked` | Whether this request newly unlocked visitor access. |
+| `visitorSkipped` | Whether visitor access was skipped because no visitor data was available. |
+| `totalCreditsUsed` | Total credits charged by this request: `0`, `2000`, `3000`, or `5000`. |
+| `balanceAfter` | Remaining balance after a paid unlock, or `null` when both layers were already unlocked. |
+| `event` | Minimal event identity for confirmation and audit logs. |
+
+## Atomicity and idempotency
+
+The base event and visitor unlocks run in one transaction. If the paid operation fails, neither access layer should be partially committed. Repeating the call charges only for any layer that is still missing.
+
+## Error responses
+
+- `400 Bad Request` — no chargeable base-event contacts or no active subscription for an available visitor layer.
+- `401 Unauthorized`
+- `402 Payment Required`
+- `404 Not Found`
+- `409 Conflict` — another credit operation is in progress.
+- `429 Too Many Requests`
+
+## Related endpoints
+
+- [Unlock event](/api-reference/events/unlock-event-access)
+- [Unlock event visitor access](/api-reference/events/unlock-event-visitor-access)
+- [Credits balance](/api-reference/credits/get-credits-balance)
+- [Actions precheck](/api-reference/actions/precheck-an-external-action)
+
+---
+
## Exhibitors list
Source: /api-reference/exhibitors/list-event-exhibitors
@@ -2044,7 +2412,7 @@ Choose this endpoint when:
Use [Exhibitor event search](/api-reference/exhibitors/search-events-by-exhibitor-company-name) 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/get-credits-balance) and confirm with the user before calling it in a credit-safe workflow.
+ This endpoint consumes `50` credits only when the response contains at least one exhibitor. An empty result does not consume credits. Check [Credits balance](/api-reference/credits/get-credits-balance) and confirm with the user before calling it in a credit-safe workflow.
## Endpoint
@@ -2063,11 +2431,17 @@ See [Authentication](/authentication)
| `page` | No | integer | Defaults to `1`. |
| `pageSize` | No | integer | Defaults to `20`; maximum `100`. |
+## Headers
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `x-call-source` | No | string | Optional usage source. Use `api` or `agent`; defaults to `api`. |
+
## Request example
```bash
curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company-name" \
-H "Authorization: Bearer $LENSMOR_API_KEY" \
-H "Content-Type: application/json" \
+ -H "x-call-source: api" \
-d '{"company_name":"Acme","page":1,"pageSize":20}'
```
@@ -2127,7 +2501,7 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-by-company
## 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`.
+This endpoint consumes `50` credits after the company-name matcher finds at least one exhibitor. A successful empty response consumes `0` credits. If a non-empty search cannot spend the required credits, the API returns `402 Payment Required` instead of returning the matched records.
Refresh [Credits balance](/api-reference/credits/get-credits-balance) after successful searches when your UI or audit log needs final credit reconciliation.
@@ -2143,7 +2517,7 @@ This endpoint is precision-first. It is designed to avoid surprising broad match
## Notes
- This endpoint uses `POST` because company name matching may evolve to accept additional body parameters. It does not create a persistent resource.
-- No matches return an empty paginated success response rather than `404 Not Found`.
+- No matches return an empty paginated success response rather than `404 Not Found`, and do not consume credits.
- If you need event results for a company name, use `POST /external/exhibitors/search-events`.
---
@@ -2184,6 +2558,7 @@ See [Authentication](/authentication)
| Name | Required | Type | Notes |
| --- | --- | --- | --- |
| `company_name` | Yes | string | Company name input. Incomplete names are allowed, but matching stays precision-first. |
+| `sponsor_match_starred` | No | integer | `1` returns starred Sponsor Match events, `0` returns non-starred events; omit to include both. |
| `page` | No | integer | Defaults to `1`. |
| `pageSize` | No | integer | Defaults to `20`; maximum `100`. |
@@ -2198,7 +2573,7 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
-H "Authorization: Bearer $LENSMOR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-call-source: api" \
- -d '{"company_name":"Acme","page":1,"pageSize":20}'
+ -d '{"company_name":"Acme","sponsor_match_starred":1,"page":1,"pageSize":20}'
```
## Response example
@@ -2218,11 +2593,10 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
"city": "Las Vegas",
"region": "Nevada",
"country": "United States",
- "attendeeCount": null,
"exhibitorCount": 100,
- "personnelCount": 50,
+ "sponsorMatchStarred": 1,
"image": null,
- "dataSource": null,
+ "dataSource": "Lensmor",
"matchedExhibitors": [
{
"id": "456",
@@ -2248,13 +2622,15 @@ curl -X POST "https://platform.lensmor.com/external/exhibitors/search-events" \
| `name`, `nickname`, `description`, `url` | Event display and source fields. |
| `dateStart`, `dateEnd` | Event date range. |
| `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. |
+| `exhibitorCount` | Exhibitor coverage count when available. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | Always `Lensmor` for public event records. |
| `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`.
+This endpoint consumes `50` credits after the company-name matcher finds at least one event. A successful empty response consumes `0` credits. If a non-empty search cannot spend the required credits, the API returns `402 Payment Required` instead of returning the matched records.
Refresh [Credits balance](/api-reference/credits/get-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.
@@ -2266,10 +2642,10 @@ Refresh [Credits balance](/api-reference/credits/get-credits-balance) after succ
## Notes
- This endpoint is separate from `GET /external/exhibitors/events`, which requires a known `exhibitor_id`.
-- This endpoint currently consumes `50` credits per successful search attempt. See [Credits and access](/concepts/credits-and-access).
+- This endpoint consumes `50` credits only for a non-empty result. See [Credits and access](/concepts/credits-and-access).
- Compact-normalized queries shorter than `3` characters use exact-only matching after case, whitespace, and connector-punctuation normalization.
- Longer queries use strict token or prefix admission instead of permissive `contains` matching.
-- No matches return an empty paginated success response rather than `404 Not Found`.
+- No matches return an empty paginated success response rather than `404 Not Found`, and do not consume credits.
- `matchedExhibitors` contains only the admitted matched exhibitors that actually participate in the returned event, and event results are returned in deterministic recency order.
---
@@ -2413,8 +2789,9 @@ curl "https://platform.lensmor.com/external/exhibitors/events?exhibitor_id=456&p
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 12,
@@ -2435,7 +2812,9 @@ curl "https://platform.lensmor.com/external/exhibitors/events?exhibitor_id=456&p
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `image`, `dataSource` | Media and source metadata. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
@@ -2515,15 +2894,20 @@ curl "https://platform.lensmor.com/external/personnel/list?event_id=139574&depar
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": {
"activityLevel": "ACTIVE",
"isActive": true,
"latestPostAt": "1747123200000"
},
- "linkedinActivityStatus": "ready"
+ "linkedinActivityStatus": "ready",
+ "eventCount": 5,
+ "outreachMessageStatus": "ready",
+ "outreachMessageChannels": ["email", "linkedin_message"]
}
],
"total": 3,
@@ -2567,11 +2951,16 @@ curl "https://platform.lensmor.com/external/personnel/list?event_id=139574&depar
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | LinkedIn profile URL when available. |
| `companyName` | Associated company or exhibitor name. |
-| `sourceType` | Source context for the person record. |
+| `sourceType` | Normalized source labels for the person record. Values can include `exhibitor`, `social`, and `visitors`; an empty array means no supported source label is available. |
| `email` | Email address if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Contact access state, such as `locked` or unlocked states. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
| `linkedinActivity` | LinkedIn activity object (`activityLevel` `ACTIVE`/`WARM`/`DORMANT`/`""`, `isActive`, `latestPostAt` unix ms or `null`) when the caller has unlocked it and analysis is complete; otherwise `null`. Independent of `contactUnlockStatus`. |
| `linkedinActivityStatus` | `ready` (data available), `processing` (unlock/analysis running), or `null` (not unlocked / no record). |
+| `eventCount` | Number of events associated with the personnel record. |
+| `outreachMessageStatus` | Current outreach generation status when a record exists. |
+| `outreachMessageChannels` | Channels associated with the latest outreach record. |
| `semantics` | Access metadata for preview/full event coverage. |
## Access semantics
@@ -2641,9 +3030,12 @@ curl "https://platform.lensmor.com/external/personnel/profile?personnel_id=789"
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
}
```
@@ -2656,9 +3048,12 @@ curl "https://platform.lensmor.com/external/personnel/profile?personnel_id=789"
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | Public LinkedIn profile URL when available. |
| `companyName` | Associated company or exhibitor. |
-| `sourceType` | Source context, such as exhibitor-derived data. |
+| `sourceType` | Normalized source labels such as `exhibitor`, `social`, and `visitors`. |
| `email` | Email address if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Current contact access state for this API key owner. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `eventCount` | Number of associated events. |
## Email access
@@ -2731,8 +3126,9 @@ curl "https://platform.lensmor.com/external/personnel/events?personnel_id=789&pa
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 5,
@@ -2753,7 +3149,9 @@ curl "https://platform.lensmor.com/external/personnel/events?personnel_id=789&pa
| `dateStart`, `dateEnd` | Event date range. |
| `venue`, `city`, `region`, `country` | Location metadata. |
| `exhibitorCount` | Known exhibitor count when available. |
-| `image`, `dataSource` | Media and source metadata. |
+| `sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `image` | Event image URL when available. |
+| `dataSource` | Always `Lensmor` for public event records. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
## Error responses
@@ -2820,9 +3218,12 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
"seniorityLevel": "vp",
"linkedinUrl": "https://www.linkedin.com/in/jane-smith/",
"companyName": "Acme Retail Systems",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
},
"events": {
"items": [
@@ -2840,8 +3241,9 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
+ "sponsorMatchStarred": 0,
"image": null,
- "dataSource": "database"
+ "dataSource": "Lensmor"
}
],
"total": 1,
@@ -2860,9 +3262,14 @@ curl "https://platform.lensmor.com/external/personnel/events/by-linkedin?linkedi
| `personnel` | Matched person record using the standard contact-style response shape. |
| `personnel.id` | Personnel identifier for profile and email unlock workflows. |
| `personnel.email` | Email if already unlocked for the caller; otherwise `null`. |
+| `personnel.phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `personnel.contactUnlockStatus` | Current contact access state. |
+| `personnel.phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `personnel.eventCount` | Number of associated events. |
| `events` | Paginated event results connected to the matched person. |
| `events.items[].eventId` | Public event identifier for event-scoped follow-up calls. |
+| `events.items[].sponsorMatchStarred` | Sponsor Match starred state (`0` or `1`). |
+| `events.items[].dataSource` | Always `Lensmor` for public event records. |
| `events.total`, `events.page`, `events.pageSize`, `events.totalPages`, `events.hasMore` | Pagination metadata. |
## URL normalization
@@ -2993,9 +3400,29 @@ Use `POST /external/personnel/generate-outreach-message` when you want to:
This endpoint is asynchronous. Each personnel item may create a background task. Use [Get outreach message detail](/api-reference/personnel/get-outreach-message-detail) to retrieve the generated content once processing completes.
- Channels must be one or more of: `email`, `linkedin_message`. You can request both simultaneously.
+ Channels must be one or more of: `email`, `linkedin_message`. When requesting LinkedIn output, use `linkedin_message_types` to choose `inmail`, `connection_note`, and/or `connected_message`.
+## Request body
+
+| Name | Required | Type | Notes |
+| --- | --- | --- | --- |
+| `personnel_ids` | Yes | string[] | Maximum `50` personnel IDs. |
+| `event_id` | Yes | string | Supports external `eventId` or internal event ID. |
+| `channels` | Yes | string[] | One or both of `email`, `linkedin_message`. |
+| `linkedin_message_types` | No | string[] | LinkedIn variants: `inmail`, `connection_note`, `connected_message`. |
+| `outreach_purpose` | No | string | Purpose or offer context for personalization. |
+
+```json
+{
+ "personnel_ids": ["789"],
+ "event_id": "139574",
+ "channels": ["email", "linkedin_message"],
+ "linkedin_message_types": ["inmail", "connection_note"],
+ "outreach_purpose": "Invite the contact to a product demo"
+}
+```
+
## Credit behavior
Outreach message generation consumes credits per personnel record processed.
@@ -3027,6 +3454,8 @@ Use `GET /external/personnel/outreach` when:
## Notes
- Returns the most recent outreach generation result for the given personnel and event combination.
+- `messages.linkedin` can contain separate `inmail`, `connection_note`, and `connected_message` objects. InMail can include `subject` and `body`; the other variants contain `body`.
+- `messages.linkedin_message` remains available as the legacy single-message field when present.
- Re-generating outreach for the same personnel + event will overwrite previous content.
---
@@ -3093,11 +3522,14 @@ curl "https://platform.lensmor.com/external/contacts/search?company_name=Acme&pe
"seniorityLevel": "vp",
"linkedinUrl": "https://linkedin.com/in/jane",
"companyName": "Acme",
- "sourceType": "exhibitor",
+ "sourceType": ["exhibitor"],
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": null,
- "linkedinActivityStatus": null
+ "linkedinActivityStatus": null,
+ "eventCount": 3
}
],
"total": 1,
@@ -3118,9 +3550,12 @@ curl "https://platform.lensmor.com/external/contacts/search?company_name=Acme&pe
| `department`, `seniorityLevel` | Normalized role metadata when available. |
| `linkedinUrl` | LinkedIn profile URL when available. |
| `companyName` | Matched company name. |
-| `sourceType` | Source context for the contact record. |
+| `sourceType` | Normalized source labels for the contact record. Values can include `exhibitor`, `social`, and `visitors`. |
| `email` | Email if already unlocked for the caller; otherwise `null`. |
+| `phone` | Phone number if already unlocked for the caller; otherwise `null`. |
| `contactUnlockStatus` | Contact access state for this API key owner. |
+| `phoneUnlockStatus` | `ready`, `processing`, `failed`, or `null`. |
+| `eventCount` | Number of associated events when available. |
| `linkedinActivity` | Always `null` on this endpoint. Only [Personnel list](/api-reference/personnel/list-event-personnel) populates LinkedIn activity data. |
| `linkedinActivityStatus` | Always `null` on this endpoint. |
| `total`, `page`, `pageSize`, `totalPages`, `hasMore` | Pagination metadata. |
@@ -3161,7 +3596,7 @@ This endpoint is intentionally asynchronous because email unlock can involve mul
- 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.
+ A `201 Created` response means the request was processed. `status: "accepted"` means a task was created; `status: "success"` means there was no chargeable work to enqueue. Neither state guarantees that every submitted personnel record will return an email.
## Endpoint
@@ -3206,9 +3641,11 @@ curl -X POST "https://platform.lensmor.com/external/contacts/unlock" \
| Field | Description |
| --- | --- |
-| `status` | Initial task acceptance state. Usually `accepted` when the task was created. |
-| `task_id` | Identifier used to poll task status. Store this value. |
-| `job_id` | Alias for the background job identifier. It currently matches `task_id`. |
+| `status` | `accepted` when an asynchronous task was created, or `success` when there was no chargeable work to enqueue. |
+| `task_id` | Identifier used to poll task status. Present only when `status` is `accepted`. |
+| `job_id` | Alias for `task_id`. Present only when `status` is `accepted`. |
+| `skipped_personnel_ids` | Personnel IDs omitted from the unlock task, when applicable. |
+| `skipped_detail` | Optional grouped reasons for skipped IDs, including invalid, missing, unlinked, already unlocked, or currently unlocking records. |
## Polling pattern
@@ -3366,7 +3803,7 @@ Stop polling when `taskStatus` is `completed` or `failed`. If some items remain
Source: /api-reference/contacts/unlock-contact-phone-numbers
-Start an asynchronous job to unlock phone numbers for one or more personnel records.
+Submit a request to unlock phone numbers for one or more personnel records. Chargeable work creates an asynchronous task; a request with no chargeable work completes immediately without a task.
Use this endpoint after finding contacts from personnel or contact search results when you need direct phone numbers rather than email addresses.
@@ -3374,26 +3811,33 @@ Use this endpoint after finding contacts from personnel or contact search result
Use `POST /external/contacts/unlock-phone` when your integration needs verified phone numbers for outbound calling or SMS workflows.
-This endpoint is asynchronous. The create call returns a task identifier; poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) until the job reaches a terminal state.
+When the response has `status: "accepted"`, the create call returns a task identifier. Poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) until that job reaches a terminal state. When the response has `status: "success"`, no task was created and there is nothing to poll.
Phone unlock is a separate workflow from email unlock. Use [Unlock contact emails](/api-reference/contacts/unlock-contact-emails) for email addresses.
- A `201 Created` response means the task was accepted. Always inspect item-level results before counting a contact phone as delivered.
+ A `201 Created` response means the request was processed. `status: "accepted"` means a task was created; `status: "success"` means there was no chargeable work to enqueue. Neither state guarantees that every submitted personnel record will return a phone number.
## Credit behavior
-Phone unlock costs credits per chargeable contact.
+Phone unlock costs `150` credits per chargeable contact.
- Already unlocked contacts are not charged again.
- Insufficient balance returns `402 Payment Required`.
- The API rejects batches larger than 100 personnel IDs.
+If there is no chargeable work to enqueue, the endpoint can return `status: "success"` without `task_id` or `job_id`. The response can also include `skipped_personnel_ids` and `skipped_detail` so the caller can explain invalid, missing, unlinked, already unlocked, or currently unlocking records.
+
+## Response handling
+
+- For `status: "accepted"`, use `task_id` (or its `job_id` alias) to poll the phone unlock task.
+- For `status: "success"`, do not poll. Inspect `skipped_personnel_ids` and `skipped_detail` to explain why no task was needed.
+
## Notes
-- This endpoint creates an asynchronous unlock job. Poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) with `task_id`.
+- Only poll [Get phone unlock task](/api-reference/contacts/get-phone-unlock-task) when `status` is `accepted` and `task_id` is present.
- Do not create duplicate unlock tasks while a prior task is still pending or processing.
- See [Credits and access](/concepts/credits-and-access) for shared credit behavior.
@@ -3515,6 +3959,7 @@ curl -X POST "https://platform.lensmor.com/external/profile-matching/actions/app
"priceUpper": "2395",
"eventType": "conference_led",
"quality": "Standard",
+ "sponsorMatchStarred": 0,
"sourceTags": [],
"categories": [
{
@@ -3606,7 +4051,8 @@ Each event item includes the full event detail shape:
| `verified`, `future`, `historic` | integer | Status flags (0 or 1). |
| `historicEvent` | string \| null | Name of the recurring event series. |
| `image` | string \| null | Event image URL. |
-| `dataSource` | string | Source label (e.g. `"Lensmor"`, `"local_import"`). |
+| `sponsorMatchStarred` | integer | Sponsor Match starred state (`0` or `1`). |
+| `dataSource` | string | Always `Lensmor` for public event records. |
| `exhibitorCount`, `personnelCount` | integer \| null | Known counts when available. |
| `eventTypes` | array | Event type metadata. |
| `createTime`, `updateTime` | string | Unix timestamps in milliseconds as strings. |
@@ -4031,7 +4477,7 @@ Use the identifier returned by the matching resource endpoint:
Source: /concepts/credits-and-access
-Some Lensmor API operations are credit-aware. Read-only discovery endpoints are available with a valid API key, while unlock and reverse-lookup workflows may consume credits.
+Some Lensmor API operations are credit-aware. Most read-only discovery endpoints are available with a valid API key, while unlock and paid company-name lookup workflows may consume credits.
API credits are shared with the Lensmor SaaS app. The API key belongs to a subscribed Lensmor user account, and API usage draws from the same credit pool as product usage in the app.
@@ -4041,9 +4487,11 @@ Use `GET /external/credits/balance` to inspect the API key owner's current balan
The balance is split into:
- `subscriptionBalance` — credits from the active subscription period.
+- `addonBalance` — credits from active one-time add-on purchases.
- `giftBalance` — non-expiring gift credits.
- `totalBalance` — total currently available credits.
- `resetAt` — the earliest active subscription credit expiration timestamp in milliseconds, or `null`.
+- `addonExpireAt` — the latest active add-on credit expiration timestamp in milliseconds, or `null`.
## Preview and full access
Event-scoped exhibitor and personnel lists can return preview results before the event is unlocked.
@@ -4055,15 +4503,18 @@ When an event is locked, list responses may include `semantics` metadata describ
- whether more results require unlocking the event
- which unlock action and credit amount applies
-Unlocking an event grants full access to event-scoped exhibitor and personnel results for that event.
+Base event unlock grants full access to event-scoped exhibitor and personnel results. Visitor access is a separate data layer that can be unlocked after base event access or together through the full-access endpoint.
## Credit-consuming operations
| Operation | Typical cost | Notes |
| --- | --- | --- |
-| Event unlock | `2000` credits | Unlocks full event access when contacts are available and the event is not already unlocked. |
+| Base event unlock | `2000` credits | Unlocks full exhibitor and personnel access when contacts are available and the event is not already unlocked. |
+| Event visitor unlock | `3000` credits | Requires base event access, an active subscription, and available visitor data. Repeated unlocks are free. |
+| Full event access | `0`–`5000` credits | Atomically unlocks missing base and visitor layers: `2000` for base event access plus `3000` for visitor access. |
| 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. |
+| Contact phone unlock | `150` credits per chargeable contact | Batch unlock creates an asynchronous task. Already unlocked contacts are not charged again. |
+| Exhibitor company search | `50` credits for a non-empty result | `POST /external/exhibitors/search-by-company-name` charges only when at least one exhibitor is returned. Empty results are free. |
+| Exhibitor event search | `50` credits for a non-empty result | `POST /external/exhibitors/search-events` charges only when at least one event is returned. Empty results are free. |
| 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.
@@ -4093,7 +4544,7 @@ Use a confirmation step for operations that can spend credits:
Call `GET /external/credits/balance` and show the current total balance.
- Display the known cost from this page, endpoint docs, or `semantics.unlock`.
+ Call precheck for access-dependent actions and display the returned cost, or use the known cost from this page or `semantics.unlock`.
Call the unlock or reverse-lookup endpoint only after the user confirms.
@@ -4113,7 +4564,7 @@ Supported values are:
- `api` — default customer API usage.
- `agent` — agent-driven usage.
-If omitted or unrecognized, the API records the call as `api`. This header is currently used by event unlock, contact unlock, phone unlock, exhibitor event search, LinkedIn activity unlock, and outreach message generation workflows.
+If omitted or unrecognized, the API records the call as `api`. This header is currently used by base event unlock, visitor unlock, full event access, contact unlock, phone unlock, exhibitor company search, exhibitor event search, LinkedIn activity unlock, and outreach message generation workflows.
## Handling `402 Payment Required`
@@ -4133,7 +4584,10 @@ Recommended behavior:
Check available subscription and gift credits.
- Unlock full event-scoped coverage.
+ Unlock base event-scoped exhibitor and personnel coverage.
+
+
+ Unlock missing base and visitor access layers in one atomic call.
Start an asynchronous email unlock task.
diff --git a/openapi.json b/openapi.json
index dd14ecf..e2295dc 100644
--- a/openapi.json
+++ b/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Lensmor API",
- "version": "0.23.1",
+ "version": "0.24.0",
"description": "Lensmor Event Intelligence API for event discovery, exhibitor research, personnel lookup, credits, and profile matching.",
"license": {
"name": "Lensmor Terms of Service",
@@ -56,7 +56,7 @@
"Credits"
],
"summary": "Get credits balance",
- "description": "Return subscription, gift, and total credits for the authenticated API key owner without spending credits.",
+ "description": "Return subscription, add-on, gift, and total credits for the authenticated API key owner without spending credits.",
"operationId": "getExternalCreditsBalance",
"responses": {
"200": {
@@ -67,10 +67,13 @@
"$ref": "#/components/schemas/CreditBalance"
},
"example": {
- "totalAmount": 35050,
- "totalBalance": 30050,
+ "totalAmount": 45050,
+ "totalBalance": 38050,
"subscriptionAmount": 35000,
"subscriptionBalance": 30000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
"giftAmount": 50,
"giftBalance": 50,
"resetAt": 1714521600000
@@ -251,6 +254,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"image": "https://example.com/ces-image.png",
"dataSource": "Lensmor"
}
@@ -316,6 +320,7 @@
"region": "Nevada",
"country": "United States",
"exhibitorCount": 3259,
+ "sponsorMatchStarred": 0,
"dataSource": "Lensmor"
}
}
@@ -401,7 +406,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"summary": {
"attendeeCount": 40000,
@@ -458,7 +464,8 @@
"region": "NY",
"country": "United States",
"exhibitorCount": 950,
- "dataSource": "database"
+ "sponsorMatchStarred": 0,
+ "dataSource": "Lensmor"
},
"score": 8.1,
"recommendation": "recommended",
@@ -583,7 +590,7 @@
"Events"
],
"summary": "Unlock event access",
- "description": "Unlock full exhibitor and personnel access for an event; a first unlock can consume 2,000 credits and repeated unlocks are idempotent.",
+ "description": "Unlock base event access for complete exhibitor and personnel results. A first unlock can consume 2,000 credits, repeated unlocks are idempotent, and the separate visitor data layer is not included.",
"operationId": "unlockEvent",
"parameters": [
{
@@ -635,6 +642,143 @@
}
}
},
+ "/external/events/{id}/visitors/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock event visitor access",
+ "description": "Unlock visitor records for an event that already has base event access. The first successful visitor unlock consumes 3,000 credits, requires an active subscription and available visitor data, and repeated unlocks are idempotent.",
+ "operationId": "unlockEventVisitors",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Visitor access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventVisitorUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "creditsSpent": 3000,
+ "balanceAfter": {
+ "totalAmount": 45050,
+ "totalBalance": 35050,
+ "subscriptionAmount": 35000,
+ "subscriptionBalance": 27000,
+ "addonAmount": 10000,
+ "addonBalance": 8000,
+ "addonExpireAt": 1784851200000,
+ "giftAmount": 50,
+ "giftBalance": 50,
+ "resetAt": 1785542400000
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
+ "/external/events/{id}/full-access/unlock": {
+ "post": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Unlock full event access",
+ "description": "Atomically unlock base event access and visitor access, charging only for missing access layers: 2,000 credits for base event access and 3,000 credits for visitor access, up to 5,000 credits total.",
+ "operationId": "unlockEventFullAccess",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/EventIdPath"
+ },
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Full event access unlock result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EventFullAccessUnlockResult"
+ },
+ "example": {
+ "success": true,
+ "alreadyUnlocked": false,
+ "eventUnlocked": true,
+ "visitorUnlocked": true,
+ "visitorSkipped": false,
+ "totalCreditsUsed": 5000,
+ "balanceAfter": {
+ "subscriptionBalance": 25000,
+ "permanentBalance": 50,
+ "totalBalance": 33050,
+ "unlimited": false
+ },
+ "event": {
+ "id": "26855",
+ "eventId": "26855",
+ "name": "CES 2025"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
"/external/exhibitors/list": {
"get": {
"tags": [
@@ -838,8 +982,13 @@
"Exhibitors"
],
"summary": "Search exhibitors by company name",
- "description": "Run a precision-first exhibitor lookup by company name; a successful search attempt can consume 50 credits.",
+ "description": "Run a precision-first exhibitor lookup by company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchExhibitorsByCompanyName",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/CallSource"
+ }
+ ],
"requestBody": {
"$ref": "#/components/requestBodies/CompanyNameBody"
},
@@ -880,6 +1029,9 @@
"401": {
"$ref": "#/components/responses/Unauthorized"
},
+ "402": {
+ "$ref": "#/components/responses/PaymentRequired"
+ },
"429": {
"$ref": "#/components/responses/RateLimited"
}
@@ -892,7 +1044,7 @@
"Exhibitors"
],
"summary": "Search events by exhibitor company name",
- "description": "Find events associated with a company name; a successful reverse event search can consume 50 credits.",
+ "description": "Find events associated with a company name. A non-empty result consumes 50 credits; an empty result does not consume credits.",
"operationId": "searchEventsByExhibitorCompanyName",
"parameters": [
{
@@ -900,7 +1052,7 @@
}
],
"requestBody": {
- "$ref": "#/components/requestBodies/CompanyNameBody"
+ "$ref": "#/components/requestBodies/SearchEventsByCompanyNameBody"
},
"responses": {
"201": {
@@ -908,7 +1060,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/EventPage"
+ "$ref": "#/components/schemas/EventReverseSearchPage"
},
"example": {
"items": [
@@ -919,7 +1071,15 @@
"dateStart": "2026-03-15",
"dateEnd": "2026-03-18",
"city": "Las Vegas",
- "country": "United States"
+ "country": "United States",
+ "sponsorMatchStarred": 1,
+ "dataSource": "Lensmor",
+ "matchedExhibitors": [
+ {
+ "id": "456",
+ "companyName": "Acme Retail Systems"
+ }
+ ]
}
],
"total": 1,
@@ -1135,7 +1295,12 @@
"title": "VP of Retail Technology",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
],
"total": 42,
@@ -1193,7 +1358,12 @@
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"companyName": "Acme Retail Systems",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5,
+ "outreachMessageStatus": null,
+ "outreachMessageChannels": []
}
}
}
@@ -1317,7 +1487,10 @@
"fullName": "Jane Smith",
"linkedinUrl": "https://linkedin.com/in/jane-smith",
"email": null,
- "contactUnlockStatus": "locked"
+ "phone": null,
+ "contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
+ "eventCount": 5
},
"events": {
"items": [
@@ -1473,9 +1646,12 @@
"title": "VP Marketing",
"companyName": "Acme",
"email": null,
+ "phone": null,
"contactUnlockStatus": "locked",
+ "phoneUnlockStatus": null,
"linkedinActivity": null,
- "linkedinActivityStatus": null
+ "linkedinActivityStatus": null,
+ "eventCount": 3
}
],
"total": 1,
@@ -1550,11 +1726,11 @@
},
"responses": {
"201": {
- "description": "Unlock task accepted",
+ "description": "Unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1814,7 +1990,7 @@
"Contacts"
],
"summary": "Unlock contact phone numbers",
- "description": "Start an asynchronous phone unlock task that consumes credits per chargeable contact.",
+ "description": "Start an asynchronous phone unlock task that consumes 150 credits per chargeable contact.",
"operationId": "unlockContactPhones",
"parameters": [
{
@@ -1839,11 +2015,11 @@
},
"responses": {
"201": {
- "description": "Phone unlock task accepted",
+ "description": "Phone unlock submission accepted, or completed with no chargeable work",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactUnlockAccepted"
+ "$ref": "#/components/schemas/ContactUnlockSubmission"
},
"example": {
"status": "accepted",
@@ -1955,6 +2131,10 @@
"email",
"linkedin_message"
],
+ "linkedin_message_types": [
+ "inmail",
+ "connection_note"
+ ],
"outreach_purpose": "Book a product demo"
}
}
@@ -2043,7 +2223,16 @@
],
"messages": {
"email": "Hi Jane, I would like to compare notes before Retail Technology Expo.",
- "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?"
+ "linkedin_message": "Hi Jane, are you open to a quick conversation before the event?",
+ "linkedin": {
+ "inmail": {
+ "subject": "Retail Technology Expo",
+ "body": "Hi Jane, I would like to compare notes before the event."
+ },
+ "connection_note": {
+ "body": "Hi Jane, would be glad to connect before the event."
+ }
+ }
},
"outreach_purpose": "Book a product demo",
"tone": "professional",
@@ -2273,6 +2462,51 @@
}
}
},
+ "SearchEventsByCompanyNameBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "company_name"
+ ],
+ "properties": {
+ "company_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "sponsor_match_starred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Filter to events whose Sponsor Match starred state is 0 or 1. Omit the field to include both states."
+ },
+ "page": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "pageSize": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20
+ }
+ }
+ },
+ "example": {
+ "company_name": "Acme",
+ "sponsor_match_starred": 1,
+ "page": 1,
+ "pageSize": 20
+ }
+ }
+ }
+ },
"ProfileMatchingBody": {
"required": true,
"content": {
@@ -2459,6 +2693,9 @@
"totalBalance",
"subscriptionAmount",
"subscriptionBalance",
+ "addonAmount",
+ "addonBalance",
+ "addonExpireAt",
"giftAmount",
"giftBalance",
"resetAt"
@@ -2476,6 +2713,18 @@
"subscriptionBalance": {
"type": "number"
},
+ "addonAmount": {
+ "type": "number"
+ },
+ "addonBalance": {
+ "type": "number"
+ },
+ "addonExpireAt": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
"giftAmount": {
"type": "number"
},
@@ -2572,6 +2821,14 @@
"null"
]
},
+ "sponsorMatchStarred": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "Sponsor Match starred state returned by Lensmor."
+ },
"image": {
"type": [
"string",
@@ -2579,10 +2836,8 @@
]
},
"dataSource": {
- "type": [
- "string",
- "null"
- ]
+ "type": "string",
+ "const": "Lensmor"
}
},
"additionalProperties": true
@@ -2714,6 +2969,79 @@
},
"additionalProperties": true
},
+ "MatchedExhibitorSummary": {
+ "type": "object",
+ "required": [
+ "id",
+ "companyName"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "companyName": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventReverseSearchItem": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventItem"
+ },
+ {
+ "type": "object",
+ "required": [
+ "matchedExhibitors"
+ ],
+ "properties": {
+ "matchedExhibitors": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MatchedExhibitorSummary"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "EventReverseSearchPage": {
+ "type": "object",
+ "required": [
+ "items",
+ "total",
+ "page",
+ "pageSize",
+ "totalPages",
+ "hasMore"
+ ],
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/EventReverseSearchItem"
+ }
+ },
+ "total": {
+ "type": "integer"
+ },
+ "page": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "totalPages": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
"EventFitScore": {
"type": "object",
"required": [
@@ -3065,6 +3393,131 @@
}
}
},
+ "EventUnlockIdentity": {
+ "type": "object",
+ "required": [
+ "id",
+ "eventId",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "eventId": {
+ "type": "string"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "EventVisitorUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "creditsSpent",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "creditsSpent": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreditBalance"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
+ "EventFullAccessUnlockResult": {
+ "type": "object",
+ "required": [
+ "success",
+ "alreadyUnlocked",
+ "eventUnlocked",
+ "visitorUnlocked",
+ "visitorSkipped",
+ "totalCreditsUsed",
+ "balanceAfter",
+ "event"
+ ],
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "alreadyUnlocked": {
+ "type": "boolean"
+ },
+ "eventUnlocked": {
+ "type": "boolean"
+ },
+ "visitorUnlocked": {
+ "type": "boolean"
+ },
+ "visitorSkipped": {
+ "type": "boolean"
+ },
+ "totalCreditsUsed": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "balanceAfter": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscriptionBalance",
+ "permanentBalance",
+ "totalBalance",
+ "unlimited"
+ ],
+ "properties": {
+ "subscriptionBalance": {
+ "type": "number"
+ },
+ "permanentBalance": {
+ "type": "number"
+ },
+ "totalBalance": {
+ "type": "number"
+ },
+ "unlimited": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "event": {
+ "$ref": "#/components/schemas/EventUnlockIdentity"
+ }
+ }
+ },
"ExhibitorBase": {
"type": "object",
"required": [
@@ -3438,12 +3891,24 @@
]
},
"sourceType": {
+ "type": "array",
+ "description": "Normalized Lensmor source labels for this person. Possible values are exhibitor, social, and visitors.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "exhibitor",
+ "social",
+ "visitors"
+ ]
+ }
+ },
+ "email": {
"type": [
"string",
"null"
]
},
- "email": {
+ "phone": {
"type": [
"string",
"null"
@@ -3452,6 +3917,18 @@
"contactUnlockStatus": {
"type": "string"
},
+ "phoneUnlockStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "ready",
+ "processing",
+ "failed",
+ null
+ ]
+ },
"linkedinActivity": {
"type": [
"object",
@@ -3477,6 +3954,26 @@
"string",
"null"
]
+ },
+ "eventCount": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "outreachMessageStatus": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Current outreach generation state when this endpoint includes outreach metadata."
+ },
+ "outreachMessageChannels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Generated or requested outreach channels when outreach metadata is present."
}
}
},
@@ -3611,25 +4108,90 @@
},
"additionalProperties": true
},
- "ContactUnlockAccepted": {
+ "ContactUnlockSubmission": {
"type": "object",
"required": [
- "status",
- "task_id",
- "job_id"
+ "status"
],
"properties": {
"status": {
"type": "string",
- "const": "accepted"
+ "enum": [
+ "accepted",
+ "success"
+ ],
+ "description": "accepted when an asynchronous task was created; success when there was no chargeable work to enqueue."
},
"task_id": {
- "type": "string"
+ "type": "string",
+ "description": "TaskCenter identifier. Present when status is accepted."
},
"job_id": {
- "type": "string"
+ "type": "string",
+ "description": "Alias of task_id. Present when status is accepted."
+ },
+ "skipped_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "skipped_detail": {
+ "type": "object",
+ "properties": {
+ "invalid_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "missing_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlinked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "already_unlocked_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "unlocking_personnel_ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
- }
+ },
+ "oneOf": [
+ {
+ "properties": {
+ "status": {
+ "const": "accepted"
+ }
+ },
+ "required": [
+ "task_id",
+ "job_id"
+ ]
+ },
+ {
+ "properties": {
+ "status": {
+ "const": "success"
+ }
+ }
+ }
+ ]
},
"ContactUnlockTask": {
"type": "object",
@@ -3733,7 +4295,10 @@
"enum": [
"query_event_personnel",
"unlock_event_contacts",
+ "unlock_event_visitors",
+ "unlock_event_full_access",
"unlock_contact_emails",
+ "unlock_contact_phones",
"search_exhibitor_events",
"others"
]
@@ -3991,6 +4556,18 @@
]
}
},
+ "linkedin_message_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "inmail",
+ "connection_note",
+ "connected_message"
+ ]
+ },
+ "description": "LinkedIn message variants to generate when linkedin_message is requested."
+ },
"outreach_purpose": {
"type": "string"
}
@@ -4074,6 +4651,38 @@
"string",
"null"
]
+ },
+ "linkedin": {
+ "type": "object",
+ "properties": {
+ "inmail": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connection_note": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ },
+ "connected_message": {
+ "type": "object",
+ "properties": {
+ "body": {
+ "type": "string"
+ }
+ }
+ }
+ }
}
}
},
diff --git a/scripts/test_sync_public_assets.py b/scripts/test_sync_public_assets.py
index 565f1e2..feebf9d 100644
--- a/scripts/test_sync_public_assets.py
+++ b/scripts/test_sync_public_assets.py
@@ -14,6 +14,38 @@
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "sync-public-assets.py"
+EXPECTED_PUBLIC_OPERATIONS = {
+ ("GET", "/external/credits/balance"),
+ ("POST", "/external/actions/precheck"),
+ ("GET", "/external/events/list"),
+ ("GET", "/external/events/{id}"),
+ ("GET", "/external/events/brief"),
+ ("POST", "/external/events/fit-score"),
+ ("POST", "/external/events/rank"),
+ ("POST", "/external/events/{id}/unlock"),
+ ("POST", "/external/events/{id}/visitors/unlock"),
+ ("POST", "/external/events/{id}/full-access/unlock"),
+ ("GET", "/external/exhibitors/list"),
+ ("POST", "/external/exhibitors/search"),
+ ("POST", "/external/exhibitors/search-by-company-name"),
+ ("POST", "/external/exhibitors/search-events"),
+ ("GET", "/external/exhibitors/profile"),
+ ("GET", "/external/exhibitors/events"),
+ ("GET", "/external/personnel/list"),
+ ("GET", "/external/personnel/profile"),
+ ("GET", "/external/personnel/events"),
+ ("GET", "/external/personnel/events/by-linkedin"),
+ ("POST", "/external/personnel/unlock-linkedin-activity"),
+ ("POST", "/external/personnel/generate-outreach-message"),
+ ("GET", "/external/personnel/outreach"),
+ ("GET", "/external/contacts/search"),
+ ("POST", "/external/contacts/unlock"),
+ ("GET", "/external/contacts/unlock-tasks/{taskId}"),
+ ("POST", "/external/contacts/unlock-phone"),
+ ("GET", "/external/contacts/unlock-phone-tasks/{taskId}"),
+ ("POST", "/external/profile-matching/actions/apply-recommended-events/paged"),
+ ("GET", "/external/profile-matching/recommendations/exhibitors"),
+}
def load_sync_module():
@@ -46,10 +78,19 @@ def test_public_openapi_has_descriptions_and_examples(self) -> None:
if method.lower() in self.sync.HTTP_METHODS:
operations.append((method.upper(), path, operation))
- self.assertEqual(len(operations), 28)
+ self.assertEqual(
+ {(method, path) for method, path, _ in operations},
+ EXPECTED_PUBLIC_OPERATIONS,
+ )
self.assertFalse(
any(path.startswith("/external/integrations/") for _, path, _ in operations)
)
+ self.assertFalse(
+ any(path.startswith("/external/agent-files/") for _, path, _ in operations)
+ )
+ self.assertFalse(
+ any(path.startswith("/external/debug/") for _, path, _ in operations)
+ )
self.assertTrue(spec["info"]["license"]["name"].strip())
self.assertEqual(
[tag["name"] for tag in spec["tags"] if not tag.get("description", "").strip()],
@@ -94,6 +135,195 @@ def test_public_openapi_has_descriptions_and_examples(self) -> None:
self.assertEqual(missing_request_examples, [])
self.assertEqual(missing_response_examples, [])
+ def test_company_search_contract_matches_current_credit_rule(self) -> None:
+ spec = json.loads(self.sync.OPENAPI_SOURCE.read_text(encoding="utf-8"))
+ self.assertEqual(spec["info"]["version"], "0.24.0")
+
+ company_search = spec["paths"]["/external/exhibitors/search-by-company-name"]["post"]
+ self.assertIn("non-empty", company_search["description"])
+ self.assertIn("402", company_search["responses"])
+
+ def test_event_search_and_event_item_contracts(self) -> None:
+ spec = json.loads(self.sync.OPENAPI_SOURCE.read_text(encoding="utf-8"))
+ schemas = spec["components"]["schemas"]
+ request_bodies = spec["components"]["requestBodies"]
+
+ event_search = spec["paths"]["/external/exhibitors/search-events"]["post"]
+ self.assertEqual(
+ event_search["requestBody"]["$ref"],
+ "#/components/requestBodies/SearchEventsByCompanyNameBody",
+ )
+ self.assertEqual(
+ event_search["responses"]["201"]["content"]["application/json"]["schema"]["$ref"],
+ "#/components/schemas/EventReverseSearchPage",
+ )
+ event_search_fields = request_bodies["SearchEventsByCompanyNameBody"]["content"][
+ "application/json"
+ ]["schema"]["properties"]
+ self.assertEqual(event_search_fields["sponsor_match_starred"]["enum"], [0, 1])
+
+ event_fields = schemas["EventItem"]["properties"]
+ self.assertEqual(event_fields["dataSource"]["const"], "Lensmor")
+ self.assertEqual(event_fields["sponsorMatchStarred"]["enum"], [0, 1])
+
+ event_examples = [
+ spec["paths"]["/external/events/list"]["get"]["responses"]["200"]["content"]
+ ["application/json"]["example"]["items"][0],
+ spec["paths"]["/external/events/{id}"]["get"]["responses"]["200"]["content"]
+ ["application/json"]["example"]["event"],
+ spec["paths"]["/external/events/brief"]["get"]["responses"]["200"]["content"]
+ ["application/json"]["example"]["event"],
+ spec["paths"]["/external/events/fit-score"]["post"]["responses"]["201"]
+ ["content"]["application/json"]["example"]["event"],
+ ]
+ for example in event_examples:
+ with self.subTest(event=example["eventId"]):
+ self.assertIn(example["sponsorMatchStarred"], [0, 1])
+ self.assertEqual(example["dataSource"], "Lensmor")
+
+ def test_contact_unlock_and_outreach_contracts(self) -> None:
+ spec = json.loads(self.sync.OPENAPI_SOURCE.read_text(encoding="utf-8"))
+ schemas = spec["components"]["schemas"]
+
+ contact_fields = schemas["ContactItem"]["properties"]
+ self.assertTrue(
+ {
+ "phone",
+ "phoneUnlockStatus",
+ "eventCount",
+ "outreachMessageStatus",
+ "outreachMessageChannels",
+ }.issubset(contact_fields)
+ )
+
+ unlock_submission = schemas["ContactUnlockSubmission"]
+ self.assertEqual(unlock_submission["properties"]["status"]["enum"], ["accepted", "success"])
+ self.assertIn("skipped_personnel_ids", unlock_submission["properties"])
+ self.assertIn("skipped_detail", unlock_submission["properties"])
+
+ outreach_request_fields = schemas["OutreachMessageRequest"]["properties"]
+ self.assertEqual(
+ outreach_request_fields["linkedin_message_types"]["items"]["enum"],
+ ["inmail", "connection_note", "connected_message"],
+ )
+ outreach_message_fields = schemas["OutreachDetail"]["properties"]["messages"][
+ "properties"
+ ]
+ self.assertIn("linkedin", outreach_message_fields)
+
+ def test_v024_access_and_balance_contracts(self) -> None:
+ spec = json.loads(self.sync.OPENAPI_SOURCE.read_text(encoding="utf-8"))
+ schemas = spec["components"]["schemas"]
+
+ action_types = set(
+ schemas["ActionPrecheckRequest"]["properties"]["action_type"]["enum"]
+ )
+ self.assertTrue(
+ {
+ "unlock_event_visitors",
+ "unlock_event_full_access",
+ "unlock_contact_phones",
+ }.issubset(action_types)
+ )
+
+ visitor_unlock = spec["paths"]["/external/events/{id}/visitors/unlock"]["post"]
+ full_access = spec["paths"]["/external/events/{id}/full-access/unlock"]["post"]
+ self.assertIn("3,000", visitor_unlock["description"])
+ for expected_cost in ("2,000", "3,000", "5,000"):
+ self.assertIn(expected_cost, full_access["description"])
+ self.assertTrue(
+ {"400", "401", "402", "404", "409", "429"}.issubset(
+ visitor_unlock["responses"]
+ )
+ )
+ self.assertTrue(
+ {"400", "401", "402", "404", "409", "429"}.issubset(
+ full_access["responses"]
+ )
+ )
+
+ self.assertEqual(
+ set(schemas["EventFullAccessUnlockResult"]["required"]),
+ {
+ "success",
+ "alreadyUnlocked",
+ "eventUnlocked",
+ "visitorUnlocked",
+ "visitorSkipped",
+ "totalCreditsUsed",
+ "balanceAfter",
+ "event",
+ },
+ )
+
+ credit_fields = schemas["CreditBalance"]["properties"]
+ self.assertTrue(
+ {"addonAmount", "addonBalance", "addonExpireAt"}.issubset(credit_fields)
+ )
+
+ source_type = schemas["ContactItem"]["properties"]["sourceType"]
+ self.assertEqual(source_type["type"], "array")
+ self.assertEqual(
+ source_type["items"]["enum"],
+ ["exhibitor", "social", "visitors"],
+ )
+
+ for page_name in (
+ "unlock-event-visitor-access.mdx",
+ "unlock-full-event-access.mdx",
+ ):
+ page = (
+ ROOT / "api-reference-backup" / "events" / page_name
+ ).read_text(encoding="utf-8")
+ self.assertIn("Actions precheck", page)
+ self.assertIn("idempotency", page.lower())
+
+ def test_event_prose_uses_the_public_event_shape(self) -> None:
+ event_pages = [
+ ROOT / "api-reference-backup" / "events" / "list.mdx",
+ ROOT / "api-reference-backup" / "events" / "detail.mdx",
+ ROOT / "api-reference-backup" / "exhibitors" / "events.mdx",
+ ROOT
+ / "api-reference-backup"
+ / "profile-matching"
+ / "actions-apply-recommended-events-paged.mdx",
+ ROOT / "api-reference-backup" / "personnel" / "events.mdx",
+ ROOT / "api-reference-backup" / "personnel" / "events-by-linkedin.mdx",
+ ]
+
+ for path in event_pages:
+ with self.subTest(path=path.relative_to(ROOT)):
+ page = path.read_text(encoding="utf-8")
+ self.assertIn("sponsorMatchStarred", page)
+ self.assertIn("Always `Lensmor`", page)
+ self.assertNotIn('"dataSource": "database"', page)
+ self.assertNotIn('"local_import"', page)
+
+ def test_personnel_prose_matches_endpoint_specific_shapes(self) -> None:
+ profile_page = (
+ ROOT / "api-reference-backup" / "personnel" / "profile.mdx"
+ ).read_text(encoding="utf-8")
+ linkedin_events_page = (
+ ROOT / "api-reference-backup" / "personnel" / "events-by-linkedin.mdx"
+ ).read_text(encoding="utf-8")
+
+ self.assertNotIn("outreachMessageStatus", profile_page)
+ self.assertNotIn("outreachMessageChannels", profile_page)
+ for field in ("phone", "phoneUnlockStatus", "eventCount"):
+ with self.subTest(field=field):
+ self.assertIn(field, linkedin_events_page)
+
+ def test_phone_unlock_prose_handles_accepted_and_no_work_success(self) -> None:
+ page = (
+ ROOT / "api-reference-backup" / "contacts" / "unlock-phone.mdx"
+ ).read_text(encoding="utf-8")
+
+ self.assertIn('status: "accepted"', page)
+ self.assertIn('status: "success"', page)
+ self.assertIn("there is nothing to poll", page)
+ self.assertIn("Only poll", page)
+ self.assertNotIn("A `201 Created` response means the task was accepted", page)
+
def test_agent_integrations_are_explicitly_excluded(self) -> None:
inventory = (
ROOT / "docs" / "endpoint-inventory" / "external-api-endpoints.md"
@@ -106,7 +336,7 @@ def test_llms_full_covers_every_navigation_entry(self) -> None:
outputs = self.sync.build_outputs()
llms_full = outputs[self.sync.LLMS_FULL].decode("utf-8")
sources = [
- line.removeprefix("Source: ")
+ line[len("Source: "):]
for line in llms_full.splitlines()
if line.startswith("Source: ")
]