Skip to main content

Account Applications

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

12 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

POST /v1/account-applications

POSThttps://api.codespar.dev/v1/account-applications

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 BaaS tomadora; the text comes in pt-BR to appear as it is, not to be rewritten.

Request body

FieldTypeRequiredDescription
buyerobjectyesThe 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. files (A5b): the documents, [{ type, data }], data a URL the provider downloads, valid for at least one hour, PDF/PNG/JPEG (CONTRATO_SOCIAL is PDF). Natural person: RG/CNH/RNE and SELFIE; legal person: also CONTRATO_SOCIAL, and PROCURACAO_PODERES when an owner is a REPRESENTANTE. Checked before anything is sent. A test project that sends none gets the Test sample documents; a live project never does, and a live request naming a sample document is refused.
consumer_idstringnoThe 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_insarray of stringnoOpt-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

StatusBodyDescription
200objectOK
400object | objectThe 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) — invalid_body. Or the documents were refused before sending (A5b): onboarding_sample_file_in_live for a live request naming a Test sample document or a public specimen, onboarding_files_invalid for a malformed files list, a format the provider does not take, or a missing required document. BARE BODY, not the { error: { code, message } } envelope: this handler writes its failure inline and there is no request_id.
403object | object | objectTwo answers. onboarding_capability_inactive (E2): this test project's onboarding capability is not active, so no proposal is opened; an owner or admin of the project asks for its onboarding access in the dashboard. Nothing was sent. And the organization's policy: 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.
409objectONBOARDING 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. CBE022 is attributed only on this organization's own evidence: onboarding_account_exists_in_project, onboarding_account_exists_elsewhere (another project of the organization) or onboarding_account_exists_unattributed (no evidence; not concluded to be anyone's). E2 also answers here onboarding_attempt_in_progress (another request holds this clientCode's reservation; repeat later), onboarding_client_code_conflict for a clientCode this project used for another document, and onboarding_unreserved. On the OBE062 recovery, consumer_onboarded_in_sibling_project: the consumer is already onboarded in another project of the caller's organization, and the body carries holding_project_id. Bare body.
422objectThe provider refused the proposal itself (e.g. CBE007 fullName not complete, OBE166 contrato social not a PDF). No proposal exists. Bare body.
429object | objectE2: the sandbox proposal quota is used up, 2 per test project in 30 days (window: project_30d) or 30 per São Paulo day across every organization (window: global_day). Nothing was reserved or sent. A refused or locally invalid proposal never counts. Or project_access_call_cap: the sandbox call caps (60 per clock hour and 300 per São Paulo day per project, 3,000 per day globally) refused the call before it was sent; Retry-After is the seconds until that fixed window turns (the next clock hour, or São Paulo midnight). Bare body.
502objectThe 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. E2: onboarding_provider_failure is the provider refusing for a reason that is not the titular's data (its credential, product, our payload, "try later", or a code not in the evidence table), with provider_code; no proposal exists, and it is retriable.
503object | object | object | objectTwo answers. onboarding_outcome_unknown: the proposal may have reached the provider and its answer was lost, and a query by this request's clientCode found nothing yet. Nothing was sent again. Repeat the SAME request after Retry-After: the same consumer and document reuse the clientCode, so no second proposal can be opened. E2: onboarding_paused, the server paused opening sandbox accounts (an operational switch); no proposal is opened, one already opened can still be read; Retry-After 3600. onboarding_calls_unavailable, the register of onboarding calls could not be written; Retry-After 30. policy_denied: The policy engine could not answer, so the call was refused fail-closed and nothing was dispatched. Infrastructure, not a rule: retry. Bare body. Bare body.

Response 200

FieldTypeRequiredDescription
already_submittedtruenoPresent when the create's own answer was lost (or the provider said the proposal exists) and the proposal was found by this request's clientCode instead of being sent again.
dda_opt_inobjectnoPresent 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.
disclosureobject,nullyesNull when the router did not land on an institution with registered text.
hosted_urlstring,nullyesThe verification page, when the provider uses one. Null when it is all done through the API.
next_stepobject,nullyesWhat to do next: read GET /v1/account-applications/{id} later. There is no account until the proposal reaches RESOURCE_CREATED.
sample_filestruenoPresent in a test project that sent no files: the Test sample documents were sent.
statusstringyesWhere the proposal stands at the provider, in the provider's own word.
verification_idstringyesThe provider's proposal id. It is what GET /v1/account-applications/{id} reads.
Example request
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"
       ]
     }'
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"
  ]
}
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()
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();
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, 422),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "verification_id": "verification_0000000000000000",
  "status": "string",
  "hosted_url": "https://example.com/hook",
  "disclosure": {
    "institution": "string",
    "institution_document": "string",
    "codespar_role": "tomadora",
    "text": "string"
  },
  "next_step": {
    "code": "in_review_check_later",
    "verification_id": "verification_0000000000000000",
    "message": "string",
    "retry_after_seconds": 0
  },
  "sample_files": true,
  "already_submitted": true,
  "dda_opt_in": {
    "status": "pending",
    "message": "string"
  }
}

