> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rinne.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboarding

> Get a merchant approved by a payment provider with one model: Rinne tells you exactly what is missing, collects it through your own flow or the Rinne onboarding link, reuses everything already collected, and submits on its own.

export const OnboardingRequirementsExplorer = () => {
  const COMPANY = {
    id: 'a7089150-0d61-498f-b7ca-4767e4e4d6a4',
    cnpj: '12345678000195',
    contactCpf: '52998224806',
    personCpf: '39053344020'
  };
  const IDS = {
    IDENTITY_DOCUMENT: '9d6c5c69-1c92-4d25-93cf-fc0a63f1251c',
    BIOMETRIC_VERIFICATION: 'a688bf7d-513e-4786-9ee0-73eaf57da58f',
    ARTICLES_OF_INCORPORATION: '9571ddf8-d605-4879-84cd-02056ab48244',
    POWER_OF_ATTORNEY: 'cbca4307-c152-4dc3-a5da-b301e8c0ded7',
    identityFront: '161e2065-1179-4484-9ea3-faf62199595e',
    identityBack: '3268b183-1ceb-40e0-b258-40aaa4715e21',
    verification: '336b4995-d8ed-4837-97b2-b94990b30240'
  };
  const PROVIDERS = [{
    value: 'CELCOIN',
    hint: 'Documents and biometrics'
  }, {
    value: 'CAPPTA',
    hint: 'Nothing up front'
  }];
  const SHAPES = [{
    value: 'PJ',
    label: 'Limited company or corporation',
    hint: 'PJ'
  }, {
    value: 'ME',
    label: 'Microenterprise',
    hint: 'ME'
  }, {
    value: 'MEI',
    label: 'Individual microentrepreneur',
    hint: 'MEI'
  }, {
    value: 'PF',
    label: 'Natural person',
    hint: 'PF'
  }];
  const ROLES = [{
    value: 'PARTNER',
    label: 'A partner'
  }, {
    value: 'REPRESENTATIVE',
    label: 'A representative'
  }];
  const LABELS = {
    IDENTITY_DOCUMENT: 'Identity document',
    BIOMETRIC_VERIFICATION: 'Biometric verification',
    ARTICLES_OF_INCORPORATION: 'Articles of incorporation',
    POWER_OF_ATTORNEY: 'Power of attorney'
  };
  const HOW = {
    IDENTITY_DOCUMENT: 'A CNH_DIGITAL, or the front and back of a CNH, RG or RNE',
    BIOMETRIC_VERIFICATION: 'A selfie with liveness check, done by the person',
    ARTICLES_OF_INCORPORATION: 'The consolidated contrato social or the estatuto social',
    POWER_OF_ATTORNEY: 'The document that lets the contact represent the merchant'
  };
  const DEFAULTS = {
    provider: 'CELCOIN',
    shape: 'PJ',
    role: 'PARTNER'
  };
  const [provider, setProvider] = useState(DEFAULTS.provider);
  const [shape, setShape] = useState(DEFAULTS.shape);
  const [role, setRole] = useState(DEFAULTS.role);
  const [identityOnFile, setIdentityOnFile] = useState(false);
  const [verified, setVerified] = useState(false);
  const [view, setView] = useState('requirements');
  const isPerson = shape === 'PF';
  const collects = provider === 'CELCOIN';
  const contactCpf = isPerson ? COMPANY.personCpf : COMPANY.contactCpf;
  const companyDocument = isPerson ? COMPANY.personCpf : COMPANY.cnpj;
  const reset = () => {
    setProvider(DEFAULTS.provider);
    setShape(DEFAULTS.shape);
    setRole(DEFAULTS.role);
    setIdentityOnFile(false);
    setVerified(false);
  };
  const baseSet = () => {
    if (!collects) return [];
    const rows = [{
      type: 'IDENTITY_DOCUMENT',
      subject: contactCpf,
      role: 'CONTACT'
    }, {
      type: 'BIOMETRIC_VERIFICATION',
      subject: contactCpf,
      role: 'CONTACT'
    }];
    if (shape === 'PJ' || shape === 'ME') {
      rows.push({
        type: 'ARTICLES_OF_INCORPORATION',
        subject: companyDocument,
        role: 'COMPANY'
      });
    }
    if (!isPerson && role === 'REPRESENTATIVE') {
      rows.push({
        type: 'POWER_OF_ATTORNEY',
        subject: companyDocument,
        role: 'COMPANY'
      });
    }
    return rows.map(row => {
      const satisfied = row.type === 'IDENTITY_DOCUMENT' && identityOnFile || row.type === 'BIOMETRIC_VERIFICATION' && verified;
      return {
        ...row,
        status: satisfied ? 'SATISFIED' : 'PENDING'
      };
    });
  };
  const rows = baseSet();
  const pending = rows.filter(row => row.status === 'PENDING').length;
  const outcome = () => {
    if (!collects) {
      return {
        status: 'PROCESSING',
        tone: 'ok',
        step: 1,
        text: 'Submitted at once with the merchant data and the settlement bank account. If the risk analysis asks for more, new requirements arrive on this same affiliation.'
      };
    }
    if (pending > 0) {
      return {
        status: 'WAITING_DOCUMENTS',
        tone: 'wait',
        step: 0,
        text: `${pending} ${pending === 1 ? 'item is' : 'items are'} pending. onboarding_url carries the link, and Rinne submits on its own when the last one arrives.`
      };
    }
    return {
      status: 'PENDING_APPROVAL',
      tone: 'ok',
      step: 1,
      text: 'Everything is already satisfied, so the affiliation is submitted to the provider at once. No link is needed.'
    };
  };
  const accepted = type => {
    if (type === 'IDENTITY_DOCUMENT') {
      return ['CNH_DIGITAL', 'CNH_FRONT', 'CNH_BACK', 'RG_FRONT', 'RG_BACK', 'RNE_FRONT', 'RNE_BACK'];
    }
    if (type === 'BIOMETRIC_VERIFICATION') return null;
    return [type];
  };
  const satisfiedBy = row => {
    if (row.status !== 'SATISFIED') return null;
    if (row.type === 'IDENTITY_DOCUMENT') {
      return {
        onboarding_document_ids: [IDS.identityFront, IDS.identityBack],
        biometric_verification_id: null,
        answer: null
      };
    }
    return {
      onboarding_document_ids: [],
      biometric_verification_id: IDS.verification,
      answer: null
    };
  };
  const json = () => JSON.stringify({
    data: rows.map(row => ({
      id: IDS[row.type],
      company_id: COMPANY.id,
      provider,
      type: row.type,
      subject_document_number: row.subject,
      subject_role: row.role,
      origin: 'BASE',
      description: null,
      status: row.status,
      accepted_document_types: accepted(row.type),
      satisfied_by: satisfiedBy(row),
      waived_at: null,
      created_at: '2026-10-02T11:58:10.000Z',
      updated_at: '2026-10-02T11:58:10.000Z'
    }))
  }, null, 2);
  const highlight = source => {
    const pattern = /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?)/g;
    const parts = [];
    let last = 0;
    let match;
    while ((match = pattern.exec(source)) !== null) {
      if (match.index > last) parts.push(source.slice(last, match.index));
      const key = match.index;
      if (match[1] && match[2]) {
        parts.push(<span key={key} className="rco-tk-key">{match[1]}</span>, match[2]);
      } else if (match[1]) {
        parts.push(<span key={key} className="rco-tk-str">{match[1]}</span>);
      } else if (match[3]) {
        parts.push(<span key={key} className="rco-tk-lit">{match[3]}</span>);
      } else {
        parts.push(<span key={key} className="rco-tk-num">{match[4]}</span>);
      }
      last = pattern.lastIndex;
    }
    parts.push(source.slice(last));
    return parts;
  };
  const checkIcon = () => <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>;
  const clockIcon = () => <svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5.5" fill="none" stroke="currentColor" strokeWidth="1.6" /><path d="M8 5v3.2l2 1.3" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>;
  const resetIcon = () => <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8a5 5 0 1 0 1.6-3.7M3 2.5v2.8h2.8" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>;
  const chips = (name, value, options, onChange) => <div className="rco-chips" role="radiogroup" aria-label={name}>
      {options.map(option => <button key={option.value} type="button" role="radio" aria-checked={value === option.value} className="rco-chip" onClick={() => onChange(option.value)}>
          <span className="rco-chip-dot" aria-hidden="true" />
          {option.label || option.value}
        </button>)}
    </div>;
  const shapeCards = () => <div className="rco-cards" role="radiogroup" aria-label="Merchant shape">
      {SHAPES.map(option => <button key={option.value} type="button" role="radio" aria-checked={shape === option.value} className="rco-card" onClick={() => setShape(option.value)}>
          <span className="rco-card-title">{option.label}</span>
          <code className="rco-card-hint">{option.hint}</code>
        </button>)}
    </div>;
  const toggle = (label, checked, onChange, disabled) => <label className={disabled ? 'rco-switch is-off' : 'rco-switch'}>
      <input type="checkbox" checked={checked && !disabled} disabled={disabled} onChange={e => onChange(e.target.checked)} />
      <span className="rco-track" aria-hidden="true" />
      <span className="rco-switch-label">{label}</span>
    </label>;
  const requirementRow = row => {
    const done = row.status === 'SATISFIED';
    return <li key={row.type} className={done ? 'rco-req is-done' : 'rco-req'}>
        <span className="rco-req-icon">{done ? checkIcon() : clockIcon()}</span>
        <div className="rco-req-body">
          <div className="rco-req-top">
            <span className="rco-req-title">{LABELS[row.type]}</span>
            <span className={done ? 'rco-tag ok' : 'rco-tag wait'}>{row.status}</span>
          </div>
          <code className="rco-req-type">{row.type}</code>
          <p className="rco-req-how">{HOW[row.type]}</p>
          <div className="rco-req-meta">
            <span className="rco-tag">{row.role}</span>
            <code>{row.subject}</code>
          </div>
        </div>
      </li>;
  };
  const track = result => {
    const steps = ['Collect what is pending', 'Rinne submits', 'The provider decides'];
    return <ol className="rco-track-steps" aria-label="What happens next">
        {steps.map((label, index) => {
      const skipped = index === 0 && result.step > 0;
      const state = skipped ? 'is-skipped' : index < result.step ? 'is-done' : index === result.step ? 'is-now' : '';
      return <li key={label} className={`rco-step ${state}`}>
              <span className="rco-step-dot" aria-hidden="true" />
              <span className="rco-step-label">{skipped ? 'Nothing to collect' : label}</span>
            </li>;
    })}
      </ol>;
  };
  const result = outcome();
  const path = `/v1/onboarding-requirements?provider=${provider}`;
  return <div className="rco">
      <div className="rco-studio">
        <section className="rco-panel rco-inspector" aria-label="The merchant">
          <header className="rco-panel-head">
            <div className="rco-panel-title">
              Configure
              <span className="rco-badge"><span className="rco-live" aria-hidden="true" />Simulation</span>
            </div>
            <button type="button" className="rco-btn-ghost" onClick={reset}>
              {resetIcon()}
              Reset
            </button>
          </header>

          <div className="rco-fields">
            <div className="rco-field">
              <div className="rco-label">Provider</div>
              {chips('Provider', provider, PROVIDERS, setProvider)}
              <p className="rco-note">{PROVIDERS.find(option => option.value === provider).hint}</p>
            </div>

            <div className="rco-field">
              <div className="rco-label">Its contact</div>
              {isPerson ? <p className="rco-note rco-note-box">A natural person is their own contact.</p> : chips('Contact role', role, ROLES, setRole)}
              {!isPerson && <p className="rco-note"><code>{role}</code> on the merchant's contact</p>}
            </div>

            <div className="rco-field rco-field-wide">
              <div className="rco-label">The merchant</div>
              {shapeCards()}
            </div>

            <div className="rco-field rco-field-wide">
              <div className="rco-label">Already on file in your organization</div>
              <div className="rco-switches">
                {toggle('The contact’s identity document, sent for another merchant', identityOnFile, setIdentityOnFile, !collects)}
                {toggle('An approved biometric verification of the contact', verified, setVerified, !collects)}
              </div>
            </div>
          </div>

          <footer className="rco-panel-foot">Simulation only. Nothing here calls the API.</footer>
        </section>

        <section className="rco-panel rco-stage" aria-label="What Rinne answers">
          <div className="rco-tabs" role="tablist" aria-label="View">
            <button type="button" role="tab" aria-selected={view === 'requirements'} className="rco-tab" onClick={() => setView('requirements')}>
              Requirements
            </button>
            <button type="button" role="tab" aria-selected={view === 'response'} className="rco-tab" onClick={() => setView('response')}>
              Response
            </button>
          </div>

          <div className="rco-request">
            <span className="rco-method">GET</span>
            <code>{path}</code>
          </div>

          {view === 'requirements' ? <div className="rco-stage-body">
              {rows.length === 0 ? <div className="rco-empty">
                  <span className="rco-empty-icon">{checkIcon()}</span>
                  <div>
                    <strong>No requirements up front.</strong>
                    <p>Anything the provider asks for later arrives here as a new requirement.</p>
                  </div>
                </div> : <div>
                  <div className="rco-summary">
                    <span className="rco-summary-count">
                      {pending === 0 ? 'All on file' : `${pending} of ${rows.length} to collect`}
                    </span>
                    <span className="rco-meter" aria-hidden="true">
                      {rows.map(row => <i key={row.type} className={row.status === 'SATISFIED' ? 'is-done' : ''} />)}
                    </span>
                  </div>
                  <ul className="rco-reqs">{rows.map(requirementRow)}</ul>
                </div>}

              <div className="rco-outcome">
                <div className="rco-outcome-head">
                  <span className="rco-method rco-method-post">POST</span>
                  <code>/v1/affiliations</code>
                  <span className="rco-arrow" aria-hidden="true">→</span>
                  <span className={`rco-tag ${result.tone}`}>{result.status}</span>
                </div>
                <p className="rco-outcome-text">{result.text}</p>
                {track(result)}
              </div>
            </div> : <div className="rco-code">
              <div className="rco-code-bar">
                <span className="rco-code-status">200 OK</span>
                <span>application/json</span>
              </div>
              <pre tabIndex={0}>{highlight(json())}</pre>
            </div>}
        </section>
      </div>
    </div>;
};

