Cowdidocs

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-01

Read 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.

  1. Deprecated. The route or field keeps working on every version. The reference marks it with a Deprecated badge, and its description names the replacement.
  2. 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

RouteReplacement
POST /v1/kyc/applications/{id}/sessionPOST /v1/kyc/applications/{id}/verification
GET /v1/kyc/applications/{id}/captureGET /v1/kyc/applications/{id}/verification
POST /v1/kyc/applications/{id}/capturePOST /v1/kyc/applications/{id}/verification
GET /v1/organizations/{organizationId}/merchants/{merchantId}/invitationsGET /v1/merchants/{id}/invitations
POST /v1/organizations/{organizationId}/merchants/{merchantId}/invitationsPOST /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/merchantsPOST /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 otp with the step OWNER_AUTHORIZATION, sent_to PHONE_NUMBER, 2 steps and 4 digits. Confirm that code, then the code sent to the email. Before, one 6-digit code went to the email. Read otp for 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_primary and validated stay false until 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.

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, not role. Invite a member takes roles, a list of role slugs such as ["member"]. A request that sends role answers 400. Invitation responses carry roles in place of role.
  • 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 400 and names the roles. You must hold every permission of each role, or the request answers 403 and names the permissions you are missing.
  • Inviting a pending email again replaces its roles. The request answers 200 and sends no second email.
  • The roles filter matches any role. List invitations with roles returns 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.

    BeforeAfter
    gracePeriodDaysgrace_period, with value and unit
    continuousSpendingcan_spend_continuously
    allowReborrowingcan_reborrow
    deferredStarthas_deferred_start
    allow_spending_when_in_arrearscan_spend_in_arrears
    cashback_sharecashback_share_basis_points
    interest_rate.period_value and interest_rate.period_unitinterest_rate.period, with value and unit

    payment_grace_period stays a separate field. A field with no value is null. Term loan and revolving credit terms always include tenure, interest_rate, interest_calculation_method, fixed_interest, accrual_period, billing_period, and fixed_emi_amount. Flex Credit terms have no can_spend_continuously. cashback_share_basis_points stays on Flex Credit only. allocation_order names PENALTY, ARREARS, INTEREST, and PRINCIPAL once 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_FINANCING terms include billing_strategy, allocation_order, grace_period, and payment_grace_period. They mean the same as on Flex Credit. Discount rate and tenure limits stay.
  • cashback_share_basis_points stays 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.basis is now basis_components. It is a list, for example ["PRINCIPAL", "ARREARS"] for the old PRINCIPAL_AND_ARREARS. It appears on product versions and invoice financing applications.
  • penalty_basis on a penalty charge transaction is now penalty_basis_components. It appears on List transactions and the other routes that return a transaction.
  • Loan application penalty terms add calculation_basis_components. calculation_basis stays 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.

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-bill and POST /v1/transactions/payees answer 409 with LOAN_REPAYMENT_PAYBILL_NOT_SUPPORTED. Before, the API created the payment and it then failed as an internal error at the payment provider. The description is 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 optional user_id. Without user_id, the response lists only products offered to every customer. Create and apply answer 404 when 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_product when you create an application. The create body is optional where the product has a default; omit loan_product to quote the default product.
  • Read pricing for how a loan is charged. Product and application responses include pricing with type INTEREST or FIXED_CHARGE. Interest-rate fields such as interest_rate and weekly_interest_rate_basis_points stay in the response for compatibility but are deprecated — they are null for a fixed-charge loan. Prefer pricing.

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 otp object gives the length. On Contact details, otp.digits is 4 or 6. 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 400 with OTP_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 otp path for each code. POST .../otp/send sends a code, also again, and POST .../otp/confirm confirms it. The paths are in the table below.
  • The start sends the first code. POST /v1/user/settings/emails/primary and POST /v1/user/settings/phones/primary answer 202 with the status AWAITING_OTP and send the first code. Before, they answered 200 with OTP_REQUIRED and sent nothing. Do not call otp/send right after the start: a second code less than 60 seconds after the first answers 429. The start answers 200 when it sends no code, for example because the contact is already primary.
  • An otp object describes the next code. A contact and a primary change return otp with the step to confirm (OWNER_AUTHORIZATION or NEW_CONTACT_VERIFICATION), where the code went (sent_to: EMAIL or PHONE_NUMBER), and the number of steps. It replaces verification_stage, next_action, otp_target, confirmation_step and confirmation_steps. A change status is OTP_REQUIRED, AWAITING_OTP or COMPLETED.
  • Every confirmation sends its step. Send step from the otp object with otp_code.
  • otp_url stays set until the change completes. Read status to know whether a code is due. OTP_REQUIRED shows only when the first send failed, the code expired, or the bank step ended without a confirmation.
  • The routes that 2026-09-17 retired for emails and phone numbers are gone. They answer 404 on every version. The /emails/primary/... paths that version retired serve the new API again.
