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:
- Gather the issuance characteristics of each bond.
- Register the bond with Create Security and store the returned
securityId. - Validate the registration.
- 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
spreadrate (IPCA + 6% →0.06).
- If the return is a percentage of the index, the index's daily rate is multiplied by the
- With interest payments (
interestPaymentPeriodother thanBULLET): 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
issuanceFaceValueof 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 data | GorilaCORE field | Example | Notes |
|---|---|---|---|
| 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 date | initialDate | "2025-03-10" | The bond's issuance date, not the investor's purchase date. |
| Maturity date | maturityDate | "2028-03-10" | |
| Issuer's CNPJ | issuerId | "30306294000145" (Banco BTG Pactual) | 14 digits, unformatted. Check that the issuer exists in List Issuers. |
| Issuance face value | issuanceFaceValue | 1000 | Optional, but recommended. If omitted, GorilaCORE assumes 1,000. It must be on the same scale as the transactions' bond price. |
| Index | index | "CDI" | CDI, IPCA, IGPM or SELIC. Not sent for fixed-rate bonds. |
| Rate or percentage of the index | yield, multiplier or spread | "multiplier": 1.10 | Always in decimal. See the table below. |
| Interest payment frequency | interestPaymentPeriod | "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:
| Return | type | Fields |
|---|---|---|
| 12% p.a. (fixed rate) | FIXED_RATE_BANKING_BOND | "yield": 0.12 |
| 110% of CDI | FLOATING_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,
quantityis the number of bonds andpriceis the bond price, on theissuanceFaceValuescale. 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-keyheader. That way, if the request is repeated (for example, after a timeout), the transaction isn't duplicated. See Avoiding duplicate transactions. - Custodian. The
brokerIdis the institution where the bond is held in custody, which may be different from the issuing bank (the bond'sissuerId).
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:
- List Positions: the quantity of bonds per bond and custodian.
- List Position Security Prices: the bond price used on the date.
- List Position Market Values: the market value of each position.
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:
- 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.
- Register each bond only once, with a single set of characteristics, and store the code →
securityIdmapping in your database. - 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%or0.125; - 110% of CDI shows up as
110,1.1or "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:
yieldof fixed-rate bonds between0and0.3(0% to 30% p.a.). Ayieldof12.5would be a rate of 1,250% p.a.multiplierbetween0.5and2(50% to 200% of the index). Amultiplierof110would be 11,000% of CDI.spreadbetween-0.05and0.2.- The
IPCAorIGPMindex almost always comes with aspread.CDI, in turn, comes with amultiplieror, in "CDI +" bonds, with aspread.
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 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, andscalinglists only the later changes. Repeating the initial rate as the first item ofscalingis a common mistake. - In a
RETROACTIVEbond, 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
BULLETbond, 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
securityIdstored in your mapping. -
initialDateequal to the issuance date (Option 1) or the first acquisition date (Option 2), according to the chosen modeling. -
issuanceFaceValueprovided and on the same scale as the transactions' bond price. - Rate in decimal, within the expected range, in the right field (
yield,multiplierorspread). -
interestPaymentPeriodfilled 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, withoutprice. - Interest payments and redemptions at maturity left out of the load.
-
idempotency-keywith 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.