> ## 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.

# Build with the API

> Run onboarding from your own application: read what the provider requires, upload documents, run the biometric verification, answer the provider's questions and let Rinne submit the affiliation.

The onboarding API is the set of building blocks the [onboarding link](/onboarding/onboarding-link) runs on. Use it to put onboarding inside your own product, to supply documents you already hold, or to automate the whole flow.

The blocks do not depend on each other or on any order. A document uploaded today satisfies an affiliation created next month, and a biometric verification approved before the affiliation exists is already counted when it is created.

```mermaid theme={null}
sequenceDiagram
    participant You as Your backend
    participant Rinne as Rinne API
    participant Person as Merchant contact

    You->>Rinne: GET /onboarding-requirements?provider=CELCOIN
    Rinne-->>You: What is pending, what is satisfied
    You->>Rinne: POST /onboarding-documents
    Rinne-->>You: upload_url (15 minutes)
    You->>Rinne: PUT the file, then POST /onboarding-documents/{id}/complete
    You->>Rinne: POST /biometric-verifications
    Rinne-->>You: verification_url
    You->>Person: Open verification_url
    Person->>Rinne: Selfie and liveness, at the provider's capture page
    Rinne-->>You: biometric-verification.status-changed (APPROVED)
    You->>Rinne: POST /affiliations
    Rinne-->>You: PENDING_APPROVAL, submitted at once
    Rinne-->>You: affiliation.status-changed (ACTIVE)
```

## Before you start

* **Who the calls act on.** Every route exists in two forms. The root path (`/v1/onboarding-requirements`) acts on the authenticated merchant: your organization onboarding itself, or a merchant with its own API key. The same path under `/v1/merchants/{merchantId}` is for an organization acting on one of its merchants, with the same body and response. The examples below use the merchant form.
* **Permissions.** Each route requires its own permission, named on its reference page: `onboarding-requirement.list` and `onboarding-requirement.edit`, `onboarding-document.create`, `onboarding-document.list` and `onboarding-document.view`, `biometric-verification.create`, `biometric-verification.list` and `biometric-verification.view`, with a `merchant.` twin of each for the merchant routes.
* **The merchant's contact.** Requirements about a person name the merchant's registered contact, and only that person can be verified. Register the contact's CPF when you create the merchant.

## 1. Read what is pending

`GET /v1/onboarding-requirements?provider=CELCOIN` returns the merchant's requirements for that provider, each with its `status` and what satisfies it. Reading it is optional, but it is the one call that tells you exactly what to collect and what Rinne already holds, from this merchant, another merchant of your organization, or an earlier attempt.

<RequestExample>
  ```bash cURL theme={null}
  curl "https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-requirements?provider=CELCOIN" \
    -H "x-api-key: YOUR_API_KEY"
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "data": [
      {
        "id": "9d6c5c69-1c92-4d25-93cf-fc0a63f1251c",
        "company_id": "a7089150-0d61-498f-b7ca-4767e4e4d6a4",
        "provider": "CELCOIN",
        "type": "IDENTITY_DOCUMENT",
        "subject_document_number": "52998224806",
        "subject_role": "CONTACT",
        "origin": "BASE",
        "description": null,
        "status": "SATISFIED",
        "accepted_document_types": ["CNH_DIGITAL", "CNH_FRONT", "CNH_BACK", "RG_FRONT", "RG_BACK", "RNE_FRONT", "RNE_BACK"],
        "satisfied_by": {
          "onboarding_document_ids": ["161e2065-1179-4484-9ea3-faf62199595e"],
          "biometric_verification_id": null,
          "answer": null
        },
        "waived_at": null,
        "created_at": "2026-10-02T11:58:10.000Z",
        "updated_at": "2026-10-02T11:58:10.000Z"
      },
      {
        "id": "a688bf7d-513e-4786-9ee0-73eaf57da58f",
        "company_id": "a7089150-0d61-498f-b7ca-4767e4e4d6a4",
        "provider": "CELCOIN",
        "type": "BIOMETRIC_VERIFICATION",
        "subject_document_number": "52998224806",
        "subject_role": "CONTACT",
        "origin": "BASE",
        "description": null,
        "status": "PENDING",
        "accepted_document_types": null,
        "satisfied_by": null,
        "waived_at": null,
        "created_at": "2026-10-02T11:58:10.000Z",
        "updated_at": "2026-10-02T11:58:10.000Z"
      },
      {
        "id": "9571ddf8-d605-4879-84cd-02056ab48244",
        "company_id": "a7089150-0d61-498f-b7ca-4767e4e4d6a4",
        "provider": "CELCOIN",
        "type": "ARTICLES_OF_INCORPORATION",
        "subject_document_number": "12345678000195",
        "subject_role": "COMPANY",
        "origin": "BASE",
        "description": null,
        "status": "PENDING",
        "accepted_document_types": ["ARTICLES_OF_INCORPORATION"],
        "satisfied_by": null,
        "waived_at": null,
        "created_at": "2026-10-02T11:58:10.000Z",
        "updated_at": "2026-10-02T11:58:10.000Z"
      }
    ]
  }
  ```
