Skip to main content
Persona
Persona gives you a hosted KYC flow: author an inquiry template, POST /api/v1/inquiries, hand the subject a verification link, and read the result (ID scan, selfie, database checks) back as an inquiry status or webhook. It does that job well, and the API is mature. But it is also a separate vendor account:
  • KYC lives behind its own API key, its own templates and themes, and its own dashboard — disconnected from wherever the verified person’s company, cards, phone, and secrets live.
  • An inquiry’s reference-id is a string tag back to a row in your user table. Persona knows the person passed KYC; it does not know that same person is the responsible party who then incorporates the company and owns its virtual card.
  • “This founder cleared KYC — now what is allowed to happen on their behalf?” is answered in Persona for verification, and in unrelated systems for formation, banking, and cards. The cleared identity has no shared accountability with the rest of the company’s footprint.
Naive’s verification primitive gives the same capability — hosted KYC for one or more company founders, with real-time status — but the person you verify is a governed identity, not a tagged inquiry:
  • The tenant user whose founders you KYC is the same identity that then forms the LLC, and owns its cards, phone number, email inboxes, and vault secrets.
  • Verified PII (SSN, DOB, address) is pulled from the encrypted identity vault at formation time — it never lands in your database.
  • When every member passes, ready_for_formation flips to true — KYC is structurally a step toward incorporation, not an isolated pass/fail you have to wire to a formation vendor yourself.
This guide maps Persona’s Inquiries API to Naive’s, shows the smallest working swap, and is explicit about what does not map yet.
Persona is a trademark of its owner, used here for identification only. No endorsement or affiliation is implied.
Scope this migration before you start. Naive’s verification primitive is founder / responsible-party KYC ahead of company formation — the people who own and form the company.
  • If you use Persona to verify founders before incorporation, this is a clean migration and the consolidation win is real.
  • If you use Persona to KYC your end users / customers at scale — arbitrary subjects, custom templates, AML/watchlist Reports, manual-review Cases — that does not map to Naive today. Keep Persona for that. See what doesn’t map yet.
Tested against: the Persona REST API (base https://api.withpersona.com/api/v1, header Persona-Version: 2025-10-272025-12-08 also valid — hosted flow at https://inquiry.withpersona.com, docs snapshot June 2026), and the Naive Node SDK @usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1, docs snapshot June 2026).Version notes:
  • Persona’s officially featured SDKs are client-side (persona-react / persona-web / mobile); the server-side integration is REST, which is what the “before” code below uses. There is no first-party server SDK to swap out.
  • Persona verifies one subject per inquiry against a template you author (itmpl_…). Naive verifies a set of company members in one verification.start call, and requires ownership_percentage to sum to 100, exactly one role: "primary", and exactly one is_responsible_party: true. This is formation-shaped — see gaps.
  • Persona is webhook-first (inquiry.completed / inquiry.approved / inquiry.declined). Naive’s public webhook surface advertises only email.received and approval.resolved today, so KYC status comes from the validation-token complete endpoint (instant for the primary) + polling GET /v1/verification/:id. Treat GET /v1/webhooks/event-types as the source of truth before depending on any KYC event.

Concept map

PersonaNaiveNotes
Authorization: Bearer <persona_api_key> + Persona-Version headernew Naive({ apiKey }), then naive.forUser(id)Server-side key in both; Naive scopes KYC to a tenant identity
Inquiry Template (itmpl_…) you design + themeFixed, provider-managed KYC playbookNo template authoring or theming — see gaps
POST /api/v1/inquiriesone subjectverification.start({ members: [...] })one or more foundersNaive verifies a set of members with ownership %, not one arbitrary subject
data.attributes.fields prefill (name_first, name_last, birthdate)member first_name, last_name, email, phone_numberPrefill / member identity
reference-id — string tag to a row in your DBthe tenant user (forUser(id)) + member recordsIdentity is structural, not a string
POST /inquiries/{id}/generate-one-time-linkmeta.one-time-link (or inquiry.withpersona.com/verify?inquiry-id=…)primary_link in the start responsePrimary member’s hosted link returned inline
Email the link to the subject yourselfSecondary members emailed automaticallyNaive sends secondary KYC links for you
Hosted Flow / Embedded Flow (iframe, client SDK)Naive-hosted flow at verify.usenaive.aiHosted only — no embedded SDK config
GET /api/v1/inquiries/{id}data.attributes.statusGET /v1/verification/:id → per-member status + ready_for_formationStatus read; Naive adds the formation gate
Client inquiry-session-token / validationTokenPOST /v1/verification/members/:id/complete { validation_token }Confirm a member instantly without waiting on a webhook
Regenerate a link / resumePOST /v1/verification/members/:id/resendNew session + emailed link
Webhooks inquiry.completed / inquiry.approved / inquiry.declined (recommended)Validation-token complete + poll; no advertised KYC webhookThe biggest difference — see gaps
post-inquiry approve / decline workflowready_for_formation (all members pass)Naive’s “decision” is formation-readiness, not generic decisioning
API Keys + role scopesAccount Kit verification primitive + per-user assignmentKYC is execution-time policy on the identity — see gains
Reports (Watchlist/AML, adverse media, PEP)No equivalent product
Cases (manual-review queue), Accounts, Documents API, Verifications API, TransactionsNo equivalents — see gaps
Workflows / Dynamic Flow, redaction APINot provided

Before / after: the core path

The path that matters for founder KYC is start verification for the people who own the company, hand them a hosted link, then know when they’ve passed. Here it is on both platforms.
// Server-side REST. Persona's featured SDKs are client-side; the server flow is HTTP.
const PERSONA = "https://api.withpersona.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.PERSONA_API_KEY!}`,
  "Persona-Version": "2025-10-27",
  "Content-Type": "application/json",
};

