KYC
2 operations under /v1/kyc/onboard (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.
POST /v1/kyc/onboard
https://api.codespar.dev/v1/kyc/onboardOpen a consumer account application (deprecated path)
DEPRECATED alias of POST /v1/account-applications (ent#979), kept for two releases. Same handler, same required scope; switch the path and nothing else changes.
The response is the PROPOSAL, not the account: it returns verification_id and the provider's status, and it is the read by id, after approval, that provisions the account and the Pix funding source. disclosure comes in pt-BR, to be rendered verbatim.
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. |
consumer_id | string | no | The consumer that becomes the owner of the account and of the funding source. |
opt_ins | array of string | no | Opt-ins from the same screen. Today only dda does anything, and it has governance of its own. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | object | Body outside the schema, a document rejected at the check, or buyer.files refused before sending (onboarding_sample_file_in_live, onboarding_files_invalid). RAW BODY, not the { error: { code, message } } envelope. |
403 | object | object | object | onboarding_capability_inactive (E2): this test project's onboarding capability is not active; nothing was sent. Or 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. |
409 | object | Onboarding collision: caller state, not a provider failure. provider_code carries OBE064, CBE022 or OBE062; CBE022 is attributed only on this organization's evidence. E2 adds onboarding_attempt_in_progress, onboarding_client_code_conflict 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. Raw body. |
422 | object | The provider refused the proposal (e.g. CBE007, OBE166). No proposal exists. Raw body. |
429 | object | object | E2: the sandbox proposal quota is used up (2 per test project in 30 days, or 10 per São Paulo day across every organization), or the sandbox call caps were reached (project_access_call_cap, with Retry-After until the fixed window turns: the next clock hour, or São Paulo midnight). Nothing was reserved or sent. Raw body. |
502 | object | The provider call failed. Raw body; read message before treating it as an outage. onboarding_provider_failure (E2) is a provider refusal that is not about the titular's data, with provider_code; retriable. |
503 | object | object | object | object | onboarding_outcome_unknown: the proposal's answer was lost and nothing was found by its clientCode yet; nothing was sent again. Repeat the same request after Retry-After. E2: onboarding_paused (opening sandbox accounts is paused on this server; reading an opened proposal still works; Retry-After 3600) and onboarding_calls_unavailable (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. Raw body. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
already_submitted | true | no | — |
dda_opt_in | object | no | — |
disclosure | object,null | yes | — |
hosted_url | string,null | yes | — |
next_step | object,null | yes | — |
sample_files | true | no | — |
status | string | yes | — |
verification_id | string | yes | — |
curl -X POST https://api.codespar.dev/v1/kyc/onboard \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"buyer": {},
"consumer_id": "csm_0000000000000000",
"opt_ins": [
"string"
]
}'POST /v1/kyc/onboard 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/kyc/onboard",
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/kyc/onboard", {
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/kyc/onboard", {
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);
}{
"verification_id": "verification_0000000000000000",
"status": "string",
"hosted_url": "https://example.com/hook",
"next_step": {
"code": "in_review_check_later",
"verification_id": "verification_0000000000000000",
"message": "string",
"retry_after_seconds": 0
},
"sample_files": true,
"already_submitted": true,
"disclosure": {
"institution": "string",
"institution_document": "string",
"codespar_role": "tomadora",
"text": "string"
},
"dda_opt_in": {
"status": "pending",
"message": "string"
}
}GET /v1/kyc/onboard/{proposalId}/status
https://api.codespar.dev/v1/kyc/onboard/{proposalId}/statusPoll a KYC account application
DEPRECATED alias of GET /v1/account-applications/{id} (ent#979), kept for two releases. Same handler, same required scope; switch the path and nothing else changes. The canonical path spells this same value {id}.
Poll the KYC application and, once the provider approves it, the account and funding source that were provisioned from it. This read is not read-only underneath: approval is what provisions the funding source and registers the Pix key, so the first poll that finds an approved application is the call that creates them. Provisioning is idempotent, so polling again does not create a second one.
Possession is proved by the recorded proposal, never by the query. An application with no recorded proposal is refused rather than trusted, and a document_number that disagrees with the recorded document is refused before any provider call — an approved application of one's own plus somebody else's document number buys nothing.
Requires the kyc:read scope.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
proposalId | string | yes | The application id the create call returned. It addresses a recorded proposal, and an id with no recorded proposal is refused rather than looked up. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
consumer_id | string | no | The consumer to bind a provisioned account to, when the recorded proposal does not already name one. Precedence runs the other way from what the parameter suggests: the recorded proposal's consumer WINS, and this value is the fallback used only when the proposal recorded none. Omitting it falls back to the id in the path. |
document_number | string | yes | REQUIRED, and its absence is a 400 rather than a default. It is a CLAIM about what the application is for, checked against the document the recorded proposal actually verified, and it never addresses the provider: the account is always fetched for the proposal's own document. A claim that does not match the recorded one is refused before any provider call and nothing is written. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request — document_number was absent. A bare body with one key, not the { error: { code, message } } envelope the newer routes use. |
403 | object | object | Forbidden — the provisioning write named a resource outside the organization and project these credentials authenticate for (write_scope_refused; the body is the tenant control's own two keys and carries nothing from the database's message). Or, E2, this test project's onboarding capability is not active, so the proposal is not read (onboarding_capability_inactive). |
409 | object | Conflict — E2's binding refusals (account_bound_elsewhere, account_binding_closed, consumer_holds_other_binding), or a refusal decided before anything is bound: no proposal recorded for this project under this id, a document_number (or an account read) that does not match the document the proposal verified, or a funding source that did not confirm as this consumer's in this project. Or consumer_onboarded_in_sibling_project: the consumer is already onboarded in another project of the caller's organization, refused before any write, and the body carries holding_project_id. |
429 | object | E2: 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. |
502 | object | Bad Gateway — the provider lookup or the provisioning that follows it failed. |
503 | object | E2: the register of onboarding calls could not be written, so the status was not read (fails closed). Retry-After: 30. Bare body. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
account | string,null | yes | The provisioned account number, once one exists. |
funding_source_id | string,null | yes | The consumer's funding source, written when the application is approved. Provisioning is idempotent on a key derived from the consumer, so polling this endpoint repeatedly after approval returns the same id and creates nothing new. |
funding_source_status | "active" | "held" | yes | The 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_url | string,null | yes | The document-check webview link while it is outstanding; null otherwise. |
next_step | object,null | yes | While the application has not concluded: read it again later; never open a new one. |
pix_key | string,null | yes | The Pix key registered for the account, either just now or previously — registration returns early when the key already exists, so a repeated poll does not re-register. |
pix_key_registration_error | string | no | Present ONLY when the account was provisioned but its Pix-key registration failed. It is deliberately not an error status: the account is real and usable, and the key is retried lazily the next time the consumer needs to receive. Absent means either the key registered or there is no account yet — read pix_key to tell those apart. |
pix_key_type | string,null | yes | — |
proposal_status | string | yes | The provider's own status for the proposal, or NOT_VISIBLE_YET. |
rejected_reason | string | no | On rejected, the provider's reason when it gives one. |
status | "pending" | "documentscopy_pending" | "approved" | "rejected" | yes | Four values, and the fourth is not the type's fifth. The resolver that serves this route is the only producer, and enumerating every one of its returns gives exactly these four; unknown exists in the shared union and is returned by nothing on this path. documentscopy_pending means the holder still has to finish the document check, and hosted_url is where they finish it. pending is the widest of the four: it covers the provider's own PENDING and PROCESSING, APPROVED (adjudicated, no account yet), a status outside the provider's vocabulary, a proposal not visible yet after the bounded reads, and a RESOURCE_CREATED proposal whose account could not yet be read back. Poll again for all of them; next_step says so. Only RESOURCE_CREATED binds an account. |
curl -X GET https://api.codespar.dev/v1/kyc/onboard/{proposalId}/status \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/kyc/onboard/{proposalId}/status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/kyc/onboard/{proposalId}/status",
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/kyc/onboard/{proposalId}/status", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const r = await cs.api.response("get", "/v1/kyc/onboard/{proposalId}/status", {
path: {
proposalId: "proposal_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);
}{
"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"
}Contact Verifications
2 operations under /v1/consumers/{consumerId}/contact-verifications (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
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.