Cowdidocs

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/webhooks

An 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

RequestMethodSuccess
Read one or listGET200
CreatePOST on the collection201 with the new resource
ActionPOST on the verb200, or 202 when the work continues in the background
Partial updatePATCH200 with the updated resource
DeleteDELETE204 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 _at and are ISO 8601 in UTC: "expires_at": "2026-10-27T10:30:00Z". Dates end in _date and are YYYY-MM-DD.
  • Money is an integer in minor units of the currency next to it: "loan_amount": 500000 with "currency": "KES" is KES 5,000.00. A standalone amount is {"value": 100000, "currency": "KES"}.
  • Rates are integers in basis points: 100 is 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.

StatusCodeMeaning
400INVALID_PARAMS, BAD_REQUESTThe request is malformed. See invalid_params.
400IDEMPOTENCY_KEY_REQUIREDThe Idempotency-Key header is missing or malformed. See Idempotency.
401UNAUTHORISED, TOKEN_EXPIREDThe token is missing, invalid, or expired.
403FORBIDDENThe token lacks the scope or the user may not do this.
404RESOURCE_NOT_FOUNDNo such resource, or not visible to this token.
404REFINANCING_NOT_FOUNDThe refinance offer is not this caller's, is for another account, or no reviewer approved it.
409A specific codeThe request conflicts with the resource's state.
409REFINANCING_NOT_ALLOWEDThe refinance offer expired, the loan or facility changed after the offer, or a manual block is on the borrower or the account.
409IDEMPOTENCY_KEY_CONFLICT, IDEMPOTENCY_REQUEST_IN_PROGRESSThe idempotency key was used with a different request, or its first request still runs.
422POCHI_NOT_SUPPORTED_FOR_ACCOUNT, POCHI_NOT_AVAILABLE, POCHI_AMOUNT_OUT_OF_RANGEA Pochi la Biashara payment or preview was refused.
429RATE_LIMIT_EXCEEDEDSlow down and retry after a pause.
500, 503INTERNAL_ERROR, SERVICE_TEMPORARILY_UNAVAILABLERetry 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 409 IDEMPOTENCY_KEY_CONFLICT.
  • A retry that arrives while the first request still runs returns 409 IDEMPOTENCY_REQUEST_IN_PROGRESS. Wait, then retry with the same key.
  • A 4xx response is stored like a success. To send a corrected request, use a new key. A 5xx response 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.

On this page