---
title: "KYC Onboarding"
description: "From zero to a provisioned consumer account: open the application, poll the status, and read what exists after approval. The order each handler runs in, the two shapes the POST answers in, and which refusal a retry actually fixes."
---

<MetaStrip items={[
  { label: "TIME", value: "~20 min" },
  { label: "STACK", value: (<><ServerChip name="HTTP / SDK REST" accent /><span style={{ color: "var(--color-fd-muted-foreground)", fontSize: 12 }}>no typed method</span></>) },
  { label: "SERVERS", value: (<><ServerChip name="kyc: celcoin" /></>) },
]} />

Two calls take a consumer from nothing to a provisioned account: open the application, then read it by id until the provider approves. The read is what provisions the account, the funding source and the Pix key.

<EventPipeline
  title="ONBOARDING LIFECYCLE"
  subtitle="Open the application → documentoscopy → poll → provisioned"
  nodes={[
    { tag: "1 · OPEN", name: "Submit the proposal", meta: "POST /v1/account-applications → 200 · verification_id", tone: "event" },
    { tag: "2 · VERIFY", name: "Consumer at the webview", meta: "hosted_url, only on the read by id", tone: "validation" },
    { tag: "3 · POLL", name: "Read by id", meta: "GET /v1/account-applications/{id}?document_number=", tone: "loop" },
    { tag: "4 · USE", name: "Account and Pix key", meta: "consumers · funding-sources · pix-keys", tone: "event" },
  ]}
  loopSteps={[
    { tool: "provisionCelcoinFundingSource", description: "inserts fs_celcoin_<consumer_id>, deterministic id" },
    { tool: "anchorVerifiedConsumer", description: "binds document to consumer, best effort" },
    { tool: "tryRegisterPixKey", description: "DICT registration, best effort" },
  ]}
  note="the read by id is a write: the first poll that finds approval is the call that provisions"
/>

## Prerequisites

```bash
npm install @codespar/sdk
```

Two scopes. `kyc:write` for the POST, `kyc:read` for the GET. Only `kyc:read` is written into the served document; `kyc:write` is enforced by the route and appears nowhere in the spec. A key carrying the `*` wildcard passes regardless. See [Authentication](/docs/concepts/authentication).

The organization needs the Celcoin connection wired. Without it the POST answers `502` with `no_eligible_providers` or `credential_unavailable`, and no retry ever clears that.

<Callout type="warn" title="The POST only exists from @codespar/sdk 0.16.0">
`@codespar/sdk` 0.15.0 carries 227 operations in its generated table and only the two GETs of this journey. `cs.api.post("/v1/account-applications", ...)` does not exist there. 0.16.0 carries 276 and closes the gap. [The SDK page for this group](/docs/api/sdk/rest-client/account-applications) still says the POST is available from 0.12.0 and that npm is on 0.15.0. Both sentences are wrong. Pin 0.16.0.
</Callout>

## The whole journey in one file