<Info>
  Onboarding is available in the sandbox, so you can integrate and test it now. In production, its routes answer `403` until Rinne turns it on; Rinne announces the date to its customers beforehand.
</Info>

Onboarding is how a merchant gets an [affiliation](/concepts/affiliations) approved by a payment provider. The merchant can be a legal person (CNPJ) or a natural person (CPF). Rinne tells you what the provider needs from that merchant, collects what is missing, submits everything the moment it is complete, and reports every step by webhook.

You choose how the merchant supplies what is missing: through your own screens on the API, through the **Rinne onboarding link** that comes with every affiliation, or both at once.

<CardGroup cols={2}>
  <Card title="The onboarding link" icon="link" href="/onboarding/onboarding-link">
    A mobile-first page that asks the merchant's contact only for what is still pending. Send it, or embed it in your app.
  </Card>

  <Card title="Build with the API" icon="code" href="/onboarding/api">
    The same building blocks the link runs on: requirements, documents and biometric verifications, in any order.
  </Card>
</CardGroup>

## Why it is different

<CardGroup cols={2}>
  <Card title="Asks only for what is missing" icon="list-check">
    Each requirement says what is pending, for which person or merchant, and which documents satisfy it. Nothing already valid is asked again.
  </Card>

  <Card title="Collected once, reused everywhere" icon="recycle">
    An identity document or an approved biometric verification serves every merchant of your organization where that person appears, and every later attempt.
  </Card>

  <Card title="Any order, any time" icon="shuffle">
    Upload a document today and create the affiliation next month. Start the biometric verification before the affiliation exists. The pieces do not wait for each other.
  </Card>

  <Card title="Submits on its own" icon="paper-plane">
    When the last requirement is satisfied, through the API or the link, Rinne submits the affiliation to the provider without another call from you.
  </Card>

  <Card title="Rejections you can act on" icon="route">
    A provider's refusal becomes structured `rejection_reasons`, each with an `action`. The next attempt asks only for what the provider refused or added.
  </Card>

  <Card title="Every step is an event" icon="bell">
    Documents, biometric verifications, requirements and the affiliation each report their changes by webhook, so your system never polls.
  </Card>
