---
title: "Account Applications"
description: "2 operations under /v1/account-applications (POST GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript."
---

Base URL: `https://api.codespar.dev`

Every operation below requires a Bearer token. See [Authentication](/docs/concepts/authentication).

### POST `/v1/account-applications`

<Endpoint method="POST" path="/v1/account-applications" base="https://api.codespar.dev" />

Open a consumer account application

Opens a consumer's account application at the provider, and this is the path by which a consumer comes to have an account and a Pix funding source.

**The response is not the account, it is the application.** What comes back is `verification_id` and a `status` from the provider; the account and the funding source appear when `GET /v1/account-applications/{id}` is read after the approval: that read is what provisions. A flow that expects an account here keeps waiting.

**The `disclosure` is there to be rendered.** It names the institution that holds the account and CodeSpar's role as correspondente; the text comes in pt-BR to appear as it is, not to be rewritten.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `buyer` | object | yes | The identity fields the provider asks for: `documentNumber` (CPF or CNPJ), full name, date of birth, address. Passed through as an object: the handler does not validate it field by field here, and whatever the provider refuses comes back in the provider's own refusal. Two exceptions are checked before anything is sent: the document's check digits, and, for a natural person (CPF) in a live project, `financialDetails` = `{ declaredIncome, occupation, netWorth }` in the provider's codes. That is the titular's own declaration, and CodeSpar never fills it in for them. |
| `consumer_id` | `string` | no | The consumer who becomes the owner of the account. It becomes the onboarding's `clientCode` and the owner of the funding source the approval produces. |
| `opt_ins` | array of `string` | no | Opt-ins collected on the same screen. Today only `dda` does anything, and it requires the document to be this consumer's verified one AND to be on a signed `dda_allowlist`. Without that the registration is refused and the rest of the onboarding continues. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | The body did not match the schema, the document did not pass the check, or a natural person's onboarding in a live project came without `buyer.financialDetails` (one issue per missing code, its `path` naming it). BARE BODY, not the `{ error: { code, message } }` envelope: this handler writes its failure inline and there is no `request_id`. |
| `403` | object \| object | The organization's policy decided, and nothing was dispatched. `approval_required`: a rule holds the call for a person; `approval_id` is the row in the approvals queue and `expires_at` when it lapses. Approving it runs the call then, through the approvals lane, not this request. `policy_denied`: a deny rule (or the non-overridable deny-list) refused it; `rule_id` names the rule. Bare body. |
| `409` | object | ONBOARDING COLLISION, and it is caller state and not a provider failure, which is why 409 and not 502. `provider_code` carries the provider's code (OBE064 proposal already open, CBE022 account already exists, OBE062 clientCode already linked) so the audit trail records the ground for the refusal. Bare body. |
| `502` | object | The provider call failed, or the router landed on an institution that is not the expected one. Bare body; `message` carries the underlying sentence and is what separates a provider outage from a refusal by the provider. |
| `503` | object | The policy engine could not answer, so the call was refused fail-closed and nothing was dispatched. Infrastructure, not a rule: retry. Bare body. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `dda_opt_in` | object | no | Present only when `opt_ins` asked for `dda`. `pending` is the normal path: the registration is asynchronous and turns `active` when the provider's confirmation arrives, so treat pending as in progress and not as a failure. |
| `disclosure` | `object,null` | yes | Null when the router did not land on an institution with registered text. |
| `hosted_url` | `string,null` | yes | The verification page, when the provider uses one. Null when it is all done through the API. |
| `status` | `string` | yes | Where the proposal stands at the provider, in the provider's own word. |
| `verification_id` | `string` | yes | The provider's proposal id. It is what `GET /v1/account-applications/{id}` reads. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X POST https://api.codespar.dev/v1/account-applications \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "buyer": {},
       "consumer_id": "csm_0000000000000000",
       "opt_ins": [
         "string"
       ]
     }'