```typescript title="kyc-onboarding.ts"
import { CodeSpar } from "@codespar/sdk";

const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });

const consumerId = "csm_0a1b2c3d4e5f6071";
const documentNumber = "11144477735";

// The recovery branch of the POST. The generated types do not declare it:
// they say verification_id is a required string.
type Recovered = {
  verification_id: null;
  account: string;
  funding_source_id: string;
  pix_key: string | null;
  pix_key_type: string | null;
  already_submitted: true;
  pix_key_registration_error?: string;
};

// 1. Open the application. Needs kyc:write.
const opened = await cs.api.post("/v1/account-applications", {
  body: {
    consumer_id: consumerId,
    buyer: { documentNumber, fullName: "Maria Souza" },
    opt_ins: ["dda"],
  },
});

// 2. Two shapes come back here. A null verification_id is the recovery branch:
//    the account already existed and was adopted, so there is nothing to poll.
if (opened.verification_id === null) {
  const adopted = opened as unknown as Recovered;
  console.log(adopted.account, adopted.funding_source_id, adopted.pix_key);
} else {
  const verificationId = opened.verification_id;

  // 3. Render the disclosure verbatim, in pt-BR, exactly as it arrives.
  //    institution_document can be null and a null renders as nothing.
  console.log(opened.disclosure);

  // 4. Poll. document_number is required. This call is what provisions.
  let state = await cs.api.get("/v1/account-applications/{id}", {
    path: { id: verificationId },
    query: { document_number: documentNumber, consumer_id: consumerId },
  });

  while (state.status === "pending" || state.status === "documentscopy_pending") {
    if (state.status === "documentscopy_pending" && state.hosted_url) {
      // Send the consumer to state.hosted_url. It appears only here.
      console.log(state.hosted_url);
    }
    await new Promise((r) => setTimeout(r, 15_000));
    state = await cs.api.get("/v1/account-applications/{id}", {
      path: { id: verificationId },
      query: { document_number: documentNumber, consumer_id: consumerId },
    });
  }

  if (state.status === "rejected") {
    throw new Error("the provider rejected the application");
  }

  // 5. approved. account, funding_source_id and pix_key are filled in.
  //    pix_key_registration_error is absent unless DICT registration failed,
  //    and its presence does not make the account unusable.
  console.log(state.account, state.funding_source_id, state.pix_key);
}

// 6. After approval, the consumer is the hub for everything else.
const consumer = await cs.api.get("/v1/consumers/{id}", {
  path: { id: consumerId },
});
console.log(consumer.wallet_id, consumer.funding_source_ids, consumer.mandate_ids);

// 7. The branch lives only on the funding source, in metadata.
const fundingSource = await cs.api.get("/v1/funding-sources/{id}", {
  path: { id: `fs_celcoin_${consumerId}` },
});
console.log(fundingSource.metadata);

// 8. The live DICT inventory, read at the provider on every call.
const pixKeys = await cs.api.get("/v1/consumers/{consumerId}/pix-keys", {
  path: { consumerId },
});
console.log(pixKeys);
```

## Step 0. The consumer row is optional

You can create the holder first with `POST /v1/consumers`, without a document. Nothing forces you to. The onboarding handler never checks that the row exists, and approval is what creates and anchors the consumer: `anchorVerifiedConsumer` runs an `INSERT ... ON CONFLICT` during the poll that finds the proposal approved. See [the consumers reference](/docs/api/reference/consumers).

## Step 1. Open the application

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

`buyer` is the only required field. Everything else you put inside it passes through to the provider untouched, and what the provider refuses comes back as the provider's refusal.

The handler runs in this order, and the order is what tells you what exists after a refusal:

1. Zod parse of the body. A failure here is `400 invalid_body` and nothing left the process.
2. `extractBuyerDocument` reads, in this order, `documentNumber`, then `document`, then `cpf`, then `cnpj`. The first one present wins.
3. `effectiveConsumerId` is `consumer_id` when you sent one, otherwise the derived `${orgId}:${document}`.
4. The person type comes from the canonical length of the document. Fourteen characters means CNPJ, including the alphanumeric CNPJ of IN RFB 2229/2024, and that picks `onboarding-business` over `onboarding`.
5. Check digits are verified **locally**. A CPF or CNPJ that does not close is `400 invalid_body` with `issues[0].message` reading `CPF check digits do not verify`, and nothing was sent to the provider.
6. `strategy.execute('codespar_kyc')` calls the provider.
7. If the router landed somewhere other than Celcoin, the route answers `502 provider_error`. The proposal was still opened wherever the router landed.
8. `recordKycOnboardingProposal` writes the proposal-to-document link. Best effort.
9. If `opt_ins` includes `dda`, `tryDdaOptIn` runs **now**, after submission.
10. The response.

<Callout type="warn" title="Always pass consumer_id">
Omit it and the id is derived as `${orgId}:${documentNumber}`. The raw CPF or CNPJ then lives inside the consumer id, and the consumer id is what composes the funding source id (`fs_celcoin_${consumerId}`), so the document ends up in logs, in navigation and in every response that names the funding source. Passing `consumer_id` is not optional in practice.
</Callout>

<Callout type="warn" title="A malformed buyer comes back as 502, not 400">
The per-field validation of `buyer` happens in the transform, not in the route schema. A `buyer` with no `fullName` or no document throws inside the transform, and `real-strategy.ts` wraps any throw as `MetaToolStrategyError('provider_error')`. The caller sent a bad body and receives a `502` naming the provider. Read `message` before you blame the provider for a `502` on the POST.
</Callout>