</CardGroup>

## How it works

```mermaid theme={null}
flowchart TD
    A[POST /v1/affiliations] --> W{Anything pending?}
    W -- yes --> WD[WAITING_DOCUMENTS<br/>link in onboarding_url]
    WD --> C[Collect in any order<br/>documents · biometrics · answers]
    C -- last item arrives --> S[Rinne submits on its own]
    W -- no --> S
    S --> P{The provider decides}
    P -- approves --> OK[ACTIVE]
    P -- refuses --> RJ[REJECTED<br/>with rejection_reasons]
    RJ -. a new attempt asks only for what was refused or added .-> A

    style A fill:#5B68EB,color:#fff
    style S fill:#5B68EB,color:#fff
    style OK fill:#16A75E,color:#fff
    style RJ fill:#7B89FF,color:#fff
```

<Steps>
  <Step title="Create the affiliation">
    Call `POST /v1/affiliations` (or `POST /v1/merchants/{merchantId}/affiliations` for one of your merchants), exactly as you already do. When the provider needs something the merchant has not supplied yet, the affiliation comes back `WAITING_DOCUMENTS` with its onboarding link in `onboarding_url`. When everything is already satisfied, it is submitted at once.
  </Step>

  <Step title="Collect what is pending">
    Send the onboarding link to the merchant's contact, or supply the items yourself through the API, or mix the two. [`GET /v1/onboarding-requirements`](/onboarding/requirements) always says what is still missing.
  </Step>

  <Step title="Rinne submits">
    The moment the last requirement is satisfied, Rinne submits the affiliation: `WAITING_DOCUMENTS` → `PENDING` → `PENDING_APPROVAL`, then `PROCESSING_DOCUMENTS` while the provider analyzes the documents. At a provider without a document stage, such as `CAPPTA`, it moves to `PROCESSING` instead.
  </Step>

  <Step title="The provider decides">
    The affiliation becomes `ACTIVE`, or `REJECTED` with reasons you can act on. `affiliation.status-changed` reports each move.

    <Check>
      An `ACTIVE` affiliation processes transactions. The documents it sent become `VALIDATED` and keep serving the merchant's other affiliations.
    </Check>
  </Step>
