Conventions
How every Cowdi API endpoint names, shapes, pages, and reports things.
Every endpoint on https://api.cowdi.co follows the same rules. Learn them once and any
resource in the API Reference reads the same way.
Resources and paths
A path names a resource. Collections are plural nouns; one resource is addressed by its id:
GET /v1/webhooks
GET /v1/webhooks/{id}
POST /v1/webhooksAn action that is not create, read, update, or delete is a POST on a verb below the resource,
for example POST /v1/webhooks/{id}/test or POST /v1/accounts/loan/applications/{organization}/{application_id}/apply.
The organization comes from your token. Resources under /v1/user/... belong to the signed-in
user and need a user token.
Methods and status codes
| Request | Method | Success |
|---|---|---|
| Read one or list | GET | 200 |
| Create | POST on the collection | 201 with the new resource |
| Action | POST on the verb | 200, or 202 when the work continues in the background |
| Partial update | PATCH | 200 with the updated resource |
| Delete | DELETE | 204 with no body |
Identifiers
Every resource has two identifiers. id is a UUID. resource_id is the same identity with a
type prefix, for example lapp_351t201rmv87aae5j4csam8000 for a loan application or
acc_01j5x8s9k2v7q3m4n6p8r1t2w4 for an account. Treat both as opaque strings. A reference to
another resource is a field named {resource}_id, such as account_id.
Fields
- Names are
snake_case. - Timestamps end in
_atand are ISO 8601 in UTC:"expires_at": "2026-10-27T10:30:00Z". Dates end in_dateand areYYYY-MM-DD. - Money is an integer in minor units of the
currencynext to it:"loan_amount": 500000with"currency": "KES"is KES 5,000.00. A standalone amount is{"value": 100000, "currency": "KES"}. - Rates are integers in basis points:
100is 1.00%. - Enumerations are
UPPER_SNAKE_CASE. New values can appear at any time. Handle a value you do not know as "unknown"; do not fail. - A field that has no value yet is
null. The shape of a resource does not change with its state.
Pagination
Every list returns a page:
{
"data": [{ "id": "..." }],
"links": {
"next": "/v1/transactions?size=20&after=eyJpZCI6...",
"prev": null
}
}Request the next page with the URL in links.next; it already carries the cursor. size sets
the page size (default 20). A larger size is capped at 100; the response still returns at most
100 items and a links.next cursor when more remain. Some list endpoints use a different
default (for example payees default to 50) but share the same maximum. after and before are
opaque cursors; send one at a time. Filters are query parameters named after the field, such as
status; a repeated filter matches any of its values. Time ranges use from (inclusive) and
to (exclusive).
Errors
Every error is a 4xx or 5xx status with the same body:
{
"code": "LOAN_APPLICATION_EXPIRED",
"description": "The application expired on 2026-03-01T10:00:00Z.",
"identifier": "650e8400-e29b-41d4-a716-446655440000",
"invalid_params": [{ "name": "loan_amount", "reason": "must be at least 50000" }]
}code is stable and is the value to branch on. description is for people. identifier names
the resource in conflict when there is one. invalid_params lists each invalid field on a 400.
| Status | Code | Meaning |
|---|---|---|
400 | INVALID_PARAMS, BAD_REQUEST | The request is malformed. See invalid_params. |
400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or malformed. See Idempotency. |
401 | UNAUTHORISED, TOKEN_EXPIRED | The token is missing, invalid, or expired. |
403 | FORBIDDEN | The token lacks the scope or the user may not do this. |
404 | RESOURCE_NOT_FOUND | No such resource, or not visible to this token. |
404 | REFINANCING_NOT_FOUND | The refinance offer is not this caller's, is for another account, or no reviewer approved it. |
409 | A specific code | The request conflicts with the resource's state. |
409 | REFINANCING_NOT_ALLOWED | The refinance offer expired, the loan or facility changed after the offer, or a manual block is on the borrower or the account. |
409 | IDEMPOTENCY_KEY_CONFLICT, IDEMPOTENCY_REQUEST_IN_PROGRESS | The idempotency key was used with a different request, or its first request still runs. |
422 | POCHI_NOT_SUPPORTED_FOR_ACCOUNT, POCHI_NOT_AVAILABLE, POCHI_AMOUNT_OUT_OF_RANGE | A Pochi la Biashara payment or preview was refused. |
429 | RATE_LIMIT_EXCEEDED | Slow down and retry after a pause. |
500, 503 | INTERNAL_ERROR, SERVICE_TEMPORARILY_UNAVAILABLE | Retry with backoff. |
Each operation in the reference lists the specific codes it returns.
Idempotency
Operations that must not run twice require an Idempotency-Key header. Most of them move
money or submit an application, such as POST /v1/transactions/transfers. The reference lists
the header as required on each operation that needs it. A request without the header fails with
400 IDEMPOTENCY_KEY_REQUIRED and does not run.
Create one key for each operation you intend to run, for example a UUID. Send the same key on every retry of that operation. A key has at most 255 characters: letters, digits, and dashes.
curl -X POST https://api.cowdi.co/v1/transactions/transfers \
-H "Authorization: Bearer $COWDI_ACCESS_TOKEN" \
-H "Idempotency-Key: 3f1c9a52-7d4e-4b8a-9c21-5e6f7a8b9c0d" \
-H "Content-Type: application/json" \
-d '{ ... }'- The first request runs, and Cowdi stores its response for 24 hours.
- A retry with the same key and the same request returns the stored response and does not run
again. The response carries
Idempotency-Replayed: true. - The same key with a different request returns
409IDEMPOTENCY_KEY_CONFLICT. - A retry that arrives while the first request still runs returns
409IDEMPOTENCY_REQUEST_IN_PROGRESS. Wait, then retry with the same key. - A
4xxresponse is stored like a success. To send a corrected request, use a new key. A5xxresponse is not stored, so a retry with the same key runs the request again.
Keys are scoped to your organization. An operation that does not list the header ignores it.
POST /v1/merchants and the deprecated POST /v1/financing/merchants do not use the header.
They take an optional idempotency_key UUID in the request body instead. Send the same
idempotency_key with the same details after a timeout to get the same merchant back. A changed
request with a used idempotency_key returns 409.
Versions
Additive changes ship to every version: new endpoints, new fields, new enumeration values. A
change that would break a client ships as a new route, and a later API version retires the old
one. Your organization is pinned to a version, every response says which version it ran on in
the Cowdi-Version header, and you upgrade when you are ready. See Versions.