## Step 2. The POST answers in two shapes

The served document describes one. There are two.

**Normal shape.** `verification_id` is a string, `status` is the provider's word, `hosted_url` is null, `disclosure` is the text to render. The response is the proposal, not the account.

**Recovery shape.** When the provider answers OBE062 and local recovery finds an account of ours, the route answers `200` with `verification_id: null` plus `account`, `funding_source_id`, `pix_key`, `pix_key_type`, `already_submitted: true`, and sometimes `pix_key_registration_error`. Here the response **is** the account. None of those fields exist in the served schema, which declares `verification_id` as a required non-nullable string.

<Callout type="warn" title="Branch on verification_id, not on the status code">
Both shapes are `200`. The branch you land in when you re-onboard the same consumer is the one the document does not describe. Code that reads `verification_id` and polls it will poll `null` forever. Check for null first.
</Callout>

## Step 3. Keep the verification_id

It is the only key the read has. There is no collection route to find it again, and no route to list applications at all. Persist it next to the consumer before you do anything else.

<Callout type="warn" title="A normal verification_id can already be unreadable">
`recordKycOnboardingProposal` is best effort. If the write to `celcoin_onboarding_proposals` fails, the POST still returns a perfectly ordinary `verification_id`, and every future read of that id refuses with `502 kyc_status_failed` carrying `document_ownership_unproven`. The warning goes to the server log only. The caller gets no signal at all.
</Callout>

## Step 4. Render the disclosure

The GET and the POST both return `disclosure`, and it carries `institution`, `institution_document`, `codespar_role` valued `correspondente`, and `text` in pt-BR. Render it as it arrives. `institution_document` can be null, and a null renders as nothing rather than as an empty label.

The handler repeats the disclosure on the read by id on purpose, for a consumer who resumes the onboarding on a second device. That field is real and it is absent from both GET schemas in the served document.

## Step 5. Documentoscopy

`hosted_url` on the POST is always null. It is a fixed `hosted_url: null` in the transform, not the "null when everything is done by API" the served description suggests. The webview link exists only on the read by id, and only while the status is `documentscopy_pending`. It comes from the proposal's first `documentscopys` entry, and it is null again once the application is approved or rejected.

## Step 6. Poll the read by id

```bash
curl -H "Authorization: Bearer $CODESPAR_API_KEY" \
  "https://api.codespar.dev/v1/account-applications/$VERIFICATION_ID?document_number=11144477735&consumer_id=csm_0a1b2c3d4e5f6071"
```

`document_number` is required. Its absence is `400 document_number_required`, a single field in a raw body.

Order inside `resolveCelcoinOnboarding`:

1. Load the proposal filtering by `org_id`. With no stored row, the call refuses before touching the provider.
2. Compare the canonical document against the stored one. A mismatch refuses and writes nothing.
3. From here on everything uses the **stored** facts: document, consumer, project, environment. The arguments are not used again.
4. Fetch the proposal at the provider.
5. A provider status matching `REPROV`, `REJECT`, `DENIED`, `RECUS`, `ERROR` or `CANCEL` becomes `rejected`. `PENDING_DOCUMENTSCOPY` becomes `documentscopy_pending`. `PENDING`, `PROCESSING` and an empty status become `pending`.
6. Past the pending states, fetch the account, on the personal or the business endpoint according to the stored document. No readable account means `pending`, with the raw body truncated into the log.
7. `provisionCelcoinFundingSource`.
8. `anchorVerifiedConsumer`, best effort.
9. `tryRegisterPixKey`, best effort.
10. `approved`.

<Callout type="warn" title="Stop polling and the consumer stays unprovisioned">
The read by id is not a pure read. The first poll that finds the proposal approved is the call that inserts the funding source, registers the Pix key and creates the holder. A client that stops polling leaves the consumer without an account until the Celcoin onboarding webhook reconciles it internally, or until someone calls the meta-tool. There is no published event you can wait on instead.
</Callout>

<Callout type="warn" title="The two status fields do not share a vocabulary">
The POST returns the provider's word in upper case, produced by `String(r.status ?? 'PROCESSING').toUpperCase()`. The GET returns one of four normalized lower-case values. One `switch` over both is always wrong.
</Callout>