// 1. Create an inquiry against a template you authored (one subject).
const created = await fetch(`${PERSONA}/inquiries`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    data: {
      attributes: {
        "inquiry-template-id": "itmpl_XXXXXXXX",
        "reference-id": dbUser.id, // tag back to YOUR user row
        fields: { name_first: "Alice", name_last: "Smith" },
      },
    },
  }),
}).then((r) => r.json());
const inquiryId = created.data.id; // "inq_..."

// 2. Generate the hosted KYC link and send it to the subject yourself.
const link = await fetch(`${PERSONA}/inquiries/${inquiryId}/generate-one-time-link`, {
  method: "POST",
  headers,
}).then((r) => r.json());
// → link.meta["one-time-link"] : "https://inquiry.withpersona.com/..."

// 3. Read the result (Persona recommends a webhook; polling shown for parity).
const got = await fetch(`${PERSONA}/inquiries/${inquiryId}`, { headers }).then((r) => r.json());
// → got.data.attributes.status : "completed" | "approved" | "declined" | ...
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
const client = naive.forUser(dbUser.id); // same id space as your own users

// 1. Start KYC for the founders (a set, with ownership + responsible party).
//    The SDK exposes verification.start; status/complete are REST endpoints.
const verification = await client.verification.start({
  members: [
    {
      first_name: "Alice",
      last_name: "Smith",
      email: "alice@example.com",
      ownership_percentage: 100,
      role: "primary",
      is_responsible_party: true,
    },
  ],
});
// → verification.id, verification.primary_link (hand to the primary founder),
//   verification.members[].status. Secondary members are emailed automatically.

const nv = {
  Authorization: `Bearer ${process.env.NAIVE_API_KEY!}`,
  "Content-Type": "application/json",
};

// 2. Confirm the primary instantly with the validation token from the hosted flow.
const member = await fetch(
  `https://api.usenaive.ai/v1/verification/members/${verification.members[0].id}/complete`,
  { method: "POST", headers: nv, body: JSON.stringify({ validation_token: "valtok_xxx" }) },
).then((r) => r.json());
// → member.status : "pass" | "fail"

// 3. Read the result + the formation gate (poll; no KYC webhook to subscribe to today).
const status = await fetch(
  `https://api.usenaive.ai/v1/verification/${verification.id}`,
  { headers: nv },
).then((r) => r.json());
// → status.ready_for_formation : true only when EVERY member is "pass"
The shape lines up closely. The real differences to plan for:
  • One subject vs. a set of members. Persona’s inquiry is one person tagged with a reference-id. Naive’s start takes the founding members with ownership_percentage (must sum to 100), one primary, and one is_responsible_party. That structure is the formation contract — not an arbitrary KYC subject.
  • The link comes back inline. Persona is create inquiry → generate link → email it yourself. Naive returns primary_link from start and emails secondary members automatically.
  • Status: poll + validation token, not a webhook. Persona steers you to inquiry.completed webhooks. Naive’s public webhook surface does not advertise a KYC event today — use the validation-token complete (instant for the primary) and poll verification.get for the rest.
  • The id is your identity, not a tag. In Persona, reference-id points back to your DB. In Naive, forUser(id) is the identity that owns the founders’ KYC and the company they’re about to form, its cards, phone, and vault.