</ResponseExample>

Here the contact's identity document is already on file, so only the biometric verification and the merchant's articles of incorporation are left. Each requirement type, status and origin is described in [Requirements](/onboarding/requirements).

## 2. Upload documents

A document is uploaded in three calls: open the upload, send the bytes to the signed URL it returns, and confirm. Upload each document under the CPF or CNPJ it is about (`subject_document_number`), whenever you have it, before or after the affiliation exists.

<Steps>
  <Step title="Open the upload">
    ```bash cURL theme={null}
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-documents \
      -H "x-api-key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "ARTICLES_OF_INCORPORATION",
        "subject_document_number": "12.345.678/0001-95",
        "file_name": "contrato-social-consolidado.pdf",
        "content_type": "application/pdf"
      }'
    ```

    ```json 201 Created theme={null}
    {
      "id": "d5defce5-d859-4a58-a504-3ad470b6de08",
      "company_id": "a7089150-0d61-498f-b7ca-4767e4e4d6a4",
      "subject_document_number": "12345678000195",
      "type": "ARTICLES_OF_INCORPORATION",
      "status": "PENDING_UPLOAD",
      "source": "UPLOAD",
      "requirement_id": null,
      "file_name": "contrato-social-consolidado.pdf",
      "content_type": null,
      "file_size_bytes": null,
      "upload_url": "<signed PUT URL>",
      "upload_url_expires_at": "2026-10-02T12:15:00.000Z",
      "rejections": [],
      "superseded_by_id": null,
      "screening": null,
      "screening_note": null,
      "created_at": "2026-10-02T12:00:00.000Z",
      "updated_at": "2026-10-02T12:00:00.000Z"
    }
    ```
  </Step>

  <Step title="Send the file to upload_url">
    `upload_url` is a signed `PUT` valid for 15 minutes. Send the bytes with these three headers, all signed into the URL:

    ```bash cURL theme={null}
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: application/pdf" \
      -H "x-goog-if-generation-match: 0" \
      -H "x-goog-content-length-range: 0,10485760" \
      --data-binary @contrato-social-consolidado.pdf
    ```

    `Content-Type` must equal the `content_type` you declared. A file over 10 MB is refused here, and a second `PUT` answers `412` while a file is already behind the URL.
  </Step>

  <Step title="Confirm the upload">
    ```bash cURL theme={null}
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-documents/d5defce5-d859-4a58-a504-3ad470b6de08/complete \
      -H "x-api-key: YOUR_API_KEY"
    ```

    Rinne checks that the file is a PDF, PNG or JPEG of at most 10 MB whose bytes match the declared type, and the document becomes `NOT_VALIDATED`: stored, usable, and counted toward its requirement.

    <Check>
      The response is `201` with `status` `NOT_VALIDATED` and `file_size_bytes` filled, and `onboarding-document.status-changed` fires. The `ARTICLES_OF_INCORPORATION` requirement now reads `SATISFIED`. If the merchant already held the same file for the same subject and type, the `201` carries that existing document with its current status instead, and the one you opened is closed as `SUPERSEDED`.
    </Check>
  </Step>
</Steps>

The same three steps in Node.js:

```javascript upload-document.js theme={null}
import { readFile } from 'node:fs/promises';

const BASE_URL = 'https://api-sandbox.rinne.com.br/core/v1';
const MERCHANT_ID = 'a7089150-0d61-498f-b7ca-4767e4e4d6a4';
const headers = { 'x-api-key': process.env.RINNE_API_KEY, 'Content-Type': 'application/json' };

async function uploadDocument({ type, subjectDocumentNumber, path, fileName, contentType }) {
  const opened = await fetch(`${BASE_URL}/merchants/${MERCHANT_ID}/onboarding-documents`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      type,
      subject_document_number: subjectDocumentNumber,
      file_name: fileName,
      content_type: contentType,
    }),
  });
  if (!opened.ok) throw new Error(`Open upload failed: ${opened.status}`);
  const document = await opened.json();

  const put = await fetch(document.upload_url, {
    method: 'PUT',
    headers: {
      'Content-Type': contentType,
      'x-goog-if-generation-match': '0',
      'x-goog-content-length-range': '0,10485760',
    },
    body: await readFile(path),
  });
  if (!put.ok) throw new Error(`Upload failed: ${put.status}`);

  const completed = await fetch(
    `${BASE_URL}/merchants/${MERCHANT_ID}/onboarding-documents/${document.id}/complete`,
    { method: 'POST', headers: { 'x-api-key': process.env.RINNE_API_KEY } },
  );
  if (!completed.ok) throw new Error(`Complete failed: ${completed.status}`);
  return completed.json(); // status: "NOT_VALIDATED", or an existing document with the same bytes
}

const articles = await uploadDocument({
  type: 'ARTICLES_OF_INCORPORATION',
  subjectDocumentNumber: '12345678000195',
  path: './contrato-social-consolidado.pdf',
  fileName: 'contrato-social-consolidado.pdf',
  contentType: 'application/pdf',
});
console.log(articles.id, articles.status); // d5defce5-... NOT_VALIDATED
```