| Where | Values |
|-------|--------|
| POST `status` | The provider's own word, upper case. `PROCESSING` when the provider said nothing. |
| GET `status` | `pending`, `documentscopy_pending`, `approved`, `rejected`. |

### pending is the widest bucket of the four

It covers `PENDING` and `PROCESSING` at the provider, it covers the provider returning no status at all, and it covers "past the pending states, but no account number could be parsed out of the fetch response". That third case is logged with the body truncated at 300 characters. An account that is genuinely not minted yet and a parse that is wrong look identical to the caller.

### The query is not validated by a schema

The handler reads `request.query` directly and only checks that `document_number` is present. Any other parameter is ignored in silence rather than refused. A `document_number` made only of punctuation passes the presence check and then canonicalizes to an empty string, which skips the ownership comparison entirely. The damage is contained, because the proposal is loaded filtered by `org_id` and the account is always fetched with the stored document, but "the document is always compared" is not true as written.

`consumer_id` in the query works backwards from what its name suggests. The consumer stored on the proposal wins. The parameter is used only when the proposal stored none. Omit both and the proposal id itself takes that slot.

## Step 7. What exists after approval

`GET /v1/consumers/{id}` is the hub. It carries `wallet_id`, `funding_source_ids`, `mandate_ids` and `dda_subscription_documents`, with the document masked down to its last four characters. See [the consumers reference](/docs/api/reference/consumers).

`GET /v1/funding-sources/{id}` is the single line. The id is deterministic: `fs_celcoin_` followed by the consumer id. `balance_minor` on this route is always null, and `metadata` is echoed raw, which means it can carry the consumer's document. `branch` comes out of no route in this journey: it is resolved in the same fetch as the account and written into `metadata.branch` here. See [funding sources](/docs/api/reference/funding-sources).

`GET /v1/consumers/{consumerId}/pix-keys` reads the DICT inventory at the provider on every call. A failed read is an error, never an empty list. `POST` on the same path registers another key. See [Pix keys](/docs/api/reference/pix-keys).

`GET /v1/consumers/{id}/wallet` is the unified per-currency wallet, and `GET /v1/wallets/{id}/receive` returns a deposit address only for USDC. For BRL it returns guidance with `address`, `network` and `asset_contract` all null. See [Wallets](/docs/concepts/wallets) and [the wallets reference](/docs/api/reference/wallets).

From here `codespar_wallet` and `codespar_pay` work against the same `consumer_id`.

## Step 8. The DDA opt-in is not what the document says

Two claims in the served description of `opt_ins` do not hold.

The first onboarding returns the `dda_opt_in` block as `deferred`, not `pending`. `tryDdaOptIn` requires a document that is **already** verified, through the consumer anchor or through a funding source. On a first onboarding neither exists yet, so `checkDdaDocumentOwnership` refuses and the answer is `deferred`. `pending` happens on the OBE062 recovery branch or on a re-onboarding, not on the first pass.

The served description also says the DDA opt-in here requires a signed `dda_allowlist`. It does not. This path passes `authorization: { kind: 'onboarding_opt_in' }`, and no mandate is charged for it. The allowlist check raises `dda_document_unauthorized` on the governed paths only. That is `POST /v1/consumers/{consumerId}/dda/subscriptions`, the route to use when the opt-in is not enough. See [the DDA reference](/docs/api/reference/dda).

## Step 9. Lost the verification_id

The HTTP route cannot help: the id is in the path. The one published way back is the `codespar_kyc` meta-tool with `check_type=status` and **no** `verification_id`, passing `consumer_id` and `document_number`. `recoverExistingOnboarding` finds the proposal we opened for that consumer and document pair, and provisions.

```bash
codespar kyc -i '{
  "check_type": "status",
  "buyer": {},
  "consumer_id": "csm_0a1b2c3d4e5f6071",
  "document_number": "11144477735"
}'
```

<Callout type="warn" title="buyer is required even to poll a status">
The served `/meta-tools.json` declares `required: ["buyer", "check_type"]`. Measured on CLI 0.11.2, `codespar kyc --arg check_type=status --arg document_number=123 --arg verification_id=prop_1` answers `codespar_kyc requires buyer. Nothing was sent.` An empty `buyer: {}` passes validation and reaches the network. The empty object is not a placeholder, it is the way through.
</Callout>