Knowing when a founder passed

  • Persona’s recommended path is a webhook workflow: subscribe to inquiry.completed, then approve in a post-inquiry workflow and act on inquiry.approved.
  • Naive’s reliable public path today is validation token + polling:
// Webhook handler (Persona-Signature HMAC). Persona recommends this over polling.
app.post("/webhooks/persona", (req, res) => {
  const evt = req.body; // verify Persona-Signature first
  if (evt.data.attributes.name === "inquiry.approved") {
    const inquiryId = evt.data.attributes.payload.data.id;
    // mark the user verified in YOUR system
  }
  res.sendStatus(200);
});
const nv = { Authorization: `Bearer ${process.env.NAIVE_API_KEY!}`, "Content-Type": "application/json" };

// Primary founder: confirm instantly from the hosted flow's validation token.
await fetch(`https://api.usenaive.ai/v1/verification/members/${memberId}/complete`, {
  method: "POST", headers: nv, body: JSON.stringify({ validation_token: "valtok_xxx" }),
});

// Everyone else: poll GET /v1/verification/:id until the formation gate opens.
const v = await fetch(`https://api.usenaive.ai/v1/verification/${verification.id}`, { headers: nv })
  .then((r) => r.json());
if (v.ready_for_formation) {
  // every member is "pass" — proceed to formation
}
// Confirm any future KYC webhook via GET /v1/webhooks/event-types before relying on it.
  • Member statuses map roughly as: Persona completed/approved → Naive pass; declined/failedfail; needs_reviewpending_review; pending/createdin_progress / link_sent. See the full status list.

Minimal viable migration

The smallest swap that keeps a working founder-KYC flow running is just start + hand over the link + read status.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard).
2

Swap inquiry creation for verification.start

Replace POST /api/v1/inquiries (one subject + template) with client.verification.start({ members: [...] }). Provide each founder’s name + email, ownership_percentage (summing to 100), exactly one role: "primary", and exactly one is_responsible_party: true. Drop the inquiry-template-id — the KYC playbook is managed for you.
3

Swap link delivery

Drop generate-one-time-link + your own email send. Hand the returned primary_link to the primary founder; secondary members are emailed automatically. Use verification.resend if a link expires.
4

Swap status tracking

Replace the inquiry.completed webhook with the validation-token complete call for the primary (instant) and poll GET /v1/verification/:id for the rest. Gate on ready_for_formation instead of an inquiry.approved workflow.
5

Ship it

At this point your founder-KYC flow runs on Naive. Everything below is upside — the same identity is now ready to form the company and own its cards, phone, email, and vault.

Consolidate further once you’re on Naive

This is where the migration pays for itself. In Persona, an inquiry verifies a person and stops there — incorporation, banking, cards, and phone all live in separate systems keyed off a reference-id string. On Naive, the verified founders are the identity that carries straight into formation and every downstream primitive.
// Verify the founder...
const inq = await createInquiry({ referenceId: dbUser.id });
// → status "approved"

// ...then incorporate with Stripe Atlas / doola, open a bank account, issue a card,
//    and buy a phone number in ENTIRELY separate systems, each re-keyed to dbUser.id
//    by hand. Persona never knows the verified person became this company.
const acme = await naive.users.create({ external_id: dbCompany.id, email: founder.email });
const client = naive.forUser(acme.id);

// 1. KYC the founders.
const v = await client.verification.start({ members: [/* founders */] });

// 2. Same identity → form the company once everyone passes (PII pulled from the
//    encrypted vault at submission time; it never touches your DB). Pick a
//    naics_code_id from GET /v1/formation/naics-codes.
await client.formation.submit({
  verification_id: v.id,
  entity_type: "LLC",
  state: "WY",
  naics_code_id: "<naics-code-id>",
  description: "AI-powered business automation",
  name_options: [{ name: "Acme Tech", entity_type_ending: "LLC" }],
});