```

</Tab>
<Tab value="HTTP">

```http
POST /v1/account-applications HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "buyer": {},
  "consumer_id": "csm_0000000000000000",
  "opt_ins": [
    "string"
  ]
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/account-applications",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "buyer": {},
      "consumer_id": "csm_0000000000000000",
      "opt_ins": [
        "string"
      ]
    },
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/account-applications", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "buyer": {},
    "consumer_id": "csm_0000000000000000",
    "opt_ins": [
      "string"
    ]
  }),
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const r = await cs.api.response("post", "/v1/account-applications", {
  body: {
    buyer: {},
    consumer_id: "csm_0000000000000000",
    opt_ins: [
      "string"
    ]
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "verification_id": "verification_0000000000000000",
  "status": "string",
  "hosted_url": "https://example.com/hook",
  "disclosure": {
    "institution": "string",
    "institution_document": "string",
    "codespar_role": "correspondente",
    "text": "string"
  },
  "dda_opt_in": {
    "status": "pending",
    "message": "string"
  }
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/account-applications" body={"{\"buyer\":{},\"consumer_id\":\"csm_0000000000000000\",\"opt_ins\":[\"string\"]}"} />

### GET `/v1/account-applications/{id}`

<Endpoint method="GET" path="/v1/account-applications/{id}" base="https://api.codespar.dev" />

Read the status of a consumer account application

Where a consumer's account onboarding stands, and — once the provider has approved it — the account and the Pix funding source it produced. Requires the `kyc:read` scope.

THIS IS NOT A PURE READ, and a caller should plan around that. On the approved branch the handler resolves the account from the provider and then persists: it inserts (or re-resolves) the consumer's `pix-celcoin` funding source, registers a Pix key for it, and anchors the consumer record. Every approval path in the product converges on that one place, so polling this endpoint is how an application becomes a usable funding source. It is idempotent — the funding source id is derived from the consumer id, and re-provisioning merges into the existing row rather than replacing it, so a Pix key already registered is not erased.

`document_number` IS REQUIRED, and the reason is ownership. It is never used to address the provider: the handler loads the proposal recorded under this id and compares. A mismatch, or an id with no recorded proposal, is refused before any provider call and provisions nothing — the attack it closes is an attacker's own approved proposal presented with a victim's document.

THOSE OWNERSHIP REFUSALS ARRIVE AS 502, not 403. The handler's catch is blanket, so a typed ownership refusal is relabelled `kyc_status_failed` with the refusal's own sentence in `message`. Read the message, not just the code, before treating a 502 here as a provider outage.

The query is NOT schema-validated. The handler reads `request.query` directly and checks only that `document_number` is present; any other parameter is ignored rather than rejected.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | The provider's proposal id, returned by `POST /v1/account-applications`. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `consumer_id` | `string` | no | A fallback only. The recorded proposal's own consumer id wins; this value is used when the recorded row carries none. Omitted, the proposal id stands in. |
| `document_number` | `string` | yes | REQUIRED. The CPF or CNPJ the caller claims this application is about, checked against the document recorded for the proposal. Formatting is normalized before the comparison. |

**Responses**

| Status | Body | Description |
|---|---|---|
| `200` | object | OK |
| `400` | object | Bad Request — `document_number` was not supplied. A BARE error body, not the `{ error: { code, message } }` envelope: this handler writes its failures inline and there is no `request_id` in the body. |
| `502` | object | Bad Gateway. The provider call failed — and ALSO where the typed ownership refusals land, because the handler's catch is blanket. `message` carries the underlying sentence and is the only thing that separates a provider outage from a refusal to bind a document the proposal did not verify. Bare body, no `request_id`. |

**Response `200`**

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | `string,null` | yes | The provisioned account number once one exists, else null. Stringified: the provider returns it as either a string or a number. The branch is resolved in the same fetch and is NOT returned by this operation; it is returned as the funding source's `branch`. |
| `funding_source_id` | `string,null` | yes | The `pix-celcoin` funding source, or null before one exists. Deterministic — `fs_celcoin_` followed by the consumer id, one account per consumer — so a repeat call resolves the same row instead of opening a second one. |
| `hosted_url` | `string,null` | yes | The documentoscopia webview the consumer opens, taken from the proposal's first `documentscopys` entry. Null on `rejected` and on `approved`. |
| `pix_key` | `string,null` | yes | The DICT entry registered for the account, once registration has succeeded. Null before that. |
| `pix_key_registration_error` | `string` | no | ABSENT from the body unless Pix-key registration failed for an account that is otherwise real and usable. Deliberately non-blocking and deliberately not swallowed: the account works, the wallet's own lazy registration retries the step the next time the consumer needs to receive, and the failure is surfaced here so a caller can log or alert on it. Its presence does not make the application unapproved. |
| `pix_key_type` | `string,null` | yes | — |
| `status` | `"pending"` \| `"documentscopy_pending"` \| `"approved"` \| `"rejected"` | yes | FOUR values, and the exported `CelcoinOnboardingStatus` union has five. `unknown` is in the type and is not in this enum because `resolveCelcoinOnboarding` — the only producer this route has — never returns it: its four `return` statements carry `rejected`, `documentscopy_pending`, `pending` (twice) and `approved`. The only other `"unknown"` in that file is a `conflict_case`, which is a different field on a different shape. `pending` is also the answer when the proposal is past the known pending states but no account number could be parsed from the provider's fetch response. That case is logged with the raw body truncated, so a genuine not-minted-yet and a parse that is wrong look the same to the caller and different in the log. |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","HTTP","Python","TypeScript","SDK"]}>
<Tab value="curl">

```bash
curl -X GET https://api.codespar.dev/v1/account-applications/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
```

</Tab>
<Tab value="HTTP">

```http
GET /v1/account-applications/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
```

</Tab>
<Tab value="Python">

```python
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/account-applications/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch("https://api.codespar.dev/v1/account-applications/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
```

</Tab>
<Tab value="SDK">

```ts
const result = await cs.api.get("/v1/account-applications/{id}", {
  path: {
    id: "accountapplication_0000000000000000"
  },
  query: {
    document_number: "string"
  }
});
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "status": "pending",
  "hosted_url": "https://example.com/hook",
  "account": "string",
  "funding_source_id": "fundingsource_0000000000000000",
  "pix_key": "string",
  "pix_key_type": "string",
  "pix_key_registration_error": "string"
}
```

</SplitPane>
</Split>

<TryIt method="GET" path="/v1/account-applications/{id}" />

