Versions
How API versions work, which version you are on, and how to upgrade.
Every request runs on one API version. A version is named by its release date, such as
2026-09-17. A new version only ever removes routes that an earlier version deprecated; it
never changes what an existing route does. Additive changes, such as a new field or a new
endpoint, arrive on every version. Rarely, a breaking change ships on every version at once.
Changes on every version lists each one.
Which version you are on
Your organization is pinned to a version. New organizations start on the latest version.
Organizations that existed before versions started are pinned to 2026-01-01, the baseline.
If you sign in to these docs, the reference filters to your organization's pinned version and shows only what is available on it. A banner links to Connect to view or change it.
Every response carries the version it ran on:
Cowdi-Version: 2026-01-01Read your version, the latest version, the rollback that is still possible, and every released
version with
GET /v1/organizations/{organizationId}/version.
Try a version before you upgrade
Send the Cowdi-Version header to run one request on another version:
curl https://api.cowdi.co/v1/user/invitations/organizations \
-H "Authorization: Bearer $COWDI_ACCESS_TOKEN" \
-H "Cowdi-Version: 2026-09-17"A retired route answers 410 with the code ENDPOINT_RETIRED and names its replacement. An
unreleased version answers 400 with the code API_VERSION_UNKNOWN.
Upgrade
When every call works on the latest version, move your organization to it with
POST /v1/organizations/{organizationId}/version/upgrade.
The change applies at once to every request without a Cowdi-Version header. An upgrade always
moves to the latest version.
While you are on an older version, a retired route still works and answers with the deprecation notice described below.
Roll back
For 72 hours after an upgrade you can return to the version it replaced with
POST /v1/organizations/{organizationId}/version/rollback.
While a rollback is possible, GET .../version shows the version it returns to in
rollback_to and the deadline in rollback_expires_at. After the deadline, a rollback answers
409 with the code API_VERSION_ROLLBACK_UNAVAILABLE and the upgrade is final.
Removal of old versions
Cowdi gives six months' notice before it removes an old version. When the six months end, organizations that are still on that version are upgraded automatically. Upgrade before then so you can test your integration on the new version first.
Deprecation notices
A change that removes something goes through two stages, and nothing leaves a version you are on.
- Deprecated. The route or field keeps working on every version. The reference marks it with a Deprecated badge, and its description names the replacement.
- Retired. A later version stops serving the route. Your version keeps it until you upgrade. The table for that version below names the replacement.
A deprecated or retired route answers with two headers on every version that still serves it:
Deprecation: @1789603200
Link: <https://docs.cowdi.co/docs/versions>; rel="deprecation"The Deprecation value is the date of the notice as @ followed by Unix seconds, as
RFC 9745 defines it. For a retired route it is the release
date of the version that retires it.
A deprecated field stays in the response and keeps its meaning on every version. The reference marks it on the schema and names the field to read instead. A retired field leaves the response from its retiring version on; your version keeps it until you upgrade.
While your organization is behind the latest version, every response also carries a link to it:
Link: <https://docs.cowdi.co/docs/versions>; rel="successor-version"To find what your integration still depends on, search your logs for responses with a
Deprecation header, move each call to its replacement, test with the Cowdi-Version header,
then upgrade.
Deprecated, not yet retired
| Route | Replacement |
|---|---|
POST /v1/kyc/applications/{id}/session | POST /v1/kyc/applications/{id}/verification |
GET /v1/kyc/applications/{id}/capture | GET /v1/kyc/applications/{id}/verification |
POST /v1/kyc/applications/{id}/capture | POST /v1/kyc/applications/{id}/verification |
GET /v1/organizations/{organizationId}/merchants/{merchantId}/invitations | GET /v1/merchants/{id}/invitations |
POST /v1/organizations/{organizationId}/merchants/{merchantId}/invitations | POST /v1/merchants/{id}/invitations |
DELETE /v1/organizations/{organizationId}/merchants/{merchantId}/invitations/{invitationId} | DELETE /v1/merchants/{id}/invitations/{invitationId} |
DELETE /v1/organizations/{organizationId}/merchants/{merchantId}/members/{userId} | DELETE /v1/merchants/{id}/members/{userId} |
POST /v1/financing/merchants | POST /v1/merchants. Send name as business_name, contact_email as email, and contact_phone_number as phone_number. |
Changes on every version
Email verification confirming, 24 October 2026
An email verification can show status CONFIRMING while a confirmation is in progress. The
confirmation is being processed. Poll again shortly. The change is additive. Treat an unknown
status as pending.
Own-organization access only, 23 October 2026
A bearer token may act only on the organization it was issued for. Calls whose target
organization is not the token's organization now answer 403 with code FORBIDDEN. This
changed on every version. Check your integration even if your organization has not upgraded.
- Affected operations. Cross-org calls break for Create a user,
POST /v1/users, Add a member,POST /v1/organizations/{organizationId}/members, Get the API version, Upgrade, Roll back, and Cowdi-audience organization preferences, GET and PATCH/v1/organizations/{organizationId}/preferences. - Use a token for the target organization. Partners calling their own organization are unchanged. Callers that used a token from another organization, including Cowdi operating on a partner org, must obtain a token issued for the target organization.
- Cowdi ops deploy order. Deploy backoffice roles in create #971 and prospect activation in the prospect's org #972 before relying on this access rule for organization setup and Cowdi-tab prospect activation.
First email or phone goes to the primary flow, 22 October 2026
The first email or phone number a customer adds becomes their primary contact when they confirm it. This changed on every version. Check your integration even if your organization has not upgraded.
- No second flow. A first contact that is verified becomes primary. You no longer start a primary email change for it.
- With a current account, the bank sends the codes. For a customer with an open current
account and no email, Add an email
returns
otpwith thestepOWNER_AUTHORIZATION,sent_toPHONE_NUMBER, 2stepsand 4digits. Confirm that code, then the code sent to the email. Before, one 6-digit code went to the email. Readotpfor where each code goes. - Phone numbers work the same way. Add a phone number makes the first phone number primary when its code is confirmed.
- Until then the contact is secondary.
is_primaryandvalidatedstayfalseuntil the last code. An unfinished contact expires and is removed.
Flex Credit refinancing, 22 October 2026
A Flex Credit facility can be refinanced. The refinance routes are under the Refinancing tag.
- List refinance offers, Get a refinance offer, and Accept a refinance offer for a Flex Credit facility. They work like the term loan and revolving credit routes.
- A Flex Credit purchase can have
kindREFINANCING. It finances the payoff of the purchases a refinance settled.
Refinance extra cash, 22 October 2026
A refinance offer can lend extra cash. extra_cash and destination on the offer are null when it lends none.
After acceptance, such an offer shows status DISBURSING until the extra cash reaches destination, then COMPLETED. refinancing.completed fires at that point.
payout on the offer tracks the payment of the extra cash: its status (PENDING, COMPLETED, FAILED, or CANCELLED) and completed_at. It is null when the offer lends no extra cash, and until the borrower accepts the offer. A FAILED payout waits for a retry, and the offer stays DISBURSING until the payout completes.
disbursement.completed can carry kind REFINANCING.
Member invitations with several roles, 20 October 2026
An invitation can grant any organization role that you can grant, and more than one. This changed on every version. Check your integration even if your organization has not upgraded.
- Send
roles, notrole. Invite a member takesroles, a list of role slugs such as["member"]. A request that sendsroleanswers400. Invitation responses carryrolesin place ofrole. - You can only grant what you hold. Only a signed-in user can invite. Each role must be an
organization role of your organization, or the request answers
400and names the roles. You must hold every permission of each role, or the request answers403and names the permissions you are missing. - Inviting a pending email again replaces its roles. The request answers
200and sends no second email. - The
rolesfilter matches any role. List invitations withrolesreturns the invitations that grant at least one of them.
Product version terms in snake case, 15 October 2026
The terms of a product version use public field names and one of three shapes.
product selects the shape. TERM_LOAN and REVOLVING_CREDIT share one shape.
FLEX_CREDIT and INVOICE_FINANCING each have their own. This is a breaking change
on every version.
-
List products, Get a product, and List product versions return typed
terms. Read the new names.Before After gracePeriodDaysgrace_period, withvalueandunitcontinuousSpendingcan_spend_continuouslyallowReborrowingcan_reborrowdeferredStarthas_deferred_startallow_spending_when_in_arrearscan_spend_in_arrearscashback_sharecashback_share_basis_pointsinterest_rate.period_valueandinterest_rate.period_unitinterest_rate.period, withvalueandunitpayment_grace_periodstays a separate field. A field with no value is null. Term loan and revolving credit terms always includetenure,interest_rate,interest_calculation_method,fixed_interest,accrual_period,billing_period, andfixed_emi_amount. Flex Credit terms have nocan_spend_continuously.cashback_share_basis_pointsstays on Flex Credit only.allocation_ordernamesPENALTY,ARREARS,INTEREST, andPRINCIPALonce each.
Invoice financing product terms, 15 October 2026
Invoice financing terms on a product version use their own shape. This is a
breaking change on every version.
INVOICE_FINANCINGterms includebilling_strategy,allocation_order,grace_period, andpayment_grace_period. They mean the same as on Flex Credit. Discount rate and tenure limits stay.cashback_share_basis_pointsstays on Flex Credit only. An invoice facility uses a zero cashback share.
Late fee basis components, 15 October 2026
A late fee names the balance components it charges on. This is a breaking change on
every version. PRINCIPAL is the principal that is not overdue, ARREARS is the
overdue principal, and INTEREST is the unpaid interest. Existing accounts and
products charge the same late fees as before.
penalty_fee_configuration.basisis nowbasis_components. It is a list, for example["PRINCIPAL", "ARREARS"]for the oldPRINCIPAL_AND_ARREARS. It appears on product versions and invoice financing applications.penalty_basison a penalty charge transaction is nowpenalty_basis_components. It appears on List transactions and the other routes that return a transaction.- Loan application penalty terms add
calculation_basis_components.calculation_basisstays and is deprecated. It is null when no old value has the same components.
Wallet balance adjustments, 13 October 2026
disbursement.completed can carry kind ADJUSTMENT. That pays an approved manual credit into the customer's wallet.
An approval.outcome with approval_type LOAN_ADJUSTMENT covers a balance adjustment on a lending account or a wallet. entity_type can be wallet_manual_credit, wallet_manual_debit or wallet_journal_adjustment.
Products, 13 October 2026
You can read the products your organization offers, and their versions. This is separate from the loan products you quote for a customer.
- List products, Get a product, and List product versions. The role needs
org:products:read. A version isDRAFT,PUBLISHED, orSUPERSEDED.ussd_description_templateis null when USSD does not offer the product.
Loan repayment paybill via Pay Bill, 12 October 2026
A pay-bill payment to a Cowdi loan repayment destination (for example paybill
444174, account 0054) is refused on every version. Send the user to the loan
screen to repay instead.
POST /v1/transactions/mpesa/pay-billandPOST /v1/transactions/payeesanswer409withLOAN_REPAYMENT_PAYBILL_NOT_SUPPORTED. Before, the API created the payment and it then failed as an internal error at the payment provider. Thedescriptionis for developers; show the user a clear message and send them to the loan screen.
Loan product audiences and fixed-charge pricing, 12 October 2026
Organizations can offer different loan products to different customers. Product
responses also carry a pricing object so fixed-fee products work alongside
interest-rate ones. This changed on every version.
- List products for a customer.
GET /v1/accounts/loan/products/{organization}returns only the products that organization offers to the user. A user token is that user. An organization client names the user with optionaluser_id. Withoutuser_id, the response lists only products offered to every customer. Create and apply answer404when the organization does not offer the chosen product to that user. - New products. New monthly flat-rate and fixed-charge products are available
to the organizations that offer them. Get your organization's product codes from
the list response, and pass one as
loan_productwhen you create an application. The create body is optional where the product has a default; omitloan_productto quote the default product. - Read
pricingfor how a loan is charged. Product and application responses includepricingwith typeINTERESTorFIXED_CHARGE. Interest-rate fields such asinterest_rateandweekly_interest_rate_basis_pointsstay in the response for compatibility but are deprecated — they are null for a fixed-charge loan. Preferpricing.
OTP code length, 9 October 2026
One-time codes have the length of whoever sends them: 6 digits from Cowdi, 4 digits from the custodian bank. This changed on every version.
- The
otpobject gives the length. On Contact details,otp.digitsis4or6. Size your code field from it. Before, a 4-digit bank code for a contact was refused. - A code must have exactly that length. Any other length answers
400withOTP_INVALID. This applies to every confirmation, including transactions and current account closure, whose codes come from the bank and have 4 digits.
Email and phone number OTPs, 8 October 2026
The email and phone number routes under Contact details changed on every version. Check your integration even if your organization has not upgraded.
- One
otppath for each code.POST .../otp/sendsends a code, also again, andPOST .../otp/confirmconfirms it. The paths are in the table below. - The start sends the first code.
POST /v1/user/settings/emails/primaryandPOST /v1/user/settings/phones/primaryanswer202with the statusAWAITING_OTPand send the first code. Before, they answered200withOTP_REQUIREDand sent nothing. Do not callotp/sendright after the start: a second code less than 60 seconds after the first answers429. The start answers200when it sends no code, for example because the contact is already primary. - An
otpobject describes the next code. A contact and a primary change returnotpwith thestepto confirm (OWNER_AUTHORIZATIONorNEW_CONTACT_VERIFICATION), where the code went (sent_to:EMAILorPHONE_NUMBER), and the number ofsteps. It replacesverification_stage,next_action,otp_target,confirmation_stepandconfirmation_steps. A changestatusisOTP_REQUIRED,AWAITING_OTPorCOMPLETED. - Every confirmation sends its
step. Sendstepfrom theotpobject withotp_code. otp_urlstays set until the change completes. Readstatusto know whether a code is due.OTP_REQUIREDshows only when the first send failed, the code expired, or the bank step ended without a confirmation.- The routes that
2026-09-17retired for emails and phone numbers are gone. They answer404on every version. The/emails/primary/...paths that version retired serve the new API again.
| Old route | Route now |
|---|---|
POST /v1/user/settings/emails/{id}/resend and POST /v1/user/settings/emails/{id}/verification/otp | POST /v1/user/settings/emails/{id}/otp/send |
POST /v1/user/settings/emails/{id}/confirm and POST /v1/user/settings/emails/{id}/verification/otp/confirm | POST /v1/user/settings/emails/{id}/otp/confirm |
POST /v1/user/settings/emails/primary-change | POST /v1/user/settings/emails/primary |
POST /v1/user/settings/emails/primary-change/resend and POST /v1/user/settings/emails/primary/otp | POST /v1/user/settings/emails/primary/otp/send |
POST /v1/user/settings/emails/primary-change/confirm | POST /v1/user/settings/emails/primary/otp/confirm |
GET /v1/user/settings/emails/primary-change | GET /v1/user/settings/emails/primary/change |
POST /v1/user/settings/emails/primary-change/resume | POST /v1/user/settings/emails/primary/change/resume |
GET, POST and DELETE on /v1/user/settings/phone-numbers and /v1/user/settings/phone-numbers/{id} | The same method on /v1/user/settings/phones and /v1/user/settings/phones/{id} |
POST /v1/user/settings/phones/{id}/resend, POST /v1/user/settings/phone-numbers/{id}/resend and POST /v1/user/settings/phone-numbers/{id}/verification/otp | POST /v1/user/settings/phones/{id}/otp/send |
POST /v1/user/settings/phones/{id}/confirm, POST /v1/user/settings/phone-numbers/{id}/confirm and POST /v1/user/settings/phone-numbers/{id}/verification/otp/confirm | POST /v1/user/settings/phones/{id}/otp/confirm |
POST /v1/user/settings/phones/primary-change, POST /v1/user/settings/phone-numbers/primary, POST /v1/user/settings/phone-numbers/primary-change and POST /v1/user/accounts/current/phone/changes | POST /v1/user/settings/phones/primary |
POST /v1/user/settings/phones/primary-change/resend, POST /v1/user/settings/phone-numbers/primary/otp, POST /v1/user/settings/phone-numbers/primary-change/resend and POST /v1/user/accounts/current/phone/changes/{mobileNumberChangeId}/resend | POST /v1/user/settings/phones/primary/otp/send |
POST /v1/user/settings/phones/primary-change/confirm, POST /v1/user/settings/phone-numbers/primary/otp/confirm, POST /v1/user/settings/phone-numbers/primary-change/confirm, POST /v1/user/accounts/current/phone/changes/{mobileNumberChangeId}/confirm and .../authorize | POST /v1/user/settings/phones/primary/otp/confirm |
GET /v1/user/settings/phones/primary-change, GET /v1/user/settings/phone-numbers/primary/change, GET /v1/user/settings/phone-numbers/primary-change and GET /v1/user/accounts/current/phone/changes/{mobileNumberChangeId} | GET /v1/user/settings/phones/primary/change |
POST /v1/user/settings/phones/primary-change/resume, POST /v1/user/settings/phone-numbers/primary/change/resume and POST /v1/user/settings/phone-numbers/primary-change/resume | POST /v1/user/settings/phones/primary/change/resume |
Released versions
2026-09-17
Retires the routes renamed in the API naming review and the response fields deprecated before it. Each route replacement does the same work with the same request and response bodies; only the method or the path changed. Each retired field has a replacement that carries the same value.
Retired fields
| Schema | Retired field | Replacement |
|---|---|---|
| Installment | installment_number | position |
| Installment | installment_amount | amount |
| Installment breakdown | installment_principal_amount | principal_amount |
| Installment breakdown | installment_interest_amount | interest_amount |
| Lending balance | principal_outstanding | breakdown.principal_outstanding |
| Lending balance | interest_outstanding | breakdown.interest_outstanding |
| Lending balance | arrears_outstanding | breakdown.arrears_outstanding |
| Lending balance | penalty_outstanding | breakdown.penalty_outstanding |
| Lending balance | total_used | principal_used |
| Lending balance | available_balance | total_outstanding |
| Loan application (Greenwheels, Gilanis) | tenure_weeks | tenure |
| Credit account limits | credit_limit | credit_limit on the account |
Retired routes
| Retired route | Replacement |
|---|---|
PATCH /v1/users/{userId}/activate | POST /v1/users/{userId}/activate |
DELETE /v1/agents/{userId} | POST /v1/agents/{userId}/deactivate |
DELETE /v1/agents/{userId}/customers | POST /v1/agents/{userId}/customers/unassign |
GET /v1/user/member-invitations | GET /v1/user/invitations/organizations |
POST /v1/user/member-invitations/{invitationId}/accept | POST /v1/user/invitations/organizations/{invitationId}/accept |
POST /v1/user/member-invitations/{invitationId}/reject | POST /v1/user/invitations/organizations/{invitationId}/reject |
PUT /v1/user/payees/{payeeId}/block | POST /v1/user/payees/{payeeId}/block |
DELETE /v1/user/payees/{payeeId}/block | POST /v1/user/payees/{payeeId}/unblock |
PUT /v1/user/notifications/preferences | PATCH /v1/user/notifications/preferences |
POST /v1/user/accounts/current/close | POST /v1/user/accounts/current/closure |
GET /v1/user/accounts/current/close | GET /v1/user/accounts/current/closure |
POST /v1/user/accounts/current/close/otp/confirm | POST /v1/user/accounts/current/closure/confirm |
POST /v1/user/accounts/current/close/otp/resend | POST /v1/user/accounts/current/closure/resend |
POST /v1/transactions/{id}/otp | POST /v1/transactions/{id}/resend |
POST /v1/transactions/{id}/otp/confirm | POST /v1/transactions/{id}/confirm |
POST /v1/merchants/{merchantId}/invoice-financing/applications/estimate | POST /v1/merchants/{merchantId}/invoice-financing/applications/preview |
2026-01-01
The baseline: every route that existed before API versions.