<Callout type="warn" title="The meta-tool description contradicts the meta-tool">
The served description says a status poll with only a `document_number` provisions nothing and refuses with `document_ownership_unproven`. That is false whenever a proposal of ours exists: `executeKycStatus` without a `verification_id` calls `recoverExistingOnboarding`, which finds the open proposal and provisions. It refuses only when there is neither a binding nor a proposal. The error table on [the codespar_kyc page](/docs/concepts/meta-tools/kyc) repeats the same claim. The same served description also cites `check_type=identity|document` and a `sanctions` path, and neither is in the five-value enum any more.
</Callout>

The meta-tool and the HTTP route also disagree about privacy. The meta-tool returns `account_masked`, four bullets plus the last two digits, and `proposal_status`, the provider's raw word. The HTTP route returns the **whole** `account` and no `proposal_status`. Same journey, two contracts.

Scope enforcement differs too. The per-action scope that maps `codespar_kyc/onboarding` to `kyc:write` sits behind `META_TOOL_ACTION_SCOPES_ENFORCE`, which defaults to off. The HTTP routes charge the scope unconditionally.

## Refusals, and which one a retry fixes

Bodies on this journey are **raw**. There is no `{ error: { code, message } }` envelope and no `request_id`.

| Code | HTTP | What already happened | Does a retry fix it? |
|------|------|-----------------------|----------------------|
| `invalid_body` | 400 | Nothing. The parse and the check digits run before the provider. | No. Fix the body. `issues` names the field. |
| `document_number_required` | 400 | Nothing. Only on the GET. | No. Add the query parameter. |
| `write_scope_refused` | 403 | Nothing was written. | No. The write named a resource outside the org or project of the credential. Fix the project the request resolves to. |
| `onboarding_proposal_open_elsewhere` | 409 | A proposal is already open for this document, `provider_code` OBE064. | No. An identical retry returns the identical 409. Change the consumer or the document, or follow the proposal that exists. |
| `onboarding_account_exists_elsewhere` | 409 | The account already exists, `provider_code` CBE022. | No. Follow the account that exists. |
| `onboarding_client_code_conflict` | 409 | The `clientCode` is already bound and local recovery found nothing of ours, `provider_code` OBE062. | No. |
| `funding_source_owned_elsewhere` | 409 | Same OBE062 recovery arm, different body. No `provider_code`, which the served schema declares required. | No. The code marks this branch dead since ent#1036, and ent#874 is what retires it. |
| `kyc_status_failed` | 502 | Two different faults share this code on the GET. See below. | Depends on which. |
| `provider_error` | 502 | On the POST. Either the provider failed, or the router landed off Celcoin, or the transform threw on your `buyer`. | Only when it is genuinely the provider. Read `message`. |
| `no_eligible_providers` | 502 | Nothing. The organization has no provider connection wired. | Never. This is setup, and it still arrives as a 502. |
| `credential_unavailable` | 502 | Nothing. Same cause. | Never. |
| `transform_unknown`, `tool_unknown`, `invalid_args` | 502 | The `error` is the `code` of the typed strategy error. | No. |
| `kyc_onboard_failed` | 502 | The fallback, used only when the error was not typed. | Not blind. Quote the message. |

### kyc_status_failed is two faults under one code

The `catch` is blind, so the typed ownership refusals land in the same 502 as a provider outage:

- `document_ownership_unproven`, meaning no proposal is stored for that id. Retrying never helps.
- `onboarding_document_mismatch`, meaning the `document_number` is not the one the proposal verified. Retrying never helps.
- Anything else, meaning the provider failed. Retrying helps.

Only `message` separates them. Through the meta-tool the two ownership refusals arrive as themselves, marked `dispatch='unsent'`, which states that nothing was sent and nothing was written.

### The 409 family is caller state, not a provider outage

That is why these are `409` and not `502`. The provider answered. What it answered is that this document or this client code is already spoken for. An identical retry reproduces it exactly.

## There is no Idempotency-Key here