Old routeRoute now
POST /v1/user/settings/emails/{id}/resend and POST /v1/user/settings/emails/{id}/verification/otpPOST /v1/user/settings/emails/{id}/otp/send
POST /v1/user/settings/emails/{id}/confirm and POST /v1/user/settings/emails/{id}/verification/otp/confirmPOST /v1/user/settings/emails/{id}/otp/confirm
POST /v1/user/settings/emails/primary-changePOST /v1/user/settings/emails/primary
POST /v1/user/settings/emails/primary-change/resend and POST /v1/user/settings/emails/primary/otpPOST /v1/user/settings/emails/primary/otp/send
POST /v1/user/settings/emails/primary-change/confirmPOST /v1/user/settings/emails/primary/otp/confirm
GET /v1/user/settings/emails/primary-changeGET /v1/user/settings/emails/primary/change
POST /v1/user/settings/emails/primary-change/resumePOST /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/otpPOST /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/confirmPOST /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/changesPOST /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}/resendPOST /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 .../authorizePOST /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/resumePOST /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

SchemaRetired fieldReplacement
Installmentinstallment_numberposition
Installmentinstallment_amountamount
Installment breakdowninstallment_principal_amountprincipal_amount
Installment breakdowninstallment_interest_amountinterest_amount
Lending balanceprincipal_outstandingbreakdown.principal_outstanding
Lending balanceinterest_outstandingbreakdown.interest_outstanding
Lending balancearrears_outstandingbreakdown.arrears_outstanding
Lending balancepenalty_outstandingbreakdown.penalty_outstanding
Lending balancetotal_usedprincipal_used
Lending balanceavailable_balancetotal_outstanding
Loan application (Greenwheels, Gilanis)tenure_weekstenure
Credit account limitscredit_limitcredit_limit on the account

Retired routes

Retired routeReplacement
PATCH /v1/users/{userId}/activatePOST /v1/users/{userId}/activate
DELETE /v1/agents/{userId}POST /v1/agents/{userId}/deactivate
DELETE /v1/agents/{userId}/customersPOST /v1/agents/{userId}/customers/unassign
GET /v1/user/member-invitationsGET /v1/user/invitations/organizations
POST /v1/user/member-invitations/{invitationId}/acceptPOST /v1/user/invitations/organizations/{invitationId}/accept
POST /v1/user/member-invitations/{invitationId}/rejectPOST /v1/user/invitations/organizations/{invitationId}/reject
PUT /v1/user/payees/{payeeId}/blockPOST /v1/user/payees/{payeeId}/block
DELETE /v1/user/payees/{payeeId}/blockPOST /v1/user/payees/{payeeId}/unblock
PUT /v1/user/notifications/preferencesPATCH /v1/user/notifications/preferences
POST /v1/user/accounts/current/closePOST /v1/user/accounts/current/closure
GET /v1/user/accounts/current/closeGET /v1/user/accounts/current/closure
POST /v1/user/accounts/current/close/otp/confirmPOST /v1/user/accounts/current/closure/confirm
POST /v1/user/accounts/current/close/otp/resendPOST /v1/user/accounts/current/closure/resend
POST /v1/transactions/{id}/otpPOST /v1/transactions/{id}/resend
POST /v1/transactions/{id}/otp/confirmPOST /v1/transactions/{id}/confirm
POST /v1/merchants/{merchantId}/invoice-financing/applications/estimatePOST /v1/merchants/{merchantId}/invoice-financing/applications/preview

2026-01-01

The baseline: every route that existed before API versions.

On this page