Nexus Partner API
The Partner API lets your platform keep a member dataset on Nexus and query it. You sync people in. Nexus ranks them against a free-text search or against another person, and returns the ranked list with a plain-English reason for every result. No search infrastructure, no ranking code and no Nexus-branded surface on your side.
curl -X POST https://www.nexus.app/api/partner/{partner}/search \ -H "x-partner-api-key: $NEXUS_PARTNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "climate fintech founders in Nairobi", "limit": 5 }'A self-contained brief with this page's URL and the full contract. The only step it cannot do is get your key.
Overview
Your system stays the source of truth for who your members are. Nexus holds a synced copy of the fields you choose to send, builds the search and matching intelligence on top of it, and hands the ranked answers back with your own identifiers. Three endpoints cover the whole surface.
Write
Sync
Batch upsert of member records, keyed by your stable external_id. Re-sending a record is safe.
Read
Search
A free-text query, optionally fenced to a list of your ids, returns ranked members with reasons.
Read
Match
One member as the subject, returns the people most relevant to them with a reason for each pairing.
A partner dataset is sealed. It is not visible on any public or guest Nexus page, it is not included in any Nexus community search, and it is not used in Nexus campaigns. The only way to read it is with your API key. Nexus never returns its own internal identifiers: theexternal_id you send is the identifier you get back, end to end.
Everything you sync is member-visible by design
Authentication
Every request carries your partner key in the x-partner-api-key header. Nexus issues the key when your partner tenant is provisioned and delivers it out of band. Keep it on your server. There is no query-string or body alternative, and no self-service key management.
curl -X POST https://www.nexus.app/api/partner/{partner}/match \ -H "x-partner-api-key: $NEXUS_PARTNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject_external_id": "acme-1042" }'The key is compared in constant time. A missing or incorrect key returns 401 with the standard error envelope and the code unauthorized. Authentication is checked before anything else, so an unauthenticated call never consumes budget and never touches your data.
Conventions
All endpoints are served over HTTPS from https://www.nexus.app. Every endpoint is a POST with a JSON body and returns JSON. Send Content-Type: application/json. Responses are computed per request and never cached.
Every path contains a partner segment, shown here as {partner}. It is the slug assigned to your tenant at provisioning and is confirmed alongside your key. Paths are otherwise identical for every partner.
Every non-2xx response has the same shape: an error object with a machine-readable code and a human-readable message. Branch on the code, never on the message.
{
"error": {
"code": "missing_query",
"message": "query is required."
}
}Budget responses add one top-level field, retry_after_seconds, the number of seconds until the daily budget resets.
{
"error": {
"code": "search_cap_exceeded",
"message": "Daily search budget reached."
},
"retry_after_seconds": 18342
}Each endpoint evaluates a request in the same order. Knowing it explains which error you see first.
401 immediately.429.400.500 with partner_group_missing.Budget is consumed before validation
400 still counts against that day's budget. Validate on your side before retrying in a loop.A body that is not valid JSON is treated as an empty body, so you receive the endpoint's "required field missing" error rather than a parse error. Unknown fields in a request are ignored. String inputs are trimmed before validation, so a value of only whitespace counts as missing.
Sync is idempotent: replaying the same batch produces the same dataset. Search and match are stateless reads. Nexus stores nothing per call beyond an anonymous daily counter, so re-running either is always safe.
Endpoint 1
Create, update or remove members in your Nexus dataset in batches of up to 200 records, each keyed by your stable external_id.
/api/partner/{partner}/members/syncThe body is an object with one key, members, holding 1 to 200 record objects. Each record has the fields below. Anything not listed is ignored.
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| external_id | string | Required | Non-empty after trimming. Must be stable across calls. | Your identifier for the person. It is the upsert key and the identifier returned by every read endpoint. |
| string | Required | Must look like an email address. Stored trimmed and lower-cased. | Used to link a record to a person Nexus already knows when no record with this external_id exists yet. | |
| first_name | string | Optional | None | Given name. |
| last_name | string | Optional | None | Family name. |
| title | string | Optional | Truncated to 100 characters with a warning. | Job title or role. |
| organization | string | Optional | Truncated to 100 characters with a warning. | Company or organisation name. Returned as display.organization. |
| about | string | Optional | None | Free-text bio. No length cap. |
| match_context | string | Optional | Truncated to 2,000 characters with a warning. | Free text describing what this person needs, offers or is working on. The highest-leverage field for search and match quality. |
| skills | string[] | Optional | Deduplicated case-insensitively, capped at 20 with a warning. A non-array is treated as empty. | Expertise tags. Casing is normalised for display. |
| interests | string[] | Optional | Deduplicated case-insensitively, capped at 20 with a warning. A non-array is treated as empty. | Interest tags. Casing is normalised for display. |
| city | string | Optional | None | City. Combined with country to form display.location. |
| country | string | Optional | None | Country. |
| string | Optional | Must be a linkedin.com/in/ profile URL, otherwise dropped with a warning. | LinkedIn profile URL. | |
| website | string | Optional | Normalised to an absolute URL; https:// is added when missing. | Personal or company website. |
| status | "active" | "removed" | Optional | Defaults to "active". Any other value rejects the record. | "removed" takes the person out of your dataset and out of every search and match result. |
{
"members": [
{
"external_id": "acme-1042",
"email": "priya.raman@example.com",
"first_name": "Priya",
"last_name": "Raman",
"title": "Co-founder & CTO",
"organization": "Northwind Grid",
"about": "Building battery storage for rural micro-grids.",
"match_context": "Pre-seed. Wants a power-electronics advisor and hardware angel intros.",
"skills": ["embedded systems", "power electronics"],
"interests": ["climate tech", "hardware fundraising"],
"city": "Nairobi",
"country": "Kenya",
"linkedin": "https://www.linkedin.com/in/priya-raman-example",
"website": "northwindgrid.example.com",
"status": "active"
},
{
"external_id": "acme-0871",
"email": "tomas.lindqvist@example.com",
"status": "removed"
}
]
}Nexus first looks for a member in your dataset with the same external_id. If none exists, it looks for a person with the same email. What happens next depends on what it finds.
created.linked_existing_user: true.In both of the last two cases the result is created when the person was not previously in your dataset and updated when they were. After every record in the batch has been applied, each created or updated member is re-indexed before the response is sent, so a successful result is searchable and matchable as soon as the call returns.
A status: "removed" record deletes the person's membership in your dataset. Removal is idempotent: removing someone who is not in your dataset still reports removed. Removing an external_id and email Nexus has never seen is rejected with not_found. Re-sending a removed person as active restores them.
Each record is applied in its own transaction. A record that fails is reported as rejected and the rest of the batch continues.
{
"batch_id": "6f1c9c2e-3b7a-4d0e-9a1b-2c3d4e5f6a7b",
"results": [
{
"external_id": "acme-1042",
"status": "created",
"embedding": "refreshed"
},
{
"external_id": "acme-0871",
"status": "removed",
"embedding": "skipped"
}
],
"summary": {
"created": 1,
"updated": 0,
"removed": 1,
"rejected": 0
}
}| Field | Type | Meaning |
|---|---|---|
| batch_id | string (UUID) | Identifier of this batch. Quote it when contacting support about a sync. |
| results | object[] | One entry per record, in the same order as the request. |
| results[].external_id | string | null | Your identifier. Null when the record had no usable external_id (invalid_payload or missing_external_id). |
| results[].status | "created" | "updated" | "removed" | "rejected" | What happened to the record. |
| results[].code | string | Present only when status is rejected. One of the rejection codes below. |
| results[].message | string | Present only on rejected records. Human-readable explanation. |
| results[].warnings | string[] | Present only when a field was truncated or dropped. The record was still applied. |
| results[].linked_existing_user | true | Present only when the record was linked to a person who also exists elsewhere on Nexus. |
| results[].embedding | "refreshed" | "failed" | "skipped" | Present on created, updated and removed records. refreshed means the member is live in search and match. failed means the index update did not complete: re-send the record to retry. skipped is the value for removals. |
| summary | object | Counts of created, updated, removed and rejected records in this batch. |
Two more shapes a result entry can take:
{
"external_id": "acme-2210",
"status": "updated",
"linked_existing_user": true,
"warnings": [
"title truncated to 100 characters",
"linkedin dropped: not a linkedin.com/in/ profile URL"
],
"embedding": "failed"
}
{
"external_id": "acme-3305",
"status": "rejected",
"code": "invalid_email",
"message": "email \"not-an-address\" is not a valid address."
}A rejected record never fails the batch. The HTTP status is still 200 and the other records are applied.
invalid_payloadThe record is not a JSON object.missing_external_idexternal_id is absent or empty.missing_emailemail is absent or empty.invalid_emailemail does not look like an address.invalid_statusstatus is something other than "active" or "removed".not_foundA removal named a person Nexus has never seen.internal_errorThe record failed to process. Its transaction was rolled back; the rest of the batch continued.| Status | Code | When | Body |
|---|---|---|---|
| 401 | unauthorized | Missing or incorrect API key. | { "error": { "code": "unauthorized",
"message": "Invalid or missing x-partner-api-key." } } |
| 429 | sync_cap_exceeded | Daily sync budget reached. | { "error": { "code": "sync_cap_exceeded",
"message": "Daily sync budget reached." },
"retry_after_seconds": 18342 } |
| 400 | invalid_batch | Body is not { members: [...] }, or the array is empty or longer than 200. | { "error": { "code": "invalid_batch",
"message": "Body must be { members: [...] } with 1 to 200 records." } } |
| 500 | partner_group_missing | Your partner tenant is not provisioned. | { "error": { "code": "partner_group_missing",
"message": "Partner group is not provisioned." } } |
| 500 | internal_error | The batch failed as a whole. Records already committed stay committed. | { "error": { "code": "internal_error",
"message": "Sync failed; quote the batch_id in support.",
"batch_id": "6f1c9c2e-3b7a-4d0e-9a1b-2c3d4e5f6a7b" } } |
Endpoint 2
Rank the members of your dataset against a free-text query and get back the top results with a reason for each.
/api/partner/{partner}/search| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| query | string | Required | Non-empty after trimming. | Natural-language query. Structured constraints in the text, such as a location, are detected and applied as hard filters. |
| limit | integer | Optional | 1 to 36. Default 36. Out-of-range values are clamped; decimals are truncated; omitted or unparseable values fall back to the default. | Maximum number of results. |
| include_reasons | boolean | Optional | Default true. Only an explicit false disables reasons. | Set false to skip reason generation when you only need the ranking. Responses are faster. |
| candidate_external_ids | string[] | Optional | When present: 1 to 1,000 non-empty strings. Duplicates are removed. | Fence the search to this set of your ids. Ids that do not resolve are ignored. Omit to search your whole dataset. |
{
"query": "climate fintech founders in Nairobi",
"limit": 5,
"include_reasons": true,
"candidate_external_ids": ["acme-1042", "acme-1187", "acme-1201", "acme-1344"]
}The candidate fence is the mechanism for scoping: run your own deterministic filter first, send the eligible ids, and Nexus ranks only inside that set. If none of the supplied ids resolve, the response is 200 with an empty results array.
{
"query": "climate fintech founders in Nairobi",
"applied_filters": [
{
"id": "location:city:Nairobi",
"type": "location",
"field": "city",
"operator": "eq",
"value": "Nairobi",
"values": ["nairobi"],
"label": "City: Nairobi"
}
],
"results": [
{
"external_id": "acme-1187",
"type": "person",
"rank": 1,
"relevance": 0.8123,
"display_score": 9,
"reason": "Amara runs a pay-as-you-go solar financing startup in Nairobi and lists climate finance as a core skill.",
"reason_source": "llm",
"display": {
"name": "Amara Okonkwo",
"title": "Founder & CEO",
"organization": "SunLedger",
"location": "Nairobi, Kenya",
"tags": ["Climate Finance", "Mobile Payments", "Angel Investing"],
"avatar_url": "https://cdn.example.com/avatars/acme-1187.png"
}
},
{
"external_id": "acme-1042",
"type": "person",
"rank": 2,
"relevance": 0.7409,
"display_score": 8,
"reason": "Matches on Nairobi and climate tech.",
"reason_source": "evidence",
"display": {
"name": "Priya Raman",
"title": "Co-founder & CTO",
"organization": "Northwind Grid",
"location": "Nairobi, Kenya",
"tags": ["Embedded Systems", "Power Electronics", "Climate Tech", "Hardware Fundraising"],
"avatar_url": "https://cdn.example.com/avatars/acme-1042.png"
}
}
]
}| Field | Type | Meaning |
|---|---|---|
| query | string | The query as received, trimmed. |
| applied_filters | object[] | Structured constraints Nexus detected in the query and applied as hard filters. Each has id, type ("location" or "custom_field") and a display label, plus optional field, operator, value and values. Empty when nothing was detected. |
| results | object[] | Ranked results, best first. Empty when nothing matched. |
| results[].external_id | string | Your identifier for the member. |
| results[].type | "person" | Always "person". |
| results[].rank | integer | 1-based position in this response. |
| results[].relevance | number | 0 to 1, four decimal places. Higher means a stronger match. Comparable between results in the same response, not across responses. |
| results[].display_score | integer | 1 to 10. The grade Nexus shows in its own product for this result. Use this one when you display a score. |
| results[].reason | string | One sentence explaining why this member matched. Present when include_reasons is not false. |
| results[].reason_source | "llm" | "evidence" | llm for a model-written sentence, available for up to the first 15 results. evidence for a deterministic sentence built from the matching fields. |
| results[].display | object | Everything needed to render a card without a second call: name, title, organization, location, tags (skills followed by interests) and avatar_url. Every value except tags can be null. |
{
"query": "climate fintech founders in Nairobi",
"applied_filters": [],
"results": []
}Reasons never cause an error. If the model-written reasons are unavailable, or your daily reasons budget is exhausted, every result still carries a deterministic sentence with reason_source: "evidence". Ranking is never affected by reasons.
| Status | Code | When | Body |
|---|---|---|---|
| 401 | unauthorized | Missing or incorrect API key. | { "error": { "code": "unauthorized",
"message": "Invalid or missing x-partner-api-key." } } |
| 429 | search_cap_exceeded | Daily search budget reached. | { "error": { "code": "search_cap_exceeded",
"message": "Daily search budget reached." },
"retry_after_seconds": 18342 } |
| 400 | missing_query | query is absent, not a string, or empty after trimming. | { "error": { "code": "missing_query",
"message": "query is required." } } |
| 400 | invalid_candidates | candidate_external_ids is present but not an array of 1 to 1,000 non-empty strings. | { "error": { "code": "invalid_candidates",
"message": "candidate_external_ids must be 1 to 1000 non-empty strings when provided." } } |
| 500 | partner_group_missing | Your partner tenant is not provisioned. | { "error": { "code": "partner_group_missing",
"message": "Partner group is not provisioned." } } |
| 500 | internal_error | The search failed. Safe to retry. | { "error": { "code": "internal_error",
"message": "Search failed." } } |
Endpoint 3
Rank people in your dataset against one member, the subject, and get back the strongest pairings with a reason for each.
/api/partner/{partner}/match| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| subject_external_id | string | Required | Non-empty after trimming. Must be a member of your dataset. | The person to find matches for. |
| candidate_external_ids | string[] | Optional | When present: 1 to 200 non-empty strings. Duplicates are removed. | Rank exactly these people. Every id you send is accounted for in the response. Omit to rank your whole dataset. |
| limit | integer | Optional | 1 to 100. Default 36. Clamped, truncated and defaulted the same way as search. | Maximum results when candidate_external_ids is omitted. Ignored when candidates are supplied: the whole list is ranked. |
| include_reasons | boolean | Optional | Default true. Only an explicit false disables reasons. | Set false to skip reason generation. |
{
"subject_external_id": "acme-1042",
"candidate_external_ids": ["acme-1187", "acme-2210", "acme-1042", "acme-9999"],
"include_reasons": true
}{
"subject_external_id": "acme-1042",
"limit": 10
}results or in unmatched with a code. Nothing is silently dropped, and limit does not apply.limit. unmatched is always empty in this mode.Ranking is deterministic: the same people with the same synced data produce the same order. Ties are broken in a stable order.
{
"subject_external_id": "acme-1042",
"results": [
{
"external_id": "acme-1187",
"rank": 1,
"relevance": 0.6934,
"reason": "Both are building climate-focused companies in Nairobi, and Amara's experience raising for hardware-adjacent products speaks to Priya's stated fundraising need.",
"reason_source": "llm",
"display": {
"name": "Amara Okonkwo",
"title": "Founder & CEO",
"organization": "SunLedger",
"location": "Nairobi, Kenya",
"tags": ["Climate Finance", "Mobile Payments", "Angel Investing"],
"avatar_url": "https://cdn.example.com/avatars/acme-1187.png"
}
}
],
"unmatched": [
{ "external_id": "acme-1042", "code": "is_subject" },
{ "external_id": "acme-2210", "code": "no_embedding" },
{ "external_id": "acme-9999", "code": "unknown_external_id" }
]
}| Field | Type | Meaning |
|---|---|---|
| subject_external_id | string | The subject, echoed back trimmed. |
| results | object[] | Ranked candidates, strongest first. The subject never appears. |
| results[].external_id | string | Your identifier for the candidate. |
| results[].rank | integer | 1-based position in this response. |
| results[].relevance | number | 0 to 1, four decimal places. Higher means a stronger pairing. Apply your own minimum before surfacing a match. Nexus ranks, you gate. |
| results[].reason | string | One neutral, third-person sentence naming a checkable fit between the two people. Safe to show to either side. |
| results[].reason_source | "llm" | "overlap" | llm for a model-written sentence, available for up to the first 15 results. overlap for a deterministic sentence built from what the two profiles share. |
| results[].display | object | Same card block as search: name, title, organization, location, tags, avatar_url. |
| unmatched | object[] | Requested candidates that could not be ranked, each with external_id and a code. |
Codes in unmatched:
is_subjectThe candidate is the subject.unknown_external_idNo member of your dataset has this id.not_searchableThe member exists but is not currently eligible to appear in results.no_embeddingThe member has not been indexed yet. Re-sync the record; a successful sync reports embedding: "refreshed".Note the differences from search: match results carry no type and no display_score, and the deterministic reason source is overlap rather than evidence. As with search, reasons degrade to the deterministic sentence and never cause an error.
| Status | Code | When | Body |
|---|---|---|---|
| 401 | unauthorized | Missing or incorrect API key. | { "error": { "code": "unauthorized",
"message": "Invalid or missing x-partner-api-key." } } |
| 429 | match_cap_exceeded | Daily match budget reached. | { "error": { "code": "match_cap_exceeded",
"message": "Daily match budget reached." },
"retry_after_seconds": 18342 } |
| 400 | missing_subject | subject_external_id is absent, not a string, or empty after trimming. | { "error": { "code": "missing_subject",
"message": "subject_external_id is required." } } |
| 400 | invalid_candidates | candidate_external_ids is present but not an array of 1 to 200 non-empty strings. | { "error": { "code": "invalid_candidates",
"message": "candidate_external_ids must be 1 to 200 non-empty strings when provided." } } |
| 404 | subject_not_found | No member of your dataset has the subject id. | { "error": { "code": "subject_not_found",
"message": "No synced member has this external_id." } } |
| 422 | subject_not_embedded | The subject exists but has not been indexed. Re-sync that record. | { "error": { "code": "subject_not_embedded",
"message": "Re-sync this member to generate their embedding." } } |
| 500 | partner_group_missing | Your partner tenant is not provisioned. | { "error": { "code": "partner_group_missing",
"message": "Partner group is not provisioned." } } |
| 500 | internal_error | The match failed. Safe to retry. | { "error": { "code": "internal_error",
"message": "Match failed." } } |
Limits
| Limit | Value | Over the limit |
|---|---|---|
| Records per sync call | 1 to 200 | 400 invalid_batch |
| Tags per list (skills, interests) | 20 | Truncated with a warning |
| title, organization length | 100 characters | Truncated with a warning |
| match_context length | 2,000 characters | Truncated with a warning |
| Search results (limit) | 1 to 36, default 36 | Clamped |
| Search candidate fence | 1 to 1,000 ids | 400 invalid_candidates |
| Match results when candidates omitted (limit) | 1 to 100, default 36 | Clamped |
| Match candidate list | 1 to 200 ids | 400 invalid_candidates |
| Model-written reasons per response | First 15 results | Deterministic sentence for the rest |
Each endpoint has a budget of calls per UTC day. Budgets reset at 00:00 UTC, and a 429 tells you exactly how long to wait in retry_after_seconds. Budgets are counted per endpoint for your tenant, not per IP address.
| Budget | Calls per UTC day | When exhausted |
|---|---|---|
| Sync | 500 | 429 sync_cap_exceeded |
| Search | 5,000 | 429 search_cap_exceeded |
| Match | 5,000 | 429 match_cap_exceeded |
| Search reasons (model-written) | 5,000 | Reasons fall back to evidence sentences. No error. |
| Match reasons (model-written) | 5,000 | Reasons fall back to overlap sentences. No error. |
The values above are the current defaults.
Errors
HTTP-level errors use the envelope described under Conventions. Per-record sync rejections arrive inside a 200 body and are listed under Sync.
| Status | Code | When | Body |
|---|---|---|---|
| 401 | unauthorized | Any endpoint. Missing or incorrect x-partner-api-key. | { "error": { "code": "unauthorized", "message": "..." } } |
| 429 | sync_cap_exceeded | Sync. Daily budget reached. | { "error": { ... }, "retry_after_seconds": 18342 } |
| 429 | search_cap_exceeded | Search. Daily budget reached. | { "error": { ... }, "retry_after_seconds": 18342 } |
| 429 | match_cap_exceeded | Match. Daily budget reached. | { "error": { ... }, "retry_after_seconds": 18342 } |
| 400 | invalid_batch | Sync. members is missing, empty or longer than 200. | { "error": { "code": "invalid_batch", "message": "..." } } |
| 400 | missing_query | Search. query is missing or empty. | { "error": { "code": "missing_query", "message": "..." } } |
| 400 | missing_subject | Match. subject_external_id is missing or empty. | { "error": { "code": "missing_subject", "message": "..." } } |
| 400 | invalid_candidates | Search or match. candidate_external_ids present but malformed or over the cap. | { "error": { "code": "invalid_candidates", "message": "..." } } |
| 404 | subject_not_found | Match. The subject is not in your dataset. | { "error": { "code": "subject_not_found", "message": "..." } } |
| 422 | subject_not_embedded | Match. The subject has not been indexed yet. | { "error": { "code": "subject_not_embedded", "message": "..." } } |
| 500 | partner_group_missing | Any endpoint. Tenant not provisioned. | { "error": { "code": "partner_group_missing", "message": "..." } } |
| 500 | internal_error | Any endpoint. Unexpected failure. Sync includes batch_id. | { "error": { "code": "internal_error", "message": "..." } } |
Retry guidance: 429 after retry_after_seconds; 500 on search and match immediately, since both are stateless; 500 on sync by re-sending the batch, since every record is idempotent and records that already committed are unaffected. Never retry a 400, 401, 404 or 422 without changing the request.
Operations
A sync call indexes every created or updated member before it responds, one at a time, so a full batch of 200 can take minutes. The sync handler may run for up to 300 seconds; set your client timeout at least that high, and prefer smaller batches when you need faster feedback. Search and match handlers may run for up to 60 seconds; typical responses are far quicker, and sending include_reasons: false removes the slowest step.
A member whose sync result reports embedding: "refreshed" is live in search and match the moment the call returns. A member reporting embedding: "failed" keeps their previous index state until you re-send the record. A removed member disappears from both surfaces immediately.
Stable external_id values are load-bearing. Nexus matches by id first and by email second, so an id that changes on your side creates a second person on the next sync. If ids can change, say so before go-live.
No Nexus internal identifiers, no email addresses, no raw synced text other than what appears in display and the reason sentence, and no member who is not in your dataset. Search and match responses only ever contain people carrying one of your external ids.
Quote the batch_id from a sync response when reporting a sync problem. It lets Nexus find the exact batch, its per-record outcomes and its timing.
Patterns
Four integration patterns that sit directly on these three endpoints. Each one is a few dozen lines on your side.
Wire a text input to /search. Render each result straight from display and reason, show display_score as the badge, and you have semantic people-search in your product with no index to run. Because the reason sentence is generated from the member's own synced fields, it doubles as the card's subtitle.
Whoever you are pairing, advisors with companies, buyers with suppliers, new members with established ones, your eligibility logic already exists: language, capacity, programme, geography. Keep it. Filter to the eligible ids on your side, send them as candidate_external_ids, and let /match rank and explain the pairings. Apply your own threshold to relevance, write the outcome to your own matches table, and re-run whenever the roster changes. Nothing is stored on the Nexus side, so the engine is always safe to call again.
# 1. Your eligibility rules pick who is allowed to be matched.eligible_ids = pool.filter(p => p.accepting && p.languages.includes(person.language)) # 2. Nexus ranks exactly that list and explains each pairing.POST /api/partner/{partner}/match{ "subject_external_id": person.id, "candidate_external_ids": eligible_ids } # 3. Your threshold decides what gets surfaced. Store the outcome on your side.matches = response.results.filter(r => r.relevance >= YOUR_MIN_SCORE).slice(0, 3)If your product has segments, cohorts or saved lists, each one is a candidate fence. Pass the list's ids to /search and the query ranks only inside it: "investors who have done hardware" over the 200 people in your investor list, not over everyone. The fence accepts up to 1,000 ids per call.
Sync a new member, then call /match with their id and a small limit in whole-dataset mode. The top few results, each with a neutral one-sentence reason, are a welcome email or a first-session screen that took one extra request to build. Because a successful sync is live immediately, both calls can run in the same job.
Ready to integrate?
Partner tenants and API keys are provisioned by the Nexus team. Get in touch and we will set up your dataset, confirm your endpoint paths and agree budgets.
The prompt tells a coding agent such as Claude Code or Cursor to build a typed client, error handling and a smoke test against this contract, and leaves the key as the only manual step.