// 3. Same identity → it now owns the company's card, phone, email, and vault
//    (provision the number with the company EIN once formation has issued it).
await client.cards.create({ spending_limit_cents: 25_000 });
await client.phone.provision({ ein: "12-3456789", area_code: "415" });
await client.email.createInbox({ local_part: "founders" });

Gain #1 — one identity across primitives

  • With Persona, KYC is an island: an inquiry tagged with a reference-id that you manually re-key into your formation, banking, card, and phone vendors.
  • With Naive, naive.forUser(acme.id) is a single handle to KYC and formation and cards and phone and email and vault. The founders you verified are literally the responsible party the company is formed under — demonstrated by formation.submit({ verification_id }), which requires that the KYC passed.

Gain #2 — execution-time permission enforcement

  • Whether an agent may start KYC at all is policy on the Account Kit, enforced at execution time — not a Persona API-key scope you manage separately.
// Gate KYC behind human approval for agent-initiated calls.
const kit = await naive.accountKits.create({
  name: "Onboarding",
  primitives_config: {
    verification: { enabled: true, requiresApproval: true }, // KYC freezes for approval
    formation: { enabled: true, requiresApproval: true },
  },
});
await naive.accountKits.assignUser(kit.id, acme.id);
  • The agent’s code is identical either way — client.verification.start({ ... }). Whether the call runs is decided at execution time:
    • An agent whose kit gates verification.start gets 202 { status: "pending_approval", approval_id } instead of starting KYC. A human approves it and KYC begins on replay — see the start endpoint note.
    • This is the same approval model that gates formation.submit, cards, and domains.purchaseone policy surface, not one per vendor.

Gain #3 — unified accountability

  • Every KYC start, completion, link resend, and the formation it gates lands in one per-user activity log — alongside the company’s card, phone, email, and vault events, not in a separate Persona dashboard:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "who verified, who formed the company, and what has the agent done since?" — one timeline
  • That is the question that is hard to answer when KYC lives in Persona, incorporation in Stripe Atlas, and cards in Stripe Issuing. Under Naive it is a single query.

What does not map yet

A migration guide that hides gaps is worse than none. Founder KYC (start → hosted link → status → formation gate) maps cleanly, but the following Persona capabilities have no direct equivalent on Naive’s verification primitive today. Check this list against your app before you commit.
Persona featureStatus on NaiveWorkaround
General-purpose end-user KYC (verify arbitrary customers, gig workers, riders)Not provided — Naive KYC is founder / responsible-party onlyKeep Persona for end-user KYC; use Naive for the founders who form the company
Custom inquiry templates / themes (itmpl_…, theme sets, Dynamic Flow)Not configurable — fixed, provider-managed playbookNone — the flow (ID + selfie + SSN + address) is fixed
Embedded Flow (client SDK, your own iframe)Hosted flow only (verify.usenaive.ai)Redirect to the hosted primary_link
Webhooks for KYC (inquiry.completed/approved/declined)Not advertised today (email.received, approval.resolved only)Validation-token complete (instant for primary) + poll verification.get; confirm via GET /v1/webhooks/event-types
Reports — Watchlist / AML, adverse media, PEP screeningNo equivalent productNone — Naive KYC is identity verification, not sanctions/AML screening
Cases — manual-review queue + reviewer commentsNot provided (members can surface pending_review)Manual review happens in the KYC provider; no Cases API
Accounts / Documents / Verifications / Transactions APIsNot providedNone — Naive exposes verifications + members only
post-inquiry decisioning (approved/declined via Workflows)ready_for_formation gate (all members pass)Use the formation gate, not a generic approve/decline workflow
Arbitrary subject modelMembers require ownership_percentage summing to 100, one primary, one is_responsible_partyStructure is formation-shaped — not for non-founder subjects
Reusable verified profiles / one-time-link expiry tuningresend regenerates a member link; no custom expiry controlUse verification.resend when a link expires
The single most important gap: Naive does not do general-purpose end-user KYC. If your Persona usage is verifying your customers (not your company’s founders), or you depend on AML/watchlist Reports, a manual-review Cases queue, custom templates, or KYC webhooks, those are the gaps most likely to matter — Naive verifies founders ahead of formation and gates on ready_for_formation. Equally, note the direction of the model: Persona verifies a tagged subject and stops; Naive verifies the founders and carries that same identity into formation, cards, phone, and vault. That carry-through is the consolidation gain — but it means the primitive is shaped for incorporation, not for verifying arbitrary people.

Where to go next