Skip to main content

Integrating banking bonds

CDBs, LCIs, LCAs, LFs and other banking bonds are among the most common products in Brazilian portfolios and, at the same time, among the hardest to integrate. A stock has a ticker, a public quote and a single closing price per day. A banking bond, on the other hand, is a contract between the investor and the issuing bank: its characteristics (rate, index, issuance, maturity) define its price, and each issuance is, in practice, a different security.

Below we'll explore how to register these bonds in GorilaCORE and feed your clients' portfolios safely. We start with the happy path, a simple case from start to finish, and then dive into the challenges that industry data brings and how to work around them.

Why registration comes first​

For most securities, GorilaCORE already knows the security, its price and its events: you just send the transaction. Banking bonds are different, for three reasons:

  • There is no public, unique identifier. The bond's code at B3 (the "CETIP code") is not open data and doesn't exist in GorilaCORE, and the same bond may show up under different names at each custodian (in GorilaCORE, the position's broker).
  • There is no price source. Since there is no organized secondary market, the price doesn't come from a quote: it is calculated from the bond's curve, based on the issuance characteristics.
  • The characteristics define the security. Two CDBs from the same bank, with the same maturity but different rates, are different securities.

That's why the bond must be created in your organization before it can receive transactions. The integration flow always follows the same order:

  1. Gather the issuance characteristics of each bond.
  2. Register the bond with Create Security and store the returned securityId.
  3. Validate the registration.
  4. Only then send the transactions, referencing the securityId.

It's worth taking step 3 slowly, because a wrong registration can't be fixed with an edit:

  • A banking bond's characteristics can't be changed after creation. In Update Security by ID, it only accepts reclassification.
  • Banking bonds can't be deleted.
  • A transaction can't be moved to another security.

Fixing a registration means creating a new bond and recreating every transaction that pointed to the old one.

How GorilaCORE prices a banking bond​

Before registering, it helps to understand what the engine does with the data you send, because that explains almost every challenge in the following sections.

On each date, a banking bond is priced on its curve: the bond price (the PU, or preço unitário, in the Brazilian market) starts on the issuance date (initialDate) at the face value (issuanceFaceValue) and, from then on, accrues every business day at the contracted rate, on a 252-business-day year, until maturity (maturityDate).

  • Fixed rate (FIXED_RATE_BANKING_BOND): the bond price grows at the annual rate (yield).
  • Floating rate (FLOATING_RATE_BANKING_BOND): the bond price follows the index (index).
    • If the return is a percentage of the index, the index's daily rate is multiplied by the multiplier (110% of CDI → 1.10).
    • If it is the index plus a rate, the bond price accrues the index's variation and, on top of it, the spread rate (IPCA + 6% → 0.06).
  • With interest payments (interestPaymentPeriod other than BULLET): on each payment date, the accrued interest leaves the bond price and is paid to the investor.

A position's market value is, then, the quantity of bonds multiplied by the bond price on the curve on that day. Note what this implies:

  • Each transaction's bond price (price) is compared to the bond price on the curve on the same date. If they are equal, the transaction generates no result on that day. If they differ, the difference shows up as a profit or a loss.
  • The face value is the curve's scale. If the bond was registered with an issuanceFaceValue of 1,000, transactions must also come with a bond price around 1,000.

The happy path: a CDB bought at issuance​

Let's follow the simplest case: an investor invests BRL 50,000.00 in a CDB from Banco BTG Pactual paying 110% of CDI, issued on 2025-03-10 and maturing on 2028-03-10. The investor buys the bond on the issuance date and holds it until maturity.

1. Gather the issuance characteristics​

The custodian (or your database) usually has the bond's information under other names. This is the mapping to GorilaCORE:

Source dataGorilaCORE fieldExampleNotes
Bond type (CDB, LCI, LCA...)bankingBondType"CDB"Defines the security type (CORPORATE_BONDS_CDB, CORPORATE_BONDS_LCI, etc.).
Return type (fixed or floating)type"FLOATING_RATE_BANKING_BOND"FIXED_RATE_BANKING_BOND or FLOATING_RATE_BANKING_BOND.
Issuance dateinitialDate"2025-03-10"The bond's issuance date, not the investor's purchase date.
Maturity datematurityDate"2028-03-10"
Issuer's CNPJissuerId"30306294000145" (Banco BTG Pactual)14 digits, unformatted. Check that the issuer exists in List Issuers.
Issuance face valueissuanceFaceValue1000Optional, but recommended. If omitted, GorilaCORE assumes 1,000. It must be on the same scale as the transactions' bond price.
Indexindex"CDI"CDI, IPCA, IGPM or SELIC. Not sent for fixed-rate bonds.
Rate or percentage of the indexyield, multiplier or spread"multiplier": 1.10Always in decimal. See the table below.
Interest payment frequencyinterestPaymentPeriod"BULLET"BULLET (default) when everything is paid at maturity.

The rate is the most common source of errors, because every system writes it differently. In GorilaCORE, it is always decimal, and the field depends on the return type:

ReturntypeFields
12% p.a. (fixed rate)FIXED_RATE_BANKING_BOND"yield": 0.12
110% of CDIFLOATING_RATE_BANKING_BOND"index": "CDI", "multiplier": 1.10
CDI + 1.5% p.a.FLOATING_RATE_BANKING_BOND"index": "CDI", "spread": 0.015
IPCA + 6% p.a.FLOATING_RATE_BANKING_BOND"index": "IPCA", "spread": 0.06

For floating-rate bonds, send multiplier or spread, never both: the presence of one or the other is what tells the engine whether the return is a percentage of the index or the index plus a rate.

2. Register the bond​

With the data in hand, we create the bond with Create Security:

// REQUEST
// POST https://core.gorila.com.br/securities

{
"type": "FLOATING_RATE_BANKING_BOND",
"bankingBondType": "CDB",
"initialDate": "2025-03-10",
"maturityDate": "2028-03-10",
"issuerId": "30306294000145",
"issuanceFaceValue": 1000,
"interestPaymentPeriod": "BULLET",
"index": "CDI",
"multiplier": 1.10
}

The response brings the new security's ID:

// RESPONSE

{
"id": 44789069
}

Store this securityId in your database, associated with the bond's code at B3 (or with the key that identifies the bond in your system). This mapping is the centerpiece of the integration: every future transaction with this bond, in any portfolio of your organization, will use it. The bond is available only to your organization.

3. Validate the registration​

Before sending any transaction, confirm that the bond was understood as you expected.

First, read the bond with Read Security by ID. Check the generated name, the type (type), the issuance (issueDate) and maturity (maturityDate) dates, the issuer (issuer), the index (index) and the rate (yield).

Then, check the bond price on a known date. Send a financial amount (notional) and a date to Price Conversion. The response brings the bond price on the curve on that date and the corresponding quantity of bonds. Compare this bond price with the one the custodian reports for the same date: if they match, the curve is right.

// REQUEST
// POST https://core.gorila.com.br/securities/price-conversions

{
"security": {
"id": 44789069
},
"date": "2025-03-10",
"notional": 50000
}
// RESPONSE

{
"security": {
"id": 44789069,
"type": "CORPORATE_BONDS_CDB"
},
"date": "2025-03-10",
"price": 1000,
"quantity": 50
}

On the issuance date, the bond price is the face value itself. On any later date, it already includes the accrued return.

4. Send the transactions​

Now we record the investor's purchase with Create Transaction:

// REQUEST
// POST https://core.gorila.com.br/portfolios/{portfolioId}/transactions
// idempotency-key: 3b8f6c1e-9a2d-4f7b-8e15-6d0c4a9b2e71

{
"type": "REGULAR",
"transactDate": "2025-03-10",
"side": "BUY",
"quantity": 50,
"price": 1000,
"brokerId": "18945670000146",
"security": {
"id": 44789069
}
}

Three things to watch at this step:

  • Quantity and bond price, not financial amount. For banking bonds, quantity is the number of bonds and price is the bond price, on the issuanceFaceValue scale. If the source only has the amount invested, use Price Conversion to get the bond price and the quantity.
  • Idempotency key. Send the operation's identifier in your system in the idempotency-key header. That way, if the request is repeated (for example, after a timeout), the transaction isn't duplicated. See Avoiding duplicate transactions.
  • Custodian. The brokerId is the institution where the bond is held in custody, which may be different from the issuing bank (the bond's issuerId).

Redemptions before maturity are sales ("side": "SELL"), with the redeemed quantity and the day's bond price. In a CDB with daily liquidity, the redemption price is the bond price on the curve itself, and the sale generates no extra result.

5. What you don't need to send​

Periodic interest payments and the redemption at maturity are not transactions: GorilaCORE generates these events from the bond's characteristics. At maturity, GorilaCORE records the payment of the interest and the principal, and the position is closed (CLOSED).

If your data source records the interest payment or the redemption at maturity as an operation, don't send it. It would be counted twice, and the position would go negative.

6. Check the result​

With the transactions processed (status equal to COMPLETED), compare the portfolio with the custodian's position on a reference date:

If the quantity and the bond price match the custodian's, this bond's integration is done.

The challenges of industry data​

In the happy path, all the information was available and correct. In practice, banking bond data arrives incomplete, in different formats from each source and, sometimes, contradictory. Below are the most frequent problems, why they happen and how to work around them.

One bond, one securityId​

The problem. Without a public identifier, each source describes the same bond its own way, and it's easy to end up with the same bond under several securityIds. The purchase lands on one, the redemption on another, and the positions don't add up: one stays long and the other goes negative.

Registration doesn't duplicate bonds: creating a bond again with exactly the same characteristics returns the same securityId. But any difference creates a new security, such as a face value sent in one call and omitted in another, an issuance date shifted by one day or a rate rounded differently.

How to work around it. Treat registration as a step separate from loading transactions:

  1. Before sending the transactions, group the operations by the bond's code at B3. If it isn't available, use the combination of issuer, type, issuance, maturity and rate.
  2. Register each bond only once, with a single set of characteristics, and store the code → securityId mapping in your database.
  3. When loading the transactions, just look up that mapping.

Don't use the List Securities search as your mapping: it doesn't know the bond's code and may take a while to reflect newly created bonds.

Missing issuance face value or a different scale​

The problem. The face value (issuanceFaceValue) is the starting point of the curve, but it's precisely the most neglected piece of data in the sources. It may come:

  • empty (and GorilaCORE assumes 1,000);
  • equal to zero;
  • with a default value from the source system, which doesn't reflect the bond;
  • on very different scales from one issuer to another: 1,000, 100, 1 or even 0.01.

When the face value is on one scale and the transactions' bond price on another, the position starts with a huge result (positive or negative) on its first day.

How to work around it. The face value must be on the same scale as the transactions' bond price. Some ways to find it out:

  • If the investor bought at issuance, the bond price paid on the issuance date is the face value itself.
  • If the first purchase happened after issuance, the bond price paid indicates the scale: a bond price of 1,087.42 points to a face value of 1,000, and a bond price of 1.0874, to a face value of 1.
  • Check the operations' consistency: the quantity multiplied by the bond price must match each operation's financial amount. If it doesn't, the quantity or the bond price is on another scale.

Never send a face value of zero, and don't omit it without checking the scale: the default of 1,000 only works if the transactions' bond price is also around 1,000. If the scale can't be determined, don't register the bond with an estimated value. Set the case aside and look for the data at the source.

After registering, use Price Conversion to compare the bond price on the curve with the bond price reported by the custodian on a few dates. A constant difference of an order of magnitude (10×, 100×, 1,000×) is almost always the face value's scale.

Rate and index in inconsistent formats​

The problem. The same return shows up in different forms in each source:

  • 12.5% p.a. shows up as 12.5, 12,50% or 0.125;
  • 110% of CDI shows up as 110, 1.1 or "CDI + 10%";
  • an IPCA + 6% bond is registered as a percentage of the index.

How to work around it. Normalize the rate before registering and apply simple range checks, which catch most errors:

  • yield of fixed-rate bonds between 0 and 0.3 (0% to 30% p.a.). A yield of 12.5 would be a rate of 1,250% p.a.
  • multiplier between 0.5 and 2 (50% to 200% of the index). A multiplier of 110 would be 11,000% of CDI.
  • spread between -0.05 and 0.2.
  • The IPCA or IGPM index almost always comes with a spread. CDI, in turn, comes with a multiplier or, in "CDI +" bonds, with a spread.

These ranges are a suggestion for your pipeline, not API limits. Since the registration can't be edited, it's much cheaper to block an odd value before the call than to fix it afterwards.

Secondary market purchases​

The problem. When an investor buys an already issued bond from another investor, the traded price is rarely the bond price on the original curve. It's calculated with the market rate on the trade date, so that the value at maturity stays the same.

Here's an example, counting the term in whole years to keep it simple (the engine counts business days). A fixed-rate CDB paying 12% p.a., with a two-year term, issued at BRL 1,000.00, is worth at maturity:

1,000.00 × (1 + 0.12)² = 1,254.40

One year after issuance, the market rate for similar bonds has dropped to 10% p.a. Whoever buys the bond at that moment pays the amount that, earning 10% p.a. over the remaining year, reaches the same BRL 1,254.40:

P = 1,254.40 / (1 + 0.10) = 1,140.36

On the original curve, the bond would be worth BRL 1,120.00 on that day (1,000.00 × 1.12). The buyer paid a premium of BRL 20.36 per bond.

How to work around it. There are two ways to register the bond, and the choice depends on the data you have.

Option 1 (recommended): register with the issuance data. Use the issuance date in initialDate and the original rate in yield (12% in the example), and record the purchase with the bond price actually paid (1,140.36). The engine marks the bond on the original curve until maturity and recognizes the difference between the bond price paid and the bond price on the curve (the BRL 20.36 premium per bond) as the trade's result, on the purchase date. The advantages:

  • A bond code corresponds to a single securityId, across all portfolios.
  • The result of each secondary trade is visible, and the value at maturity converges to the right one.

Option 2: register from the first acquisition. When the issuance data isn't available, create the bond from the purchase. The initialDate becomes the first purchase date, the issuanceFaceValue is the bond price paid (1,140.36), and the rate is the trade's rate, the yield to maturity (YTM; 10% in the example). The engine treats the purchase as a "pseudo-issuance": the value at maturity is still right, but the premium paid over the original issuance no longer shows up. In this option:

  • The same bond code may become different securities in different portfolios, because each investor bought on a different date and at a different rate.
  • Within the same portfolio, all operations with that code must point to the same securityId, the one from the first acquisition. Only then do sales reduce the right position. Later purchases, at other rates, generate a result relative to that curve.

There's also a third way out, for when you have the issuance rate but the source doesn't report the bond price paid. Register using Option 1 and record the purchase with the bond price on the curve, obtained from Price Conversion. The market value is right, but the premium or discount actually paid doesn't show up.

Custody transfers​

The problem. When an investor brings a bond from another institution, the new custodian records an inflow by custody transfer, not a purchase. This inflow usually comes with one of these bond prices:

  • zero;
  • the bond price of the original acquisition, from years ago;
  • the one calculated by the source custodian.

If it's sent as a regular purchase, with whatever bond price is in the data, the position starts with a result that never existed.

How to work around it. The right transaction depends on where the bond was before.

From one custodian to another, within the same portfolio (both ends are in GorilaCORE): use a custody transfer (CUSTODY_TRANSFER), with sourceBrokerId and targetBrokerId. The position changes custodian and keeps its history and acquisition cost.

From outside the portfolio, without history: use an initial custody adjustment (INITIAL_CUSTODY_ADJUSTMENT) with "side": "BUY" and the transferred quantity, without providing the price. In this case, GorilaCORE uses the bond price on the curve on that date, and the position enters at its fair value, with no artificial result. On the other hand, the acquisition cost becomes the bond's value on the transfer date.

// REQUEST
// POST https://core.gorila.com.br/portfolios/{portfolioId}/transactions

{
"type": "INITIAL_CUSTODY_ADJUSTMENT",
"transactDate": "2025-08-04",
"side": "BUY",
"quantity": 35,
"brokerId": "02332886001178",
"security": {
"id": 44789069
}
}
The adjustment replaces the custody

The initial adjustment's quantity replaces the custody the position had at that custodian. If the portfolio already holds the same bond at the same custodian, record the inflow as a purchase (REGULAR, "side": "BUY") with the bond price on the curve on that date, obtained from Price Conversion. Since that price is on the curve, this purchase also generates no result.

From outside the portfolio, with history: if the source reports the date and bond price of the original acquisition, you can rebuild the complete path. Record the original purchase at the source custodian and then the CUSTODY_TRANSFER to the new custodian. It's more work, but it preserves the cost and the result since the acquisition.

In any case, the transferred quantity must be on the same scale as the bond's face value, just like purchases.

Quantity residues​

The problem. Quantities and bond prices with few decimal places (or recalculated from the financial amount) leave leftovers. After the full redemption, the position is left with 0.0000003 bonds, which don't go away.

How to work around it. To zero the position on a full redemption, use a position close (POSITION_CLOSE) instead of a sale with a quantity. The engine calculates the quantity in custody on the date and generates the exact sale. If an earlier transaction is added later, the close quantity is recalculated.

Events recorded as transactions​

The problem. Many sources record the interest payment or the redemption at maturity as sell operations. When sent to GorilaCORE, they add up to the events the engine already generates from the bond: the investor receives the money twice, and the position goes negative.

How to work around it. Filter these entries out before loading. For GorilaCORE, only the investor's decisions are transactions: investments, early redemptions, secondary sales and transfers. Interest and maturity are a consequence of the bond's characteristics. For them to happen correctly, what matters is that the registration has the right interest payment frequency (interestPaymentPeriod) and maturity.

Step-up CDBs (progressive rate)​

The problem. Some floating-rate CDBs change the percentage of CDI on set dates (for example, they start at 100% and go up 5 points every year). If the bond is registered with the initial rate only, the curve falls below the real one from the first change onward.

How to work around it. Register the bond with scaleType and scaling, as described in Step-up CDB. Two points often raise questions:

  • The issuance rate goes in the multiplier, and scaling lists only the later changes. Repeating the initial rate as the first item of scaling is a common mistake.
  • In a RETROACTIVE bond, the new rate applies since issuance, and the bond price jumps on the change date. This jump is expected and doesn't indicate a data error.

Currently, progressive rates are only accepted for bonds indexed to CDI, without spread and with interestPaymentPeriod equal to BULLET.

Price series that don't add up​

The problem. Even with the right registration, the custodian's position sometimes doesn't match the curve, because the problem is at the source. Some examples:

  • a bond price that jumps 12% from one day to the next in a fixed-rate bond paying 16% p.a.;
  • a bond price that drops between two dates in a bond that pays everything at maturity;
  • a series that grows 3% in three years in a fixed-rate bond paying 15% p.a.

How to work around it. Before investigating the registration, test the consistency of the bond price series that the source reports for each bond:

  • Monotonicity. In a BULLET bond, the bond price never drops from one day to the next. Drops are only expected on interest payment dates.
  • Implied rate. The annualized return between two bond prices must be compatible with the bond's rate (for floating-rate bonds, with the index over the period). A jump that, annualized, exceeds thousands of percent is a data error.
  • Anchor at issuance. On the issuance date, the bond price must equal the face value.

When the source series passes these tests and still doesn't match GorilaCORE's bond price, the difference is in the registration (rate, dates or face value). When the source series fails, the fix has to happen at the source. In that case, set aside the examples with the date, bond price, quantity and bond code, and take them to the custodian.

Checklist before loading​

For each bond:

  • Grouped by the bond code, with a single securityId stored in your mapping.
  • initialDate equal to the issuance date (Option 1) or the first acquisition date (Option 2), according to the chosen modeling.
  • issuanceFaceValue provided and on the same scale as the transactions' bond price.
  • Rate in decimal, within the expected range, in the right field (yield, multiplier or spread).
  • interestPaymentPeriod filled in for bonds with periodic interest.
  • Bond price on the curve checked with Price Conversion against the custodian's bond price on at least one date.

For each transaction:

  • Quantity multiplied by the bond price matches the operation's financial amount.
  • Inbound transfers without history sent as INITIAL_CUSTODY_ADJUSTMENT, without price.
  • Interest payments and redemptions at maturity left out of the load.
  • idempotency-key with the operation's identifier in your system.

Wrap-up​

Integrating banking bonds is, above all, a registration job. With the right issuance characteristics, GorilaCORE calculates the curve, the interest, the maturity and the result of each trade, and transactions become only the investor's decisions. Most of the effort lies in handling the source data before loading: grouping by bond, getting the face value's scale right, normalizing rates and choosing the right transaction for each inflow.

Once integrated, banking bonds enter every metric of the portfolio, such as market value, P&L, TWR and liquidity, which takes each bond's maturity into account.

Contact us if you have any doubts about how to get started.