Authorization: Bearer, the method Refiner recommends; the api_key= parameter and HTTP Basic both still work but are never used here, because a credential in a URL is logged by every proxy on the way.
Things to know before you wire it up:
- Two operations can create a contact.
users.identifycreates the contact on first sight, andusers.trackandsegments.syncalso create one when no match exists. All three are markedwritefor that reason, even though a track sounds like an append. users.identifyforwards every unrecognised key as a trait. Refiner infers a trait’s type from the value: a key ending in_atis parsed as a date, an unquoted integer is a number, an unquotedtrue/falsea boolean, and anything else a string. Nothing is coerced here, so send the type you mean —{"a_number": 42}rather than{"a_number": "42"}. Passaccount: { id: … }to group contacts under one account.responses.storeneeds a survey that already exists.form_uuidis required, but the survey may be empty and unpublished, so a dummy survey works as a container for imported history. Put answers at the top level keyed by question identifier, and setdateon a historical import or everything lands on the import day. Refiner de-duplicates by default;prevent_duplicates: falseturns that off, which is why the operation is never retried.responses.listincludes survey views, not only answers. With the defaultinclude: completedonly answers come back;alladds dismissed views, whosedatavalues andcompleted_atarenull. Reading a null score as a zero would quietly corrupt an average.- Timestamps in a response are
YYYY-MM-DD HH:MM:SS, not ISO 8601. That is what Refiner sends, so parse them accordingly; the report endpoints use ISO 8601 with microseconds instead. forms.archiveis an archive, not a delete. The route isDELETE /forms, but the docs say “archive” and the survey still appears underlist: 'all_with_archived'.forms.duplicatemakes a new draft, andforms.publishtakes one live.contacts.deletealso removes the contact’s survey responses. When an erasure request must leave the responses intact, the docs suggest overwriting the sensitive traits withusers.identifyinstead.segments.syncdistinguishes adding from removing by HTTP method, not by a body field. The plugin sendsPOSTto add andDELETEto remove, viaremove: true. Only manual segments can be changed this way; a computed one will not accept it.- Two lists page by cursor.
responses.listandcontacts.listanswerpagination.next_page_cursor; pass it back aspage_cursor. Refiner recommends the cursor over numeric pages above 10,000 entries.forms.list,forms.historyandsegments.listtakepageandpage_lengthonly.page_lengthcaps at 1,000. - Filters are bracketed keys.
response_data[nps]=9,contact_data[country]=Franceandaccount_data[plan]=proare built explicitly; when several keys are given, all must match. - Rate limits and errors. 4,000 requests a minute and 1,000 per 15 seconds per key, plus 100 a minute per user for the endpoints that take an identifier. A
429carriesRetry-After, which the plugin honours. A409on identify-user means another request for that user is in flight and clears on its own, so the plugin waits it out.
- 17 typed API operations
Setup
1
Install
2
Add the plugin
corsair.ts
multiTenancy: true, scope calls with corsair.withTenant(id). See Quick start for KEK + Hub keys, and Multi-tenancy for account isolation.3
Choose authentication
- API Key (Recommended)
No setup required yet. When you make your first request as a tenant, Corsair prompts for the API key.More: API Key
4
Connect a tenant
Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see Connect / OAuth.
Example API calls
Read the account usageWhat’s next
API reference
Every
refiner.api.* operation with input and output types.Connect / OAuth
createLink, Hub delivery, and tenant connect flows.
Use with agents
Expose this plugin’s operations as MCP tools.