The handler never reads the header. The only `idempotency` string in the onboarding route is an internal key for the DDA step. Repeating the POST repeats the submission, and the provider answers OBE064, OBE062 or CBE022, so you get a `409` or the recovery branch.

The real idempotency sits on the other side. `fs_celcoin_${consumerId}` is deterministic and the upsert merges metadata with `metadata || EXCLUDED.metadata`, specifically so a repeat does not erase a Pix key that was already registered.

## Deprecated aliases

Two aliases reach the same two handlers. `deprecated: true` in the served document, same body, same scopes, same responses:

| Canonical | Deprecated alias |
|-----------|------------------|
| `POST /v1/account-applications` | `POST /v1/kyc/onboard` |
| `GET /v1/account-applications/{id}` | `GET /v1/kyc/onboard/{proposalId}/status` |

Both compile on the typed REST client. Prefer the canonical path. The alias group has its own page at [the kyc reference](/docs/api/reference/kyc).

The alias is the only place where `403 write_scope_refused` is documented, even though the handler raises it on all four paths. The promise that the alias is the same handler with a different path is broken by the document, not by the code.

## What this does not do

- **No list route.** Two canonical operations and two aliases are everything that exists. Lose the `verification_id` and no collection GET returns it. The recovery path is the meta-tool in step 9.
- **No cancel, no withdraw, no resubmit.** There is no route to retract an application and no route to reissue the documentoscopy link.
- **No published webhook event.** The served snapshot has an empty `webhooks` key, and the event catalogue on [Triggers](/docs/concepts/triggers) lists no `commerce.onboarding.*` at all. The events exist in the provider webhook router and feed internal reconciliation, but nothing published lets you wait on approval. See [the webhook listener cookbook](/docs/cookbooks/webhook-listener) for the shape of what you would subscribe to if there were one.
- **No typed SDK method.** There is no `session.onboard()` and no `session.accountApplication()`. What exists is the generic REST client above, and `session.execute('codespar_kyc', arguments)`.
- **`session.verificationStatus` is a different channel.** It and `session.verificationStatusStream` read the `tool_call_id` of a meta-tool execution and answer `approved`, `rejected`, `review`, `expired` or `pending`. They do not read a `verification_id` and they do not replace the read by id. See [SDK status](/docs/api/sdk/status).
- **No CLI command for the four routes.** Measured on CLI 0.11.2, `codespar --help` has no `account-applications` family, and `surface.js` excludes it with the reason that the group belongs with an admin family that has no served routes yet. Those routes are served. `codespar kyc` and `codespar tool codespar_kyc` are aliases of each other and the only CLI path, and each opens a session before executing, so each call costs a session. `--action` is refused client-side: the spelling is `--arg check_type=status`. See [direct commands](/docs/cli/reference/comandos-diretos).
- **`codespar consumers` is not this journey.** Its 19 subcommands do not include onboarding, and `codespar sellers` is the merchant side.
- **The exported `unknown` status is unreachable.** `CelcoinOnboardingStatus` has five values and these routes produce four. Code generated from the published type carries a dead branch.
- **Several real fields are absent from the spec.** `disclosure` on the GET, every field of the POST recovery branch, `branch` on either route, the `kyc:write` scope on both POSTs, and `403` on the canonical routes. Read the behaviour here, not the schema.
- **An approved body does not prove a consistent holder.** `anchorVerifiedConsumer` is best effort. If the `consumer_id` already carries a different verified document, it returns `document_mismatch`, logs a warning, and the funding source is provisioned anyway. The GET answers `200 approved` with an inconsistent holder table behind it.
- **`pix_key_registration_error` absent means two things.** Either the key registered, or there is no account yet. Only `pix_key` separates them, and its presence never makes an application unapproved.

## Next steps

<NextStepsGrid items={[
  { label: "REFERENCE", title: "Account applications", description: "Every parameter, body and documented response for the two canonical operations.", href: "/docs/api/reference/account-applications" },
  { label: "META-TOOL", title: "codespar_kyc", description: "The check_type enum, the status recovery path, and the masked account it returns.", href: "/docs/concepts/meta-tools/kyc" },
  { label: "REFERENCE", title: "Funding sources", description: "The line approval creates, its deterministic id, and the branch in metadata.", href: "/docs/api/reference/funding-sources" },
]} />