<Note>
  An identity document is satisfied by a `CNH_DIGITAL` alone, or by the front and back of one document: `CNH_FRONT` with `CNH_BACK`, `RG_FRONT` with `RG_BACK`, or `RNE_FRONT` with `RNE_BACK`. Upload each side as its own document, one image or one PDF per side. All document types and statuses: [Documents](/onboarding/documents).
</Note>

| Status | When |
| - | - |
| `400` on create | A type a customer cannot upload, or a subject the merchant does not register (anyone but the merchant, its contact, or the subject of one of its requirements) |
| `400` on `complete` | The file is not a PDF, PNG or JPEG, exceeds 10 MB, or its bytes do not match the declared type. The file is deleted; `PUT` a corrected file to the same URL until it expires |
| `409` on `complete` | Nothing was put behind the upload URL yet |
| `410` on `complete` | The 15-minute window expired with no file behind the URL; open a new upload |
| `201` with another document | The same bytes were already stored for that subject and type; the document you opened becomes `SUPERSEDED` by the existing one |

## 3. Run the biometric verification

The biometric verification is a selfie with a liveness and face-match check of the merchant's contact, done live by the person on the provider's own capture page. It is the one item a person has to do themselves.

<Steps>
  <Step title="Check whether a capture is needed">
    The `BIOMETRIC_VERIFICATION` requirement from [step 1](#1-read-what-is-pending) decides: `SATISFIED` means an approved verification already serves this merchant, since one serves every merchant of your organization where that person is the contact, and there is nothing to do. When it is `PENDING`, list the person's verifications to follow one that is still `PENDING` instead of starting another:

    ```bash cURL theme={null}
    curl "https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/biometric-verifications?subject_document_number=52998224806&status=PENDING" \
      -H "x-api-key: YOUR_API_KEY"
    ```

    An `APPROVED` verification of the person does not settle it on its own: when Rinne could not store its selfie within a day of the approval, the requirement goes back to `PENDING` and the person [does it again](/onboarding/biometric-verification#when-an-approved-verification-has-to-be-redone). Go by the requirement's `status`.
  </Step>

  <Step title="Start a verification">
    ```bash cURL theme={null}
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/biometric-verifications \
      -H "x-api-key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "provider": "CELCOIN",
        "subject_document_number": "529.982.248-06",
        "redirect_url": "https://app.acmepagamentos.com.br/onboarding/verification-done"
      }'
    ```

    ```json 201 Created theme={null}
    {
      "id": "336b4995-d8ed-4837-97b2-b94990b30240",
      "company_id": "a7089150-0d61-498f-b7ca-4767e4e4d6a4",
      "provider": "CELCOIN",
      "subject_document_number": "52998224806",
      "status": "PENDING",
      "verification_url": "<provider capture URL>",
      "verification_url_expires_at": "2026-10-02T13:05:00.000Z",
      "redirect_url": "https://app.acmepagamentos.com.br/onboarding/verification-done",
      "selfie_onboarding_document_id": null,
      "approved_at": null,
      "rejection_reason": null,
      "created_at": "2026-10-02T12:05:00.000Z",
      "updated_at": "2026-10-02T12:05:00.000Z"
    }
    ```
  </Step>

  <Step title="Hand verification_url to the person">
    Open it on the person's phone or send it to them, in a browser tab of its own: the provider's page does not run inside a frame. It works until `verification_url_expires_at`; read that field rather than assuming a lifetime. `redirect_url` is optional and is where the person lands when the capture ends.
  </Step>

  <Step title="Receive the result">
    The result arrives by the `biometric-verification.status-changed` webhook and on `GET .../biometric-verifications/{id}`, never through the redirect.

    <Check>
      `APPROVED` stores the selfie as a `SELFIE` document (`selfie_onboarding_document_id`) and satisfies the `BIOMETRIC_VERIFICATION` requirement. `REPROVED` carries the provider's `rejection_reason`; start a new verification.
    </Check>
  </Step>
</Steps>

Only one verification per person and provider can be pending at a time: a second `POST` answers `409` with `details.pending_verification_id`, the one to follow. Statuses, reuse and the evidence package: [Biometric verification](/onboarding/biometric-verification).

## 4. Answer questions and evidence requests

A provider can ask for information in text (`INFORMATION`) or for an explanation backed by files (`EVIDENCE_PACKAGE`). These requirements carry a `description` with the request, written for the merchant's contact.

<Tabs>
  <Tab title="INFORMATION">
    Answer with the text:

    ```bash cURL theme={null}
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-requirements/a0bceefd-ee9c-4012-89ea-e20c0646049d/answer \
      -H "x-api-key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "text": "Revenue comes from retail sales of bread and pastries at the physical store; there are no online sales."
      }'
    ```

    The response is `201` with the requirement `SATISFIED` and the text in `satisfied_by.answer`.
  </Tab>

  <Tab title="EVIDENCE_PACKAGE">
    Attach the files first, each as a document that names the requirement in `requirement_id` (`type` `OTHER` for a photo, screenshot or print), then close the package with an answer:

    ```bash cURL theme={null}
    # 1. Attach a file (then PUT it and call complete, as in step 2)
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-documents \
      -H "x-api-key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "OTHER",
        "subject_document_number": "12345678000195",
        "requirement_id": "5117d6f7-9d28-4fb5-aad7-17119c9437cb",
        "file_name": "storefront.jpg",
        "content_type": "image/jpeg"
      }'

    # 2. Close the package
    curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/onboarding-requirements/5117d6f7-9d28-4fb5-aad7-17119c9437cb/answer \
      -H "x-api-key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "text": "The bakery operates at the registered address. Attached: a photo of the storefront and the opening hours on the map listing."
      }'
    ```

    The text is optional for a package, but a package with neither text nor an accepted attachment answers `400`. Once closed, the package takes no more attachments (`409`) until the provider refuses it.
  </Tab>
</Tabs>

An answer never changes the merchant's registration. Any other requirement type answers `400`.

## 5. Create the affiliation

Create it whenever you want, before, during or after collecting:

```bash cURL theme={null}
curl -X POST https://api-sandbox.rinne.com.br/core/v1/merchants/a7089150-0d61-498f-b7ca-4767e4e4d6a4/affiliations \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "CELCOIN",
    "allowed_capture_methods": ["ECOMMERCE"],
    "allowed_payment_methods": ["PIX"]
  }'
```

| What the merchant has | Status in the response | What happens next |
| - | - | - |
| Every requirement satisfied | `PENDING_APPROVAL` | Submitted to the provider at once; `onboarding_url` is `null` |
| Something pending | `WAITING_DOCUMENTS` | `onboarding_url` carries the link; Rinne submits on its own when the last item arrives |
| Everything satisfied, but a previous attempt of the same merchant still open at the provider, or still under Rinne's review | `PENDING` | Held, then submitted once the way is clear |

<Note>
  One affiliation per merchant and provider can wait for documents at a time: a second `POST /v1/affiliations` while one is `WAITING_DOCUMENTS` answers `409` naming the open one. [Cancel](/onboarding/statuses-and-events#cancel-an-affiliation) it to start over.
</Note>

From here, follow the affiliation through its webhooks: [Statuses and events](/onboarding/statuses-and-events).

## Combine the API with the link

The link and the API write to the same requirements, so you can split the work any way you like:

* **Supply what you hold, send the link for the rest.** Upload the articles of incorporation and the power of attorney your back office already has, then send `onboarding_url`. The contact sees only their identity document and the biometric step.
* **Use the link for the live step only.** Collect every document in your own screens and let the contact do the biometric verification through the link.
* **Start in your app, finish on the phone.** Anything the contact started on your screens is already received when they open the link on their phone.

Whichever way an item arrives, the requirement it satisfies changes at once, the page stops asking for it, and the affiliation is submitted when nothing is left.

## Next steps

<CardGroup cols={2}>
  <Card title="Requirements" icon="list-check" href="/onboarding/requirements">
    Types, statuses, origins and what each provider asks for.
  </Card>

  <Card title="Statuses and events" icon="bell" href="/onboarding/statuses-and-events">
    Follow the affiliation and every building block by webhook.
  </Card>

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

  <Card title="API reference" icon="code" href="/api-reference/onboarding-requirements/list-onboarding-requirements">
    Every route, parameter and response.
  </Card>
</CardGroup>


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