GET /v1/account-applications/{id}

GEThttps://api.codespar.dev/v1/account-applications/{id}

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 409 with their own code (document_ownership_unproven, onboarding_document_mismatch), and so does a proposal recorded under ANOTHER project of the organization, in the same words as an unknown id. Only RESOURCE_CREATED binds an account; the proposal is read a bounded number of times when it is not visible yet, and an inconclusive read answers pending with next_step, never a new proposal.

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

NameTypeRequiredDescription
idstringyesThe provider's proposal id, returned by POST /v1/account-applications.

Query parameters

NameTypeRequiredDescription
consumer_idstringnoA 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_numberstringyesREQUIRED. 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

StatusBodyDescription
200objectOK
400objectBad 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.
403objectE2: this test project's onboarding capability is not active, so the proposal is not read. Nothing was sent.
409objectE2 adds the binding refusals: account_bound_elsewhere (the account is, or was, bound to another project), account_binding_closed (this project closed its binding; never reopened automatically), consumer_holds_other_binding. A refusal decided before anything is bound: the id has no proposal recorded for this project (document_ownership_unproven), the document does not match the one the proposal verified, or the account the provider returned names another document (onboarding_document_mismatch), or the funding source written did not confirm as this consumer's in this project (funding_source_binding_unconfirmed). Or the consumer is already onboarded in another project of the caller's organization (consumer_onboarded_in_sibling_project), refused before any write, and the body carries holding_project_id. Bare body.
429objectE2: the sandbox call caps (60 per clock hour and 300 per São Paulo day per project, 3,000 per day globally) refused the status read before it was sent. Retry-After is the seconds until that fixed window turns (the next clock hour, or São Paulo midnight). Bare body.
502objectBad Gateway. The provider call failed. message carries the underlying sentence. Bare body, no request_id.
503objectE2: the register of onboarding calls could not be written, so the status was not read (fails closed). Retry-After: 30. Bare body.

Response 200

FieldTypeRequiredDescription
accountstring,nullyesThe 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_idstring,nullyesThe 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.
funding_source_status"active" | "held"yesThe funding source's status, or null before one exists. held for an account opened for a Test project: the account is bound to the project and no payment, receiving or consent uses it, and it has no Pix key yet. active in Live, or for an account that was already active.
hosted_urlstring,nullyesThe documentoscopia webview the consumer opens, taken from the proposal's first documentscopys entry. Null on rejected and on approved.
next_stepobject,nullyesPresent while the application has not concluded: read it again later with the same id. Never open a new application for the document; the provider allows one proposal per document every 24 hours. Null on approved and rejected.
pix_keystring,nullyesThe DICT entry registered for the account, once registration has succeeded. Null before that.
pix_key_registration_errorstringnoABSENT 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_typestring,nullyes—
proposal_statusstringyesThe provider's own status for the proposal (RESOURCE_CREATED, APPROVED, PENDING, ...), or NOT_VISIBLE_YET.
rejected_reasonstringnoOn rejected, the provider's reason when it gives one, as "CODE: message".
status"pending" | "documentscopy_pending" | "approved" | "rejected"yesFOUR 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. approved means an account EXISTS: the proposal reached RESOURCE_CREATED, the account was read and bound. A proposal the provider reports as APPROVED has no account yet and answers pending (A5b), as does any status outside the provider's published vocabulary, a proposal not visible yet after the bounded reads (proposal_status NOT_VISIBLE_YET), and a RESOURCE_CREATED proposal whose account number could not be parsed from the provider's fetch response. That last 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.
Example request
curl -X GET https://api.codespar.dev/v1/account-applications/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/account-applications/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
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()
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();
const r = await cs.api.response("get", "/v1/account-applications/{id}", {
  path: {
    id: "accountapplication_0000000000000000"
  },
  query: {
    document_number: "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);
}
Example response 200
application/json
{
  "status": "pending",
  "proposal_status": "string",
  "next_step": {
    "code": "in_review_check_later",
    "verification_id": "verification_0000000000000000",
    "message": "string",
    "retry_after_seconds": 0
  },
  "rejected_reason": "string",
  "hosted_url": "https://example.com/hook",
  "account": "string",
  "funding_source_id": "fundingsource_0000000000000000",
  "funding_source_status": "active",
  "pix_key": "string",
  "pix_key_type": "string",
  "pix_key_registration_error": "string"
}
Account Applications | CodeSpar