Nexus Partner API
Give your product the ability to understand who should meet, who can help, and why.
Nexus turns the people already inside your platform into intelligent search, personalized recommendations and ranked connections. You send the context you already have. Nexus returns the right people, in order, with a clear reason for every result.
Your product. Your experience. Nexus intelligence underneath it.
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 }'Build it with your coding agent
Copy a complete implementation brief for Claude Code, Cursor or another coding agent. It includes the API contract, endpoint behavior and examples. Add your API key and start building.
Overview
Your product already has the people. Nexus gives it the intelligence to understand how they should connect.
Search your entire network in plain English. Find the best people for any individual. Rank mentors against founders, investors against companies, members against opportunities, or any other relationship your product needs to understand.
Nexus does the hard part: understanding context, relevance, complementary needs, expertise and connection potential. Your application decides where that intelligence appears and what happens next.
Instead of building and maintaining your own people-search infrastructure, embedding pipeline, ranking logic, matching models and explanation layer, you make an API call. Nexus returns your own IDs, ranked by relevance, with a plain-English reason explaining each result.
1
Give Nexus context
Sync the people and profile information you want Nexus to understand.
2
Ask Nexus
Search in natural language, or ask Nexus to find the strongest connections for a particular person.
3
Use the intelligence anywhere
Receive ranked results using your own IDs, scores and explanations. Display them, trigger introductions, create recommendations, power workflows or apply your own business rules.
Three endpoints cover the whole surface.
Context in
Sync
Give Nexus the people and context it should understand.
Intelligence out
Search
Ask a question in plain English and get the most relevant people back.
Intelligence out
Match
Give Nexus one person and get the people they should most likely connect with.
You do not move your product into Nexus. Your system remains the source of truth for your users and your business logic.
Nexus maintains the intelligence required to search and match the profile information you choose to provide. Every result comes back using the external_id from your system, so it maps directly into the product you already have.
Your partner dataset is private to your integration. It does not appear on public or guest Nexus pages, it is not included in any Nexus community search, and it is not used in Nexus campaigns. It can only be read with your partner API key.
Nexus returns your identifiers, never its own internal IDs.
Everything you sync is member-visible by design
Use cases
These are examples, not product constraints. Nexus returns connection intelligence through the API; how you use it is entirely up to you.
Turn any directory, marketplace or network into intelligent search. A user can type:
Nexus understands the intent, ranks the people in your network and explains why each result is relevant. One /search request. No search syntax for the user to learn.
On your side, render each result straight from display and reason, and show display_score as the badge. Because the reason is generated from the person's own synced fields, it doubles as the card's subtitle.
Give every user recommendations that feel hand-picked. Pass Nexus a person and receive the strongest connections for them, ranked by relevance and accompanied by an explanation of why the two people should connect.
Use it for member recommendations, founder and investor discovery, internal experts, networking, customer communities, professional marketplaces or advisor discovery. It can power a recommendation feed, a profile sidebar, a weekly email, an introduction workflow or anything else you build.
One /match request with the person's id in whole-dataset mode returns the top limit people with a reason for each.
Power structured matching without reducing people to tags and filters.
Your application controls hard eligibility rules such as geography, availability, language, programme or capacity. Nexus handles the harder question: out of everyone who is eligible, who is actually the best match? Send Nexus the candidate IDs and it ranks the pairings and explains each one.
Technically: filter to the eligible ids on your side, send them as candidate_external_ids to /match, apply your own threshold to relevance, and write the outcome to your own matches table. Nothing is stored on the Nexus side, so you can re-run whenever the roster changes.
A new user should not have to spend weeks discovering who matters. As soon as somebody joins your product, Nexus can identify the people most relevant to them and explain why.
Use those results for "people you should meet", suggested mentors, relevant investors, recommended experts, useful peers or a personalized welcome email.
A newly synced member is available for matching immediately after a successful sync, so onboarding recommendations can happen in the same workflow: sync, then /match, in one job.
Search tells you who matches a question. Match tells you who should connect with a person. The Nexus knowledge graph goes one level higher: it helps your product understand the network itself.
Nexus maps topics, industries, organizations, expertise, needs and opportunities across the people you provide, then returns that intelligence using your own identifiers. Use it to build network analytics, identify unmet demand, surface opportunities or automatically create high-value introductions.
Contact Nexus to enable knowledge graph access for your partner account.
Authentication
Every Partner API request uses your server-side partner API key in the x-partner-api-key header. Nexus provides the key when your partner account is provisioned and delivers it out of band. Keep it server-side and you are ready to make authenticated requests. 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
The Partner API is deliberately small. Three POST endpoints, JSON request bodies, JSON responses and one authentication header.
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
Give Nexus the people and context its connection intelligence should reason over.
Create, update or remove up to 200 people per request using the stable external_id from your own system. Once a record is successfully indexed, it is immediately available to search and match.
/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 context about what this person is working on, looking for, able to help with or hoping to accomplish. One of the highest-value fields you can provide for connection 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
Ask your network a question in plain English.
Send Nexus what the user is looking for and receive the most relevant people in ranked order. Queries can be broad, "people who understand enterprise healthcare sales", or highly specific, "climate fintech founders in Nairobi".
Nexus interprets the intent, applies supported structured constraints and ranks the strongest results. Each result can include a plain-English explanation of why that person matched.
/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. Responses are faster, but the model pass that re-orders the top results by how directly each profile serves the query is skipped with it, so you get the engine's order. |
| 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
Find the right connections for any person.
Give Nexus one person, the subject, and it ranks the people most relevant to them. Nexus looks at the context available for both sides of the relationship and returns the strongest pairings, with a relevance score and a clear explanation of why each connection makes sense.
Search answers "who fits this request?". Match answers "who should this person meet?". You can let Nexus rank across the full dataset, or pass a specific eligible group and have Nexus rank only those people.
/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
}candidate_external_ids omitted). Let Nexus find the strongest connections from everyone available. Nexus ranks every eligible member of your dataset, excluding the subject, and returns the top limit. unmatched is always empty in this mode.candidate_external_ids supplied). Apply your own eligibility rules first, then let Nexus rank everyone who remains. Nexus ranks exactly that list. Every requested id appears either in results or in unmatched with a code. Nothing is silently dropped, and limit does not apply.Ranking is deterministic: the same people with the same synced data produce the same order. Ties are broken in a stable order.
# 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){
"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
Consistent error handling across every endpoint.
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
Everything below is what you need to know about timeouts, freshness, identifiers and retries when running the Partner API in production.
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.
Put Nexus connection intelligence inside your product.
Your users already have people they should know. Nexus gives your product the intelligence to find them. Search, recommend and match people natively inside your own experience without building the connection intelligence stack yourself.
Get a partner API key and we will provision your account, confirm your endpoints and help you make your first call.
Or hand it to your coding agent
Copy a self-contained implementation prompt for Claude Code, Cursor or another coding agent. It includes the complete API contract, typed client requirements, error handling and a smoke test. You provide the API key. It can handle the rest.