</Steps>

<Info>
  **Rinne never approves a merchant.** The provider decides. Rinne collects, checks the format of what it collects, submits, and reports the provider's answer.
</Info>

## See what a merchant needs

Pick the merchant's shape and the provider. The explorer shows the requirements Rinne creates for it, what happens when you create the affiliation, and how reuse changes the answer.

<OnboardingRequirementsExplorer />

The full rules, including what a provider can ask for later, are in [Requirements](/onboarding/requirements).

## Two ways to collect, one model

<Tabs>
  <Tab title="Onboarding link">
    Hand the merchant's contact the affiliation's `onboarding_url`. They confirm their CPF and complete the pending items in any order: a photo or file for each document, the biometric selfie, and any question the provider asked. They can stop and come back; what they already sent is never asked again.

    Best when you want onboarding live without building screens.

    [Explore the onboarding link →](/onboarding/onboarding-link)
  </Tab>

  <Tab title="API">
    Read the requirements, upload documents through signed upload URLs, start biometric verifications and answer questions from your own application. Every call exists for the authenticated merchant and, under `/v1/merchants/{merchantId}`, for an organization acting on one of its merchants.

    Best when onboarding is part of your own product journey.

    [Build with the API →](/onboarding/api)
  </Tab>

  <Tab title="Both">
    The link runs on the same calls as the API, so anything one does the other sees. Upload the articles of incorporation you already hold through the API, then send the link so the contact only does the biometric step and their identity document.

    Best when you hold part of the merchant's documents already.

    [Mix the two →](/onboarding/api#combine-the-api-with-the-link)
  </Tab>
</Tabs>

## What each provider asks for

| Provider | Up front | Later |
| - | - | - |
| `CELCOIN` | The contact's identity document and biometric verification; the articles of incorporation of a `PJ` or `ME` merchant; a power of attorney when a merchant with a CNPJ is represented by a `REPRESENTATIVE` contact | Anything the provider asks for after a rejection, collected for a new affiliation |
| `CAPPTA` | Nothing | Documents or information the provider's risk analysis asks for, collected on the same affiliation |
| `RINNE` | Nothing | Nothing |

The same rules apply to every merchant. Details and examples: [Requirements](/onboarding/requirements).

## Already integrated with affiliations?

Your integration keeps working. `POST /v1/affiliations` takes the same request, an affiliation still waits in `WAITING_DOCUMENTS` with its link in `onboarding_url` for up to 30 days before its first submission, and `affiliation.status-changed` reports every move. Four details are worth handling:

<AccordionGroup>
  <Accordion title="Handle the CANCELLED status" icon="ban">
    An affiliation that waits 30 days in `WAITING_DOCUMENTS` before its first submission, or that you cancel, becomes `CANCELLED` with a `cancellation_reason`. It is terminal: create a new affiliation when the merchant is ready. See [Statuses and events](/onboarding/statuses-and-events#cancelled).
  </Accordion>

  <Accordion title="Read the link from affiliation.created too" icon="link">
    An affiliation created already waiting reports no status change, so its `onboarding_url` arrives in the create response and on the `affiliation.created` webhook. See [Get the link](/onboarding/onboarding-link#get-the-link).
  </Accordion>

  <Accordion title="Use onboarding_url as it is" icon="arrow-up-right-from-square">
    Forward or embed the URL you receive without depending on its domain. Most affiliations carry the Rinne onboarding link, but an attempt that is already on the provider's own page keeps that page until it finishes.
  </Accordion>

  <Accordion title="Let an embedded link open a new tab" icon="window-restore">
    The biometric step opens the selfie capture in a new tab and resumes when the contact comes back, so a frame or app webview that holds the link must allow new windows; the camera is used for the document photos. Android's WebView cannot run the step, so open the link in the browser there. See [Embed the link](/onboarding/onboarding-link#embed-the-link-in-your-app).
  </Accordion>
</AccordionGroup>

## Start here

<CardGroup cols={2}>
  <Card title="The onboarding link" icon="link" href="/onboarding/onboarding-link">
    What the contact sees, how to send or embed the link, and how to reissue it.
  </Card>

  <Card title="Build with the API" icon="code" href="/onboarding/api">
    Read requirements, upload documents, run a biometric verification and create the affiliation.
  </Card>

  <Card title="When the provider asks for more" icon="route" href="/onboarding/rejections">
    Rejection reasons, new requirements and the next attempt.
  </Card>

  <Card title="Test in the sandbox" icon="flask" href="/onboarding/testing">
    Drive approvals and rejections of affiliations and biometric verifications.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.