# POST /v1/bank-accounts — Link a Bank Account Source: https://docs.cleo-pay.com/api-reference/bank-accounts/create POST /v1/bank-accounts links a new bank account to your business. Provide account holder details, full account number, and routing number. Link a new bank account to your business by submitting the account holder's legal name, a display name, full account number, routing number, and account type. Once created, Cleo Pay initiates the micro-deposit verification process automatically — two small deposits will arrive in the account within 1–3 business days. ## HTTP Request ``` POST https://api.cleo-pay.com/v1/bank-accounts ``` ## Authentication Include your bearer token on every request: ``` Authorization: Bearer ``` *** ## Request Body The legal name of the account holder exactly as it appears on the bank account. This is used for payment processing and account validation. A human-readable display name for this account (e.g. `"Primary Operating Account"`). This is for your own reference and is returned in all subsequent responses. The full bank account number. This value is **write-only** — it is never returned in any API response after creation. The 9-digit ABA routing number for the account. This value is **write-only** — it is never returned in any API response after creation. The type of bank account. Must be one of `checking` or `savings`. *** ## Response Fields A successful request returns HTTP `201 Created` with the new bank account object. Unique identifier for the bank account. The initial status of a newly created account is `pending_verification`, indicating that micro-deposits have been initiated. The bank account type you submitted: `checking` or `savings`. Always `manual` for accounts created via the API. The display name you provided. The name of the financial institution, if resolved from the routing number. The last four digits of the account number you submitted. The last four digits of the routing number you submitted. Whether this account has been set as the default for your business. The timestamp at which micro-deposits were initiated. Populated immediately on successful account creation. The timestamp at which micro-deposits landed in the account. This will be `null` until the deposits arrive (typically 1–3 business days). The number of failed verification attempts. Always `0` on a newly created account. *** ## Example Request ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "holderName": "Acme Corp", "name": "Primary Operating Account", "accountNumber": "123456789012", "routingNumber": "021000021", "type": "checking" }' ``` *** ## Example Response ```json theme={null} { "id": "ba_01j9kxm2p4fghe3t7q8wnvd5rc", "status": "pending_verification", "type": "checking", "connectionType": "manual", "name": "Primary Operating Account", "institutionName": "Chase", "accountNumberLast4": "9012", "routingNumberLast4": "0021", "isDefault": false, "microDepositsInitiatedAt": "2024-10-01T10:00:00Z", "microDepositsCompletedAt": null, "verificationAttempts": 0 } ``` `accountNumber` and `routingNumber` are **write-only** fields. After creation, only the last four digits of each are accessible via `accountNumberLast4` and `routingNumberLast4`. Store sensitive account details securely before submitting them to the API if you need to retain them. Micro-deposits are initiated automatically upon account creation. Check `microDepositsCompletedAt` on the account — once it is non-null, the deposits have landed and you can call the [verify micro-deposits](/api-reference/bank-accounts/verify-micro-deposits) endpoint to complete verification. # DELETE /v1/bank-accounts/{bankAccountId} — Remove a Bank Account Source: https://docs.cleo-pay.com/api-reference/bank-accounts/delete DELETE /v1/bank-accounts/{bankAccountId} removes a bank account from your business. The account status becomes 'removed' after deletion. Remove a bank account from your business. Once deleted, the account's status transitions to `removed` and it can no longer be used to initiate payments. This action is irreversible — you will need to link a new account if you want to restore access. ## HTTP Request ``` DELETE https://api.cleo-pay.com/v1/bank-accounts/{bankAccountId} ``` ## Authentication Include your bearer token on every request: ``` Authorization: Bearer ``` *** ## Path Parameters The unique identifier of the bank account to remove. This is the `id` returned when the account was created, or from the list accounts response. *** ## Response This endpoint returns **204 No Content** on success. No response body is returned. *** ## Example Request ```bash theme={null} curl --request DELETE \ --url https://api.cleo-pay.com/v1/bank-accounts/ba_01j9kxm2p4fghe3t7q8wnvd5rc \ --header 'Authorization: Bearer ' ``` *** ## Example Response ``` HTTP/1.1 204 No Content ``` Deleting a bank account is **permanent**. The account status is set to `removed` and the account cannot be restored. If the deleted account was your only verified account, you will need to link and verify a new one before you can process payments. After deletion, the bank account record remains accessible via [GET /v1/bank-accounts/](/api-reference/bank-accounts/get) and [GET /v1/bank-accounts](/api-reference/bank-accounts/list) with a `status` of `removed`. This allows you to maintain an audit trail of all accounts that have ever been linked to your business. # GET /v1/bank-accounts/{bankAccountId} — Get a Bank Account Source: https://docs.cleo-pay.com/api-reference/bank-accounts/get GET /v1/bank-accounts/{bankAccountId} retrieves a single bank account by ID, including verification status and micro-deposit timestamps. Retrieve a single bank account by its ID. Use this endpoint to check the current verification status of an account, inspect micro-deposit timestamps, or fetch account metadata before initiating a payment. ## HTTP Request ``` GET https://api.cleo-pay.com/v1/bank-accounts/{bankAccountId} ``` ## Authentication Include your bearer token on every request: ``` Authorization: Bearer ``` *** ## Path Parameters The unique identifier of the bank account to retrieve. This is the `id` returned when the account was created, or from the list accounts response. *** ## Response Fields Unique identifier for the bank account. Current verification status of the account. One of: * `pending_verification` — micro-deposits have been initiated and are awaiting confirmation. * `verified` — account is verified and ready to use for payments. * `unverified` — verification failed because the maximum number of attempts was exceeded. * `removed` — account has been deleted. The bank account type: `checking` or `savings`. How the account was connected. Either `manual` (linked via the API) or `plaid`. The API only creates `manual` accounts. The display name assigned to this account. The name of the financial institution. The last four digits of the account number. The last four digits of the routing number. Whether this account is the default account for your business. The timestamp at which micro-deposits were initiated, or `null` if not yet started. The timestamp at which micro-deposits landed in the account. Verification can be attempted once this field is non-null. The number of failed verification attempts made so far. The maximum is 3; exceeding it sets the account status to `unverified`. *** ## Example Request ```bash theme={null} curl --request GET \ --url https://api.cleo-pay.com/v1/bank-accounts/ba_01j9kxm2p4fghe3t7q8wnvd5rc \ --header 'Authorization: Bearer ' ``` *** ## Example Response ```json theme={null} { "id": "ba_01j9kxm2p4fghe3t7q8wnvd5rc", "status": "verified", "type": "checking", "connectionType": "manual", "name": "Primary Operating Account", "institutionName": "Chase", "accountNumberLast4": "4321", "routingNumberLast4": "0021", "isDefault": true, "microDepositsInitiatedAt": "2024-10-01T10:00:00Z", "microDepositsCompletedAt": "2024-10-03T08:30:00Z", "verificationAttempts": 0 } ``` Poll this endpoint (or listen for `bank_account.*` webhooks) to track when `microDepositsCompletedAt` becomes non-null. Once deposits have landed, prompt the account holder to submit the two deposit amounts via the [verify micro-deposits](/api-reference/bank-accounts/verify-micro-deposits) endpoint. # GET /v1/bank-accounts — List Bank Accounts Source: https://docs.cleo-pay.com/api-reference/bank-accounts/list GET /v1/bank-accounts returns all bank accounts linked to your business. Response includes verification status and account metadata. Retrieve all bank accounts linked to your business. The response contains every account regardless of its current verification status, so you can use this endpoint to audit your connected accounts or present a selection UI to your users. ## HTTP Request ``` GET https://api.cleo-pay.com/v1/bank-accounts ``` ## Authentication Include your bearer token on every request: ``` Authorization: Bearer ``` *** ## Request Parameters This endpoint does not accept any query parameters, path parameters, or a request body. *** ## Response Fields An array of bank account objects. Returns an empty array if no accounts are linked to your business. Unique identifier for the bank account. Current verification status of the account. One of: * `pending_verification` — micro-deposits have been initiated and are awaiting confirmation. * `verified` — account is verified and ready to use for payments. * `unverified` — verification failed because the maximum number of attempts was exceeded. * `removed` — account has been deleted. The bank account type. Either `checking` or `savings`. How the account was connected. Either `manual` (linked via the API) or `plaid`. The API only creates `manual` accounts. The display name assigned to this account. The name of the financial institution. The last four digits of the account number. The last four digits of the routing number. Whether this account is the default account for your business. The timestamp at which micro-deposits were initiated, or `null` if not yet started. The timestamp at which micro-deposits landed in the account. Verification can be attempted from this point forward. The number of failed verification attempts made so far. The maximum is 3. *** ## Example Request ```bash theme={null} curl --request GET \ --url https://api.cleo-pay.com/v1/bank-accounts \ --header 'Authorization: Bearer ' ``` *** ## Example Response ```json theme={null} { "items": [ { "id": "ba_01j9kxm2p4fghe3t7q8wnvd5rc", "status": "verified", "type": "checking", "connectionType": "manual", "name": "Primary Operating Account", "institutionName": "Chase", "accountNumberLast4": "4321", "routingNumberLast4": "0001", "isDefault": true, "microDepositsInitiatedAt": "2024-10-01T10:00:00Z", "microDepositsCompletedAt": "2024-10-03T08:30:00Z", "verificationAttempts": 0 }, { "id": "ba_02k8lzn3q5gihf4u8r9xowe6sd", "status": "pending_verification", "type": "savings", "connectionType": "manual", "name": "Reserve Savings", "institutionName": "Bank of America", "accountNumberLast4": "8765", "routingNumberLast4": "0002", "isDefault": false, "microDepositsInitiatedAt": "2024-10-05T14:22:00Z", "microDepositsCompletedAt": null, "verificationAttempts": 0 } ] } ``` This endpoint is **not paginated**. All bank accounts linked to your business are returned in a single response. If your business has many accounts, consider filtering client-side by `status` to surface only the accounts relevant to your workflow. # POST /v1/bank-accounts/{bankAccountId}/verify-micro-deposits Source: https://docs.cleo-pay.com/api-reference/bank-accounts/verify-micro-deposits POST .../verify-micro-deposits completes bank account verification. Submit the two micro-deposit dollar amounts to confirm account ownership. Complete verification for a bank account by submitting the two micro-deposit amounts that were sent to it. When the amounts match what Cleo Pay deposited, the account status transitions to `verified` and becomes eligible for payments. This step confirms that your business has access to the bank account. ## HTTP Request ``` POST https://api.cleo-pay.com/v1/bank-accounts/{bankAccountId}/verify-micro-deposits ``` ## Authentication Include your bearer token on every request: ``` Authorization: Bearer ``` *** ## Path Parameters The unique identifier of the bank account to verify. This is the `id` returned when the account was created. *** ## Request Body The first micro-deposit amount in **dollars** (not cents). For example, `"0.05"` represents five cents. Check the account holder's bank statement for the exact value. The second micro-deposit amount in **dollars** (not cents). For example, `"0.07"` represents seven cents. Check the account holder's bank statement for the exact value. *** ## Response Fields On success, the API returns the updated bank account object with `status` set to `verified`. Unique identifier for the bank account. The updated verification status. Set to `verified` on a successful call, or `unverified` if the maximum number of attempts has been exceeded. The bank account type: `checking` or `savings`. How the account was connected. Always `manual` for accounts created via the API. The display name assigned to this account. The name of the financial institution. The last four digits of the account number. The last four digits of the routing number. Whether this account is the default account for your business. The timestamp at which micro-deposits were initiated. The timestamp at which micro-deposits landed in the account. The cumulative number of **failed** verification attempts. On a successful verification this value is not incremented. The maximum is 3. *** ## Example Request ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts/ba_01j9kxm2p4fghe3t7q8wnvd5rc/verify-micro-deposits \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "amount1": "0.05", "amount2": "0.07" }' ``` *** ## Example Response ```json theme={null} { "id": "ba_01j9kxm2p4fghe3t7q8wnvd5rc", "status": "verified", "type": "checking", "connectionType": "manual", "name": "Primary Operating Account", "institutionName": "Chase", "accountNumberLast4": "4321", "routingNumberLast4": "0021", "isDefault": false, "microDepositsInitiatedAt": "2024-10-01T10:00:00Z", "microDepositsCompletedAt": "2024-10-03T08:30:00Z", "verificationAttempts": 0 } ``` You have a **maximum of 3 attempts** to submit the correct amounts. Each incorrect submission increments `verificationAttempts`. If all 3 attempts are exhausted, the account status permanently transitions to `unverified` and can no longer be verified. You will need to delete it and link a new account. Submit amounts in **dollars**, not cents. For example, use `"0.05"` — not `"5"`. Submitting amounts in cents is a common mistake that will count as a failed attempt. You can only call this endpoint after `microDepositsCompletedAt` is non-null on the account, which indicates that the deposits have arrived. Check the account status using [GET /v1/bank-accounts/](/api-reference/bank-accounts/get) before attempting verification. # Archive Contact — POST /v1/contacts/{contactId}/archive Source: https://docs.cleo-pay.com/api-reference/contacts/archive POST /v1/contacts/{contactId}/archive sets a contact's status to archived, hiding it from default list results. Archived contacts can be restored. Use this endpoint to archive a contact. Archiving sets the contact's status to `archived` and hides it from the default results of `GET /v1/contacts`. Archived contacts can be retrieved by filtering with `status=archived` and can be restored to active status at any time using the unarchive endpoint. ## Base URL ``` POST https://api.cleo-pay.com/v1/contacts/{contactId}/archive ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Path Parameters The unique identifier of the contact to archive. ## Request Body This endpoint does not accept a request body. ## Response Returns the updated `Contact` object with HTTP status `200 OK` and `status: "archived"`. Unique identifier for the contact. The contact type: `payee` or `business`. The contact's updated status. Will be `archived`. The contact's display name. Email addresses associated with the contact. Phone number for the contact. Allowed payment direction: `bidirectional`, `can_pay`, `can_be_paid`, or `none`. Net payment terms in days, if configured. Summary of the contact's linked bank account. Last four digits of the bank account number. Tax information for the contact. Legal name associated with the tax ID. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. Whether this contact can currently receive payments. ISO 8601 timestamp of when the contact was created. ISO 8601 timestamp of the most recent update. ISO 8601 timestamp of when the contact was archived. Populated upon successful archival. ## Example Request ```bash curl theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/contacts/cnt_01hq1z2k3m4n5p6q7r8s9t0u/archive" \ --header "Authorization: Bearer " ``` ## Example Response ```json 200 theme={null} { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "archived", "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": 30, "bankAccount": { "last4": "7890" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "6789" }, "paymentReady": false, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-04-10T11:00:00Z", "archivedAt": "2024-04-10T11:00:00Z" } ``` Archiving is not the same as revoking. An **archived** contact has been intentionally hidden from your active contact list and can be restored using `POST /v1/contacts/{contactId}/unarchive`. A **revoked** contact has had its access removed by Cleo Pay and cannot be restored through the API. Use archiving to keep your active contact list clean without permanently deleting contact records. Archived contacts — and their associated payable history — remain accessible when filtered explicitly. # Create Contact — POST /v1/contacts API Reference Source: https://docs.cleo-pay.com/api-reference/contacts/create POST /v1/contacts creates a new payee or business contact. Optionally include bank account and tax info to make the contact payment-ready on creation. Use this endpoint to create a new contact in your account. Contacts can represent either a `payee` (a vendor you manage) or a `business` (an independent Cleo business). To make a contact payment-ready at creation time, include `bankAccount` and `taxInfo` in the request body. ## Base URL ``` POST https://api.cleo-pay.com/v1/contacts ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Request Body The display name for the contact. This is the primary identifier shown in the Cleo Pay dashboard. List of email addresses for the contact. Maximum 5 addresses. Only applicable to `payee`-type contacts — this field is ignored for `business`-type contacts. Phone number for the contact, in any standard format. Optional. Mailing address for the contact. Optional. Primary street address line. Secondary street address line (e.g., suite or unit number). Optional. City name. Two-letter US state code (e.g., `CA`, `NY`). US ZIP code. Bank account details for the contact. Providing this field makes the contact eligible to receive ACH payments. All fields within this object are **write-only** — they are never returned in API responses. Only `last4` is returned after creation. Full bank account number. **Write-only.** Nine-digit ABA routing number. **Write-only.** Type of bank account. Accepted values: `checking`, `savings`. Tax identification information for the contact. Providing this field is required for 1099 reporting. The full tax ID is **write-only** — only the last four digits are returned as `taxIdMask` after creation. Legal name as it appears on tax filings. Type of tax ID. Accepted values: `ein` (Employer Identification Number), `ssn` (Social Security Number). Full tax identification number. **Write-only** — only the last four digits are returned as `taxIdMask` in all subsequent responses. When `false` (the default), Cleo Pay checks for an existing contact with a matching name or email and returns an error if a potential duplicate is found. Set this to `true` to bypass the duplicate-contact guard and force creation. The `bankAccount.accountNumber`, `bankAccount.routingNumber`, and `taxInfo.taxId` fields are strictly write-only. They are accepted on creation but **never returned** in any API response. Use `bankAccount.last4` and `taxInfo.taxIdMask` to confirm the values that were saved. ## Response Returns the created `Contact` object with HTTP status `201 Created`. Unique identifier for the newly created contact. The contact type: `payee` or `business`. Initial status of the contact. Newly created contacts have a status of `active`. The display name provided in the request. Email addresses associated with the contact. Empty for `business`-type contacts. Phone number, if provided. Allowed payment direction: `bidirectional`, `can_pay`, `can_be_paid`, or `none`. Net payment terms in days, if configured. Summary of the linked bank account. Last four digits of the bank account number. Tax information summary. Legal name provided at creation. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. The full tax ID is never returned. Whether the contact can currently receive payments. This is `true` when a valid bank account is linked. ISO 8601 timestamp of contact creation. ISO 8601 timestamp of the most recent update. `null` immediately after creation. ISO 8601 timestamp of archival. `null` for newly created contacts. ## Example Request ```bash curl theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/contacts" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "address": { "streetOne": "123 Commerce Drive", "city": "Austin", "stateCode": "TX", "zipCode": "78701" }, "bankAccount": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxId": "12-3456789" }, "allowDuplicates": false }' ``` ## Example Response ```json 201 theme={null} { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "active", "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": null, "bankAccount": { "last4": "2012" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "6789" }, "paymentReady": true, "createdAt": "2024-04-10T12:00:00Z", "updatedAt": null, "archivedAt": null } ``` To add a contact without bank or tax details now and complete them later, omit `bankAccount` and `taxInfo` from the request. You can update the contact's display name, emails, phone, address, and net terms via a subsequent `PATCH /v1/contacts/{contactId}` call. # Get Contact — GET /v1/contacts/{contactId} Reference Source: https://docs.cleo-pay.com/api-reference/contacts/get GET /v1/contacts/{contactId} retrieves a single contact by ID with payment direction, bank account status, tax info, and paymentReady flag. Use this endpoint to fetch the full details of a single contact by its unique ID. The response includes the contact's current status, payment direction, bank account summary, tax information, and whether the contact is currently payment-ready. ## Base URL ``` GET https://api.cleo-pay.com/v1/contacts/{contactId} ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Path Parameters The unique identifier of the contact to retrieve. This is the same ID referenced as `contact.id` in payables. ## Response Returns a `Contact` object. Unique identifier for the contact. The contact type: `payee` or `business`. The contact's current status: `active`, `archived`, or `revoked`. The contact's display name, if set. Email addresses associated with the contact. Always empty for `business`-type contacts. Phone number for the contact. Always `null` for `business`-type contacts. Indicates the allowed payment direction for this contact. One of: * `bidirectional` — can both send and receive payments * `can_pay` — can only send payments * `can_be_paid` — can only receive payments * `none` — no payment capability configured Net payment terms in days, if configured. Summary of the contact's linked bank account. `null` if no bank account has been added. Last four digits of the bank account number. Tax information for the contact. `null` if no tax info has been provided. Legal name associated with the tax ID. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. The full tax ID is never returned. Whether this contact can currently receive payments. A contact is payment-ready when a valid bank account is on file and the contact is in `active` status. ISO 8601 timestamp of when the contact was created. ISO 8601 timestamp of the most recent update. `null` if the contact has never been updated. ISO 8601 timestamp of when the contact was archived. `null` if the contact is not archived. ## Example Request ```bash curl theme={null} curl --request GET \ --url "https://api.cleo-pay.com/v1/contacts/cnt_01hq1z2k3m4n5p6q7r8s9t0u" \ --header "Authorization: Bearer " ``` ## Example Response ```json 200 theme={null} { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "active", "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": 30, "bankAccount": { "last4": "7890" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "6789" }, "paymentReady": true, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-03-01T08:00:00Z", "archivedAt": null } ``` The `paymentReady` flag reflects whether a payment can be initiated to this contact right now. A contact may have a bank account on file but still have `paymentReady: false` if their account is in a non-active status. # List Contacts — GET /v1/contacts Endpoint Reference Source: https://docs.cleo-pay.com/api-reference/contacts/list GET /v1/contacts returns a paginated list of contacts. Filter by type, status, or free-text search. Revoked contacts are excluded by default. Use this endpoint to retrieve all contacts associated with your account. Results are paginated and can be filtered by contact type, status, or a free-text search string matching name or email. Unless you explicitly filter by `status=revoked`, revoked contacts are excluded from the response. ## Base URL ``` GET https://api.cleo-pay.com/v1/contacts ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Query Parameters The page number to retrieve. Minimum value is `1`. Number of contacts to return per page. Minimum value is `1`, maximum value is `100`. Filter contacts by type. Accepted values: * `payee` — a vendor managed by your business * `business` — an independent Cleo business Filter contacts by status. Accepted values: `active`, `archived`, `revoked`. By default, the response includes contacts with `active` or `archived` status. Revoked contacts are only returned when you explicitly pass `status=revoked`. Free-text search string. Matches against a contact's display name or email address. ## Response Returns a `ContactList` object. The total number of contacts matching the query, across all pages. Array of contact objects for the current page. Unique identifier for the contact. This is the same ID referenced as `contact.id` in payables. The contact type: `payee` or `business`. The contact's current status: `active`, `archived`, or `revoked`. The contact's display name, if set. List of email addresses associated with the contact. Always empty for `business`-type contacts. Phone number for the contact. Always `null` for `business`-type contacts. Indicates the allowed payment direction for this contact. One of: `bidirectional`, `can_pay`, `can_be_paid`, `none`. Net payment terms in days, if configured. Summary of the contact's linked bank account, if any. Last four digits of the bank account number. Tax information for the contact, if provided. Legal name associated with the tax ID. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. The full tax ID is never returned. Whether this contact can currently receive payments. ISO 8601 timestamp of when the contact was created. ISO 8601 timestamp of the most recent update, if any. ISO 8601 timestamp of when the contact was archived, if applicable. ## Example Request ```bash curl theme={null} curl --request GET \ --url "https://api.cleo-pay.com/v1/contacts?page=1&pageSize=25&type=payee&status=active" \ --header "Authorization: Bearer " ``` ## Example Response ```json 200 theme={null} { "totalCount": 2, "items": [ { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "active", "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": 30, "bankAccount": { "last4": "7890" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "1234" }, "paymentReady": true, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-03-01T08:00:00Z", "archivedAt": null }, { "id": "cnt_02ab3c4d5e6f7g8h9i0j1k2l", "type": "payee", "status": "active", "displayName": "Global Freight Partners", "emails": ["ap@globalfreight.com", "invoices@globalfreight.com"], "phone": null, "paymentDirection": "can_be_paid", "netTerms": null, "bankAccount": null, "taxInfo": null, "paymentReady": false, "createdAt": "2024-02-20T14:45:00Z", "updatedAt": null, "archivedAt": null } ] } ``` Use the `search` parameter to quickly locate a contact by name or email without needing to paginate through all results. # Restore Contact — POST /v1/contacts/{contactId}/unarchive Source: https://docs.cleo-pay.com/api-reference/contacts/unarchive POST /v1/contacts/{contactId}/unarchive restores an archived contact to active status. Revoked contacts cannot be unarchived through this endpoint. Use this endpoint to restore an archived contact to `active` status. Once unarchived, the contact will appear again in the default results of `GET /v1/contacts` and can be used in new payments. ## Base URL ``` POST https://api.cleo-pay.com/v1/contacts/{contactId}/unarchive ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Path Parameters The unique identifier of the contact to restore. ## Request Body This endpoint does not accept a request body. This endpoint only works for contacts with `status: "archived"`. Contacts with `status: "revoked"` cannot be restored through the API. Attempting to unarchive a revoked contact will return an error. ## Response Returns the updated `Contact` object with HTTP status `200 OK` and `status: "active"`. Unique identifier for the contact. The contact type: `payee` or `business`. The contact's updated status. Will be `active`. The contact's display name. Email addresses associated with the contact. Phone number for the contact. Allowed payment direction: `bidirectional`, `can_pay`, `can_be_paid`, or `none`. Net payment terms in days, if configured. Summary of the contact's linked bank account. Last four digits of the bank account number. Tax information for the contact. Legal name associated with the tax ID. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. Whether this contact can currently receive payments. ISO 8601 timestamp of when the contact was created. ISO 8601 timestamp of this update. Will be `null` after a successful unarchive. ## Example Request ```bash curl theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/contacts/cnt_01hq1z2k3m4n5p6q7r8s9t0u/unarchive" \ --header "Authorization: Bearer " ``` ## Example Response ```json 200 theme={null} { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "active", "displayName": "Acme Supplies LLC", "emails": ["billing@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": 30, "bankAccount": { "last4": "7890" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "6789" }, "paymentReady": true, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-04-10T14:30:00Z", "archivedAt": null } ``` After unarchiving, the contact's `archivedAt` field is reset to `null` and the contact resumes appearing in the default results of `GET /v1/contacts`. # Update Contact — PATCH /v1/contacts/{contactId} Reference Source: https://docs.cleo-pay.com/api-reference/contacts/update PATCH /v1/contacts/{contactId} updates a contact's display name, emails, phone, address, or net terms. Only payee contacts support most fields. Use this endpoint to update an existing contact. You can modify the display name on any contact type. Emails, phone, address, and net terms are only supported for `payee`-type contacts. All fields are optional — include only the fields you want to change. ## Base URL ``` PATCH https://api.cleo-pay.com/v1/contacts/{contactId} ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Path Parameters The unique identifier of the contact to update. ## Request Body Updated display name for the contact. Applies to both `payee` and `business`-type contacts. Updated list of email addresses. Maximum 5 addresses. **Payee contacts only.** This field **replaces** all existing email addresses on the contact. If you want to keep existing emails, include them in the array alongside any new addresses. Updated phone number. **Payee contacts only.** Updated mailing address. **Payee contacts only.** Primary street address line. Secondary street address line (e.g., suite or unit number). Optional. City name. Two-letter US state code (e.g., `CA`, `NY`). US ZIP code. Net payment terms in days. Must be `0` or greater. **Payee contacts only.** The `emails`, `phone`, `address`, and `netTerms` fields are only applicable to `payee`-type contacts. Sending these fields for a `business`-type contact will result in an error. ## Response Returns the updated `Contact` object with HTTP status `200 OK`. Unique identifier for the contact. The contact type: `payee` or `business`. The contact's current status: `active`, `archived`, or `revoked`. The updated display name. The full, updated list of email addresses. Empty for `business`-type contacts. The updated phone number. Allowed payment direction: `bidirectional`, `can_pay`, `can_be_paid`, or `none`. Updated net payment terms in days, if set. Summary of the contact's linked bank account. Last four digits of the bank account number. Tax information for the contact. Legal name associated with the tax ID. Type of tax ID: `ein` or `ssn`. Last four digits of the tax ID. Whether this contact can currently receive payments. ISO 8601 timestamp of when the contact was created. ISO 8601 timestamp of this update. ISO 8601 timestamp of archival, if applicable. ## Example Request ```bash curl theme={null} curl --request PATCH \ --url "https://api.cleo-pay.com/v1/contacts/cnt_01hq1z2k3m4n5p6q7r8s9t0u" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "displayName": "Acme Supplies LLC (Updated)", "emails": ["billing@acmesupplies.com", "ap@acmesupplies.com"], "netTerms": 45 }' ``` ## Example Response ```json 200 theme={null} { "id": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "type": "payee", "status": "active", "displayName": "Acme Supplies LLC (Updated)", "emails": ["billing@acmesupplies.com", "ap@acmesupplies.com"], "phone": "+15550001234", "paymentDirection": "can_be_paid", "netTerms": 45, "bankAccount": { "last4": "7890" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxIdMask": "6789" }, "paymentReady": true, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-04-10T09:15:00Z", "archivedAt": null } ``` When updating emails, always include the complete desired list. For example, if the contact currently has one email and you want to add a second, pass both addresses in the `emails` array — omitting the original will remove it. # POST /v1/payables — Create a New Payable Source: https://docs.cleo-pay.com/api-reference/payables/create POST /v1/payables creates a new payable (invoice). Provide contact info, total amount in cents, and optional line items, dates, and memo. The Create Payable endpoint lets you programmatically generate a new payable (invoice) in Cleo Pay. You supply at minimum a contact and a total amount; optionally, you can attach structured line items, set issue and due dates, provide your own reference number, and add a free-text description. Newly created payables start in `draft` status and must proceed through your configured approval workflow before payment can be initiated. ## Request **`POST https://api.cleo-pay.com/v1/payables`** Include your bearer token and set the content type on every request: ``` Authorization: Bearer Content-Type: application/json ``` ### Request Body Parameters The counterparty for this payable. Cleo Pay will match against existing contacts by email, or create a new contact record if no match is found. Full display name of the contact (e.g. `"Acme Corporation"`). Email address of the contact. Used to match or create a contact record. Optional, but recommended for deduplication. Total invoice amount expressed in cents. Must be `1` or greater (e.g. pass `10000` for \$100.00). If you also supply `items`, ensure this value equals the sum of all `items[].totalCents` to avoid reconciliation discrepancies. Payment due date in `YYYY-MM-DD` format (e.g. `"2024-08-15"`). Optional. When omitted, the payable is created without a due date. Invoice issue date in `YYYY-MM-DD` format (e.g. `"2024-07-15"`). Optional. Defaults to the creation date when omitted. Your own invoice or purchase-order reference number (e.g. `"INV-1042"`). Optional. This value is stored as-is and surfaced in the dashboard alongside the system-generated `publicId`. Free-text memo or description for this payable (e.g. `"Q3 software licensing fees"`). Optional. Structured line items that make up the invoice. Optional. Each element represents one line on the invoice. Short label for the line item (e.g. `"Professional Services"`). Optional. Longer description of the line item. Optional. Number of units for this line item. Must be a positive number. Price per unit in cents. Must be a positive number (e.g. `5000` = \$50.00 per unit). ### Example Request ```bash theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/payables" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "contact": { "name": "Acme Corporation", "email": "ap@acme.example.com" }, "totalAmountCents": 250000, "dueDate": "2024-08-15", "issueDate": "2024-07-15", "customNumber": "INV-1042", "description": "Q3 software licensing fees", "items": [ { "name": "Enterprise License", "description": "Annual seat license — 10 users", "quantity": 10, "unitAmountCents": 20000 }, { "name": "Implementation Support", "description": "Onboarding and configuration services", "quantity": 5, "unitAmountCents": 10000 } ] }' ``` ## Response A successful request returns HTTP `201 Created` with the full `PayableDetail` object, including all line items as stored. Internal opaque identifier for the new payable. Use this when calling other endpoints that reference a specific payable. Stable public identifier shown in the Cleo Pay dashboard. Initial lifecycle status of the payable. Newly created payables always start as `draft`. Initial payment status. Always `none` for newly created payables. Total invoice amount in cents, as provided in the request. Amount paid so far, in cents. Always `0` for newly created payables. Outstanding balance in cents. Equal to `totalAmountCents` for newly created payables. Payment due date in `YYYY-MM-DD` format, or `null` if not provided. Invoice issue date in `YYYY-MM-DD` format, or `null` if not provided. Your custom reference number as stored, or `null` if not provided. Memo or description as stored, or `null` if not provided. Resolved contact record associated with this payable. Internal identifier of the matched or created contact record. Display name of the contact as stored. Line items attached to this payable, as stored. Line item label, or `null` if not provided. Line item description, or `null` if not provided. Number of units for this line item. Price per unit in cents. Computed total for this line item in cents (`quantity × unitAmountCents`). ### Example Response ```json theme={null} { "id": "pay_01hx9k2mq3fvb8n7cptd6ewrja", "publicId": "PAY-2024-00042", "status": "draft", "paymentStatus": "none", "totalAmountCents": 250000, "amountPaidCents": 0, "remainingAmountCents": 250000, "dueDate": "2024-08-15", "issueDate": "2024-07-15", "customNumber": "INV-1042", "description": "Q3 software licensing fees", "contact": { "id": "con_01hx8a1bc2def3gh4ijk5lm6no", "displayName": "Acme Corporation" }, "items": [ { "name": "Enterprise License", "description": "Annual seat license — 10 users", "quantity": 10, "unitAmountCents": 20000, "totalCents": 200000 }, { "name": "Implementation Support", "description": "Onboarding and configuration services", "quantity": 5, "unitAmountCents": 10000, "totalCents": 50000 } ] } ``` # GET /v1/payables/{payableId} — Retrieve a Payable Source: https://docs.cleo-pay.com/api-reference/payables/get GET /v1/payables/{payableId} retrieves a single payable by ID, including full line items, status, payment progress, and contact details. The Get Payable endpoint retrieves the complete detail record for a single payable, identified by its internal `id`. The response includes every field available on a payable — lifecycle and payment statuses, amount breakdowns, dates, your custom reference number, the associated contact, and the full structured line-item array. Use this endpoint to display invoice detail pages, poll for status changes, or reconcile payment progress in your own systems. ## Request **`GET https://api.cleo-pay.com/v1/payables/{payableId}`** Include your bearer token in every request: ``` Authorization: Bearer ``` ### Path Parameters The internal opaque `id` of the payable to retrieve (e.g. `pay_01hx9k2mq3fvb8n7cptd6ewrja`). This is the `id` field returned when you [create a payable](/api-reference/payables/create) or [list payables](/api-reference/payables/list) — not the `publicId`. ### Example Request ```bash theme={null} curl --request GET \ --url "https://api.cleo-pay.com/v1/payables/pay_01hx9k2mq3fvb8n7cptd6ewrja" \ --header "Authorization: Bearer " ``` ## Response A successful request returns HTTP `200` with the full `PayableDetail` object. Internal opaque identifier for the payable. Stable public identifier shown in the Cleo Pay dashboard. Safe to display to end users or reference in support conversations. Current lifecycle status of the payable. One of: `draft`, `pending_approval`, `approved`, `declined`, `blocked`, `void`. Current payment processing status. One of: `none`, `scheduled`, `processing`, `settled`, `failed`, `clawback`. Total invoice amount in cents (e.g. `250000` = \$2,500.00). Amount already paid against this payable, in cents. Outstanding balance remaining, in cents. Equals `totalAmountCents - amountPaidCents`. Payment due date in `YYYY-MM-DD` format, or `null` if not set. Invoice issue date in `YYYY-MM-DD` format, or `null` if not set. Your custom invoice or purchase-order reference number, or `null` if not provided. Free-text memo or description, or `null` if not provided. The counterparty associated with this payable. Internal identifier of the contact record, or `null` if not resolved. Human-readable name for the contact, or `null` if not available. Structured line items attached to this payable. Returns an empty array if no items were provided at creation. Short label for the line item, or `null` if not provided. Longer description of the line item, or `null` if not provided. Number of units for this line item. Price per unit in cents. Computed total for this line item in cents (`quantity × unitAmountCents`). ### Example Response ```json theme={null} { "id": "pay_01hx9k2mq3fvb8n7cptd6ewrja", "publicId": "PAY-2024-00042", "status": "approved", "paymentStatus": "processing", "totalAmountCents": 250000, "amountPaidCents": 0, "remainingAmountCents": 250000, "dueDate": "2024-08-15", "issueDate": "2024-07-15", "customNumber": "INV-1042", "description": "Q3 software licensing fees", "contact": { "id": "con_01hx8a1bc2def3gh4ijk5lm6no", "displayName": "Acme Corporation" }, "items": [ { "name": "Enterprise License", "description": "Annual seat license — 10 users", "quantity": 10, "unitAmountCents": 20000, "totalCents": 200000 }, { "name": "Implementation Support", "description": "Onboarding and configuration services", "quantity": 5, "unitAmountCents": 10000, "totalCents": 50000 } ] } ``` Every payable carries two distinct identifiers. The **`id`** (e.g. `pay_01hx9k2mq3fvb8n7cptd6ewrja`) is the internal opaque ID used in all API calls — pass it as the `payableId` path parameter here, or wherever an API endpoint requires a payable reference. The **`publicId`** (e.g. `PAY-2024-00042`) is the stable, human-readable identifier displayed in the Cleo Pay dashboard and safe to surface to your customers or in support communications. Never use `publicId` as the path parameter for API requests. # GET /v1/payables — List All Payables Source: https://docs.cleo-pay.com/api-reference/payables/list GET /v1/payables returns a paginated list of payables. Filter by status, search by text, and paginate with page and pageSize parameters. The List Payables endpoint returns a paginated collection of all payables in your account. You can narrow results by lifecycle status, run a free-text search across invoice numbers, descriptions, and contact names, and control page size to suit your UI or data-pipeline needs. Each item in the response is a summary-level [Payable](#response) object; retrieve a single payable with [GET /v1/payables/](/api-reference/payables/get) to access full line-item detail. ## Request **`GET https://api.cleo-pay.com/v1/payables`** Include your bearer token in every request: ``` Authorization: Bearer ``` ### Query Parameters The page number to return. Must be `1` or greater. Number of payables to return per page. Minimum `1`, maximum `100`. Filter results to payables matching a specific lifecycle status. Accepted values: | Value | Description | | ------------------ | ------------------------------------------ | | `draft` | Created but not yet submitted for approval | | `pending_approval` | Submitted and awaiting review | | `approved` | Approved and eligible for payment | | `declined` | Rejected during the approval process | | `blocked` | Temporarily blocked from processing | | `void` | Cancelled and no longer actionable | Free-text search string. Matched against invoice numbers, descriptions, and contact display names. ### Example Request ```bash theme={null} curl --request GET \ --url "https://api.cleo-pay.com/v1/payables?page=1&pageSize=25&status=approved" \ --header "Authorization: Bearer " ``` ## Response A successful request returns HTTP `200` with a JSON body containing a paginated list of payable objects. Array of Payable objects matching the query. Internal opaque identifier for the payable. Use this id when calling other endpoints that reference a specific payable. Stable public identifier shown in the Cleo Pay dashboard. Safe to display in your own UI. Current lifecycle status of the payable. One of: `draft`, `pending_approval`, `approved`, `declined`, `blocked`, `void`. Current payment processing status. One of: `none`, `scheduled`, `processing`, `settled`, `failed`, `clawback`. Total invoice amount in cents (e.g. `10000` = \$100.00). Amount already paid against this payable, in cents. Outstanding balance remaining, in cents. Equals `totalAmountCents - amountPaidCents`. Payment due date in `YYYY-MM-DD` format, or `null` if not set. Invoice issue date in `YYYY-MM-DD` format, or `null` if not set. Your own invoice or reference number, or `null` if not provided. Free-text memo or description, or `null` if not provided. The counterparty associated with this payable. Internal identifier of the contact record, or `null` if not resolved. Human-readable name for the contact, or `null` if not available. Total number of payables matching the current query, across all pages. Use this value to calculate page counts and drive pagination controls. ### Example Response ```json theme={null} { "items": [ { "id": "pay_01hx9k2mq3fvb8n7cptd6ewrja", "publicId": "PAY-2024-00042", "status": "approved", "paymentStatus": "scheduled", "totalAmountCents": 250000, "amountPaidCents": 0, "remainingAmountCents": 250000, "dueDate": "2024-08-15", "issueDate": "2024-07-15", "customNumber": "INV-1042", "description": "Q3 software licensing fees", "contact": { "id": "con_01hx8a1bc2def3gh4ijk5lm6no", "displayName": "Acme Corporation" } }, { "id": "pay_01hx9k2mq3fvb8n7cptd6ewrjb", "publicId": "PAY-2024-00041", "status": "approved", "paymentStatus": "settled", "totalAmountCents": 75000, "amountPaidCents": 75000, "remainingAmountCents": 0, "dueDate": "2024-07-01", "issueDate": "2024-06-01", "customNumber": "INV-1041", "description": "Implementation services — Phase 1", "contact": { "id": "con_01hx8a1bc2def3gh4ijk5lm6np", "displayName": "Globex Partners LLC" } } ], "totalCount": 84 } ``` Use `totalCount` together with your chosen `pageSize` to calculate the total number of pages: `Math.ceil(totalCount / pageSize)`. For example, `totalCount: 84` with `pageSize: 25` yields 4 pages. Increment `page` from `1` to that ceiling to iterate through all results. # Create Payment — POST /v1/payments API Reference Source: https://docs.cleo-pay.com/api-reference/payments/create POST /v1/payments sends an ACH payment against a payable or directly to a contact. Supports scheduling, partial amounts, and clearing speeds. Use this endpoint to initiate an ACH payment. You can pay against an existing payable or send a payment directly to a contact by providing a `contactId`. Payments support optional scheduling, partial amounts, and accelerated clearing speeds. Exactly one of `payableId` or `contactId` is required per request. Providing both or neither will result in a validation error. ## Base URL ``` POST https://api.cleo-pay.com/v1/payments ``` ## Authentication All requests must include a Bearer token in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` ## Request Body The ID of an existing payable to pay. Use this when paying against a specific invoice or bill. Mutually exclusive with `contactId` — provide exactly one. The ID of a contact to pay directly, without a pre-existing payable. A payable is automatically generated. Mutually exclusive with `payableId` — provide exactly one. Payment amount in US cents. Must be at least `1`. * When using `contactId`: **required**. * When using `payableId`: optional. Defaults to the remaining unpaid balance on the payable. You may specify a partial amount to make a partial payment. The ID of the payer bank account to debit. Retrieve available bank account IDs from `GET /v1/bank-accounts`. A memo or note to attach to the auto-generated payable. **Applies only when using `contactId` mode.** Ignored when `payableId` is provided. A custom reference number for the auto-generated payable, up to 21 characters. **Applies only when using `contactId` mode.** Ignored when `payableId` is provided. The ID of a category to assign to the auto-generated line item. **Applies only when using `contactId` mode.** Ignored when `payableId` is provided. Requested ACH clearing speed. Accepted values: `STANDARD`, `NEXT_AVAILABLE`. The `NEXT_AVAILABLE` clearing speed requires that accelerated clearing be enabled on your account. Contact your Cleo Pay account manager to enable this feature. A future date on which to execute the payment, formatted as `YYYY-MM-DD`. The payment will be initiated at **9:00 AM New York time** on that date. Constraints: * Must be at least **tomorrow's date** (no same-day scheduling). * Not supported for payments that are in `held` status. The `description`, `customNumber`, and `categoryId` fields are only meaningful when initiating a payment with `contactId`. They configure the payable that Cleo Pay automatically creates on your behalf. These fields have no effect when `payableId` is supplied. ## Response Returns a `CreatePaymentResponse` object with HTTP status `201 Created`. The ID of the payable associated with this payment. When using `contactId` mode, this is the ID of the automatically generated payable. Array of one or more `Payment` objects created for this request. The `amountCents` values across all objects sum to the total requested amount. Large payment amounts may be automatically split into multiple `Payment` objects by Cleo Pay. Always treat the `payments` array as potentially containing more than one entry. Unique identifier for the payment. Current status of the payment. One of: * `pending` — created but not yet submitted * `scheduled` — set to execute on a future date * `processing` — submitted to the ACH network * `held` — flagged for review before processing * `completed` — successfully settled * `failed` — processing failed; see `failureReason` * `cancelled` — cancelled before processing * `refunded` — payment was reversed after completion Amount of this specific payment in US cents. May be less than the total requested amount if the payment was split. ID of the contact associated with this payment. ID of the payable associated with this payment. ID of the bank account that was debited. The scheduled execution datetime for this payment. ISO 8601 timestamp of when the payment was processed. `null` until processing is complete. Human-readable description of why the payment failed. `null` for non-failed payments. ISO 8601 timestamp of when the payment was created. ISO 8601 timestamp of the most recent status update. ## Example Requests ```bash payableId mode theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/payments" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "payableId": "pvb_03mn4op5qr6st7uv8wx9yz0a", "bankAccountId": "bac_07gh8ij9kl0mn1op2qr3st4u", "amountCents": 250000 }' ``` ```bash contactId mode theme={null} curl --request POST \ --url "https://api.cleo-pay.com/v1/payments" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "contactId": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "amountCents": 500000, "bankAccountId": "bac_07gh8ij9kl0mn1op2qr3st4u", "description": "Q1 consulting services", "customNumber": "INV-2024-0042", "scheduledAt": "2024-04-15" }' ``` ## Example Response ```json 201 theme={null} { "payableId": "pvb_03mn4op5qr6st7uv8wx9yz0a", "payments": [ { "id": "pay_09ab1cd2ef3gh4ij5kl6mn7o", "status": "scheduled", "amountCents": 500000, "contactId": "cnt_01hq1z2k3m4n5p6q7r8s9t0u", "payableId": "pvb_03mn4op5qr6st7uv8wx9yz0a", "bankAccountId": "bac_07gh8ij9kl0mn1op2qr3st4u", "scheduledAt": "2024-04-15T13:00:00Z", "processedAt": null, "failureReason": null, "createdAt": "2024-04-10T10:00:00Z", "updatedAt": "2024-04-10T10:00:00Z" } ] } ``` For very large payment amounts, Cleo Pay may split the transaction into multiple `Payment` objects. The `amountCents` values across all entries in the `payments` array will always sum to the total amount you requested. To check available bank account IDs for `bankAccountId`, call `GET /v1/bank-accounts` before initiating a payment. # Cleo Pay API Authentication: Bearer Token Setup and Usage Source: https://docs.cleo-pay.com/authentication Add a Bearer token to every Cleo Pay API request to authenticate. Learn the header format, error codes, and token security best practices. Every request to the Cleo Pay API must include a valid Bearer token in the `Authorization` header. Requests without a token, or with an expired or malformed token, are rejected before they reach any endpoint. Tokens are JSON Web Tokens (JWTs) issued by Cleo Pay — they encode your identity and permissions and are verified server-side on every call. ## Sending the Authorization header Add the following header to every API request: ``` Authorization: Bearer ``` Replace `` with the JWT you received from Cleo Pay. ### curl example ```bash theme={null} curl --request GET \ --url https://api.cleo-pay.com/v1/payables \ --header "Authorization: Bearer " ``` ### Request with a JSON body When sending a request body (e.g. creating a payment), include `Content-Type: application/json` alongside the `Authorization` header: ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "contactId": "c_01hq2k5r8e4vwxyz", "amountCents": 50000, "bankAccountId": "ba_01hq2k5r8e4vabc" }' ``` ## Token format Cleo Pay tokens are **JWTs (JSON Web Tokens)**, a standard format consisting of three Base64URL-encoded segments separated by dots: `header.payload.signature`. The payload contains your tenant identity and permission scopes. You do not need to parse the token yourself — simply forward it in the header as-is. Tokens have an expiration time encoded in their payload (`exp` claim). If your token expires, requests will return a `401 Unauthorized` error. Obtain a fresh token and retry. ## Error responses ### 401 Unauthorized Returned when the token is missing, malformed, or expired. ```json theme={null} { "statusCode": 401, "message": "Unauthorized" } ``` **Resolution:** Check that you're including the `Authorization: Bearer ` header and that your token has not expired. ### 403 Forbidden Returned when the token is valid and authenticated, but does not have permission to perform the requested action. ```json theme={null} { "statusCode": 403, "message": "Forbidden" } ``` **Resolution:** Confirm that your token has the required scopes or roles for the operation. Contact Cleo Pay support if you believe this is a permissions misconfiguration. ## Security best practices **Never expose your API token in client-side code, browser JavaScript, mobile apps, or application logs.** Treat your Bearer token with the same care as a password. Anyone who obtains your token can make authenticated API calls on your behalf until it expires. * Store tokens in environment variables or a secrets manager (e.g. AWS Secrets Manager, HashiCorp Vault). * Rotate tokens regularly and immediately if you suspect a compromise. * Strip `Authorization` headers from any logs, traces, or error reports before storing or sharing them. ## Next steps * Follow the [Quickstart](/quickstart) to make your first authenticated API call end to end. * Read about [Payments](/concepts/payments) to understand what you can do once you're authenticated. # Bank Accounts: Link and Verify Your Funding Sources Source: https://docs.cleo-pay.com/concepts/bank-accounts Learn how Cleo Pay bank accounts work, including manual linking, micro-deposit verification, and the verification lifecycle states. Bank accounts in Cleo Pay represent your business's funding sources for outbound ACH payments. Before you can send a payment, you must link at least one bank account and verify that you own it. Cleo Pay supports two ways to connect an account: manually via the API (by supplying account and routing numbers), or through Plaid via the dashboard. The API only creates manual connections. ## Connection types | Type | How it's created | Description | | -------- | ---------------- | --------------------------------------------------------------------------------------------------------- | | `manual` | API or dashboard | You supply the full account number and routing number directly. Ownership is verified via micro-deposits. | | `plaid` | Dashboard only | Connected through Plaid's bank linking flow. Not available via the REST API. | ## Account types Bank accounts can be either `checking` or `savings`. Most business payment accounts are `checking`. ## Verification lifecycle When you create a bank account via the API, it enters the `pending_verification` status immediately. You must complete micro-deposit verification before the account can fund any payments. | Status | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `pending_verification` | The account has been created. Micro-deposits have been initiated and are on their way to the account. Awaiting verification. | | `verified` | Micro-deposit amounts were submitted correctly. The account is ready to fund payments. | | `unverified` | All 3 verification attempts were exhausted with incorrect amounts. The account cannot be verified and must be re-added. | | `removed` | The account has been removed and is no longer available. | The standard path is: `pending_verification` → **`verified`** If verification attempts are exhausted: `pending_verification` → **`unverified`** ## Micro-deposit verification flow Micro-deposits are two small credits (each typically under \$0.10) sent to your bank account to confirm ownership. 1. **Link the account** — call `POST /v1/bank-accounts` with the full account and routing numbers. The account status becomes `pending_verification` and `microDepositsInitiatedAt` is set. 2. **Wait for deposits** — check your bank statement. The two deposits typically arrive within 1–2 business days. 3. **Submit the amounts** — call `POST /v1/bank-accounts/{id}/verify-micro-deposits` with both deposit amounts as decimal dollar strings. 4. **Confirmed** — on success, `status` changes to `verified` and `microDepositsCompletedAt` is set. ### Verification request format Amounts are submitted as decimal dollar strings — **not cents**: ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts/{id}/verify-micro-deposits \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "amount1": "0.05", "amount2": "0.08" }' ``` You have a maximum of **3 verification attempts** (`verificationAttempts` field). After 3 failed attempts, the account moves to `unverified` and can no longer be verified. You will need to create a new bank account entry and restart the process. ## Security: write-only account numbers Full account and routing numbers are **write-only**. You submit them when creating a bank account, but they are never returned in any API response. Responses include only the last 4 digits of each: ```json theme={null} { "accountNumberLast4": "9012", "routingNumberLast4": "0021" } ``` Store full account numbers securely before submitting them to the API. Cleo Pay does not provide a way to retrieve them after creation. ## Default bank account The `isDefault` flag marks one bank account as the default funding source. When you create a payment without specifying a `bankAccountId`, Cleo Pay uses the default account automatically. You can have only one default account at a time. Marking a new account as default will un-mark the previous default. ## Bank account fields reference | Field | Type | Description | | -------------------------- | -------- | ------------------------------------------------------------------------------ | | `id` | string | Unique identifier for the bank account. | | `status` | string | `pending_verification`, `verified`, `unverified`, or `removed`. | | `type` | string | `checking` or `savings`. | | `connectionType` | string | `manual` or `plaid`. | | `name` | string | Your label for the account (e.g. `"Acme Main Checking"`). Nullable. | | `institutionName` | string | Bank name, if available (typically populated for Plaid connections). Nullable. | | `accountNumberLast4` | string | Last 4 digits of the account number. Nullable. | | `routingNumberLast4` | string | Last 4 digits of the routing number. Nullable. | | `isDefault` | boolean | Whether this is the default funding account. | | `microDepositsInitiatedAt` | datetime | When micro-deposits were sent. Nullable. | | `microDepositsCompletedAt` | datetime | When verification was successfully completed. `null` until verified. | | `verificationAttempts` | integer | Number of verification attempts made so far. Maximum is 3. | ## Example bank account object ```json theme={null} { "id": "ba_01hq2k5r8e4vabc123", "status": "verified", "type": "checking", "connectionType": "manual", "name": "Acme Main Checking", "institutionName": null, "accountNumberLast4": "9012", "routingNumberLast4": "0021", "isDefault": true, "microDepositsInitiatedAt": "2025-03-15T14:00:00Z", "microDepositsCompletedAt": "2025-03-17T10:22:00Z", "verificationAttempts": 1 } ``` ## Next steps * [Quickstart](/quickstart) — see the full bank account linking and verification flow in context. * [Payments](/concepts/payments) — use your verified bank account to fund ACH payments. # Contacts: Manage Your Vendors and Payees in Cleo Pay Source: https://docs.cleo-pay.com/concepts/contacts Contacts represent vendors and payees in Cleo Pay. Learn about contact types, statuses, payment direction, tax info, and the paymentReady flag. A contact in Cleo Pay represents an entity you can pay or do business with — typically a vendor, supplier, or contractor. Every payment you send is associated with a contact, either directly or through a payable. Contacts store bank account details, tax information, and payment configuration so you can send funds reliably without re-entering details on every transaction. ## Contact types Cleo Pay has two contact types that determine how the contact record is managed: | Type | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payee` | A vendor or payee that **your business manages**. You supply and control their bank account details and contact information. Most vendor and contractor contacts are `payee` type. | | `business` | An **independent Cleo Pay business** with its own account. Their details are managed on their side. Your organization has a relationship with them on the platform. | When creating a contact via the API, you will typically create `payee` contacts. The `business` type is established through the Cleo Pay platform when two businesses connect with each other. ## Contact status | Status | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | The contact is in good standing and available for payments. | | `archived` | The contact has been archived. Archived contacts are no longer available for new payments. The `archivedAt` timestamp records when this happened. | | `revoked` | The contact's access or relationship has been revoked. | ## Payment direction The `paymentDirection` field controls which direction funds can flow between your business and this contact. | Value | Meaning | | --------------- | ---------------------------------------------------- | | `bidirectional` | Payments can flow both to and from this contact. | | `can_pay` | Your business can send payments **to** this contact. | | `can_be_paid` | This contact can send payments **to** your business. | | `none` | No payment flow is currently configured. | For outbound vendor payments, you need a contact with `paymentDirection` of `can_pay` or `bidirectional`. If the direction is `none` or `can_be_paid`, you cannot send payments to that contact. ## paymentReady The `paymentReady` boolean is a convenience flag that tells you at a glance whether a contact can currently receive a payment. It is `true` only when all required conditions are met — the contact is `active`, has a valid bank account on file, and payment direction allows outbound transfers. Always check `paymentReady: true` before initiating a payment to a contact. If it is `false`, the payment will enter `held` status and wait until the contact's details are complete. ## Bank account details When a contact has a bank account on file, the `bankAccount` object exposes the last 4 digits for identification purposes. Full account numbers are never returned in any API response. ```json theme={null} "bankAccount": { "last4": "1234" } ``` If no bank account is associated with the contact, `bankAccount` is `null`. ## Tax information The `taxInfo` object stores the contact's tax identity for 1099 and compliance purposes. | Field | Description | | ----------- | ---------------------------------------------------------------------------------------------------------- | | `legalName` | The contact's legal business or individual name as it appears on tax filings. Nullable. | | `taxIdType` | `"ein"` (Employer Identification Number), `"ssn"` (Social Security Number), or `null` if not yet provided. | | `taxIdMask` | The last 4 digits of the tax ID only — the full number is never returned. Nullable. | ## Emails The `emails` field is an array of email addresses associated with the contact. This field is populated for `payee` contacts; for `business`-type contacts the array is always empty, as their contact details are managed on their own account. Emails are used for notifications and remittance communications. ## Phone The `phone` field is a string containing the contact's phone number. For `business`-type contacts this field is always `null`, as their details are self-managed. ## Net terms The `netTerms` field is an integer representing the number of days after invoice issuance that payment is due. For example, `netTerms: 30` corresponds to Net 30 payment terms. This value is informational and used when generating payables for this contact. ## Duplicate guard When creating a contact, Cleo Pay checks for existing contacts with the same name or details. If a potential duplicate is detected, the request will fail with an error. To proceed with creating the contact anyway, include `"allowDuplicates": true` in your request body. ## Example contact object ```json theme={null} { "id": "con_01hq9r2m7tuvwxyz", "type": "payee", "status": "active", "displayName": "Globex Supplies LLC", "emails": [ "billing@globex-supplies.com", "ap@globex-supplies.com" ], "phone": "+1-555-867-5309", "paymentDirection": "can_pay", "paymentReady": true, "netTerms": 30, "bankAccount": { "last4": "1234" }, "taxInfo": { "legalName": "Globex Supplies LLC", "taxIdType": "ein", "taxIdMask": "7890" }, "createdAt": "2025-01-10T09:00:00Z", "updatedAt": "2025-03-01T14:30:00Z", "archivedAt": null } ``` ## Next steps * [Bank Accounts](/concepts/bank-accounts) — understand how your own bank accounts are linked and verified. * [Payments](/concepts/payments) — send an ACH payment to a contact. * [Payables](/concepts/payables) — create an invoice associated with a contact. # Payables: Managing the Invoice Lifecycle in Cleo Pay Source: https://docs.cleo-pay.com/concepts/payables Payables are invoices in Cleo Pay. Learn the document status lifecycle, payment progress tracking, line items, and how to use them in your AP workflow. A payable represents an invoice or bill in Cleo Pay. You create a payable to record what you owe a vendor, route it through an approval workflow, and then initiate one or more payments against it. Cleo Pay tracks two independent dimensions for every payable: the **document lifecycle** (is the invoice approved?) and the **payment status** (have funds been sent?). Understanding both dimensions is key to building reliable AP automation. ## Document lifecycle status The document status reflects where the payable sits in your approval workflow. | Status | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------- | | `draft` | The payable has been created but not yet submitted for approval. It cannot be paid in this state. | | `pending_approval` | The payable has been submitted and is awaiting a reviewer's decision. | | `approved` | The payable has been approved and is eligible for payment. | | `declined` | A reviewer has rejected the payable. It cannot be paid. | | `blocked` | The payable has been administratively blocked. It cannot be paid. | | `void` | The payable has been voided and is no longer active. | The typical happy path is: `draft` → `pending_approval` → `approved`. ## Payment status The payment status tracks the financial progress of the payable, independently of its document status. A payable must be `approved` before a payment can be initiated against it. | Payment Status | Meaning | | -------------- | ----------------------------------------------------------------------- | | `none` | No payment has been initiated yet. | | `scheduled` | A payment has been scheduled but not yet sent. | | `processing` | Funds are in transit via ACH. | | `settled` | All funds have been received by the payee. The payable is fully paid. | | `failed` | The payment attempt failed (e.g. insufficient funds or bank rejection). | | `clawback` | Previously settled funds have been reversed or recalled. | These two status fields are **orthogonal** — a payable can be `approved` with payment status `none` (approved but not yet paid), or `approved` with payment status `processing` (approved and payment in flight). Always check both fields when building status-driven logic. ## IDs: `id` vs `publicId` Every payable has two identifiers: * **`id`** — an opaque internal identifier used in API requests (e.g. when creating a payment against a payable via `payableId`). * **`publicId`** — a stable, human-readable reference number shown in the dashboard and suitable for displaying in UIs, statements, or email subject lines (e.g. `INV-00042`). Also exposed as `customNumber` when you supply your own invoice number. Use `id` in all API calls. Use `publicId` or `customNumber` in customer-facing surfaces. ## Amount fields All amounts are integers in **cents**. | Field | Description | | ---------------------- | --------------------------------------------------------------------------- | | `totalAmountCents` | The full invoice amount. | | `amountPaidCents` | The cumulative amount paid so far across all payments. | | `remainingAmountCents` | `totalAmountCents` minus `amountPaidCents`. This is how much is still owed. | When sending a payment against a payable without specifying `amountCents`, Cleo Pay defaults to the `remainingAmountCents` value, paying the invoice in full. ## Date fields | Field | Format | Description | | ----------- | ------------ | ------------------------------------------ | | `issueDate` | `YYYY-MM-DD` | The date the invoice was issued. Nullable. | | `dueDate` | `YYYY-MM-DD` | The date payment is due. Nullable. | ## Contact association Each payable is associated with a contact representing the vendor. The contact object on a payable includes the contact's `id` and `displayName`, allowing you to look up the full contact record at `/v1/contacts/{id}` if needed. ## Line items Payables support an array of `items` representing individual line items on the invoice. | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------- | | `name` | string | Short label for the line item (e.g. `"Consulting Services"`). | | `description` | string | Optional longer description. | | `quantity` | number | Number of units. | | `unitAmountCents` | integer | Price per unit in cents. | | `totalCents` | integer | `quantity × unitAmountCents`, in cents. | ## Example payable object ```json theme={null} { "id": "pbl_01hrx3t7k2abcdef", "publicId": "INV-00042", "customNumber": "ACME-2025-042", "status": "approved", "paymentStatus": "processing", "totalAmountCents": 325000, "amountPaidCents": 150000, "remainingAmountCents": 175000, "issueDate": "2025-03-01", "dueDate": "2025-03-31", "description": "March consulting retainer", "contact": { "id": "con_01hq9r2m7tuvwxyz", "displayName": "Globex Supplies LLC" }, "items": [ { "name": "Consulting Services", "description": "Strategy consulting — 20 hours", "quantity": 20, "unitAmountCents": 15000, "totalCents": 300000 }, { "name": "Expense Reimbursement", "description": "Travel expenses", "quantity": 1, "unitAmountCents": 25000, "totalCents": 25000 } ] } ``` ## Next steps * [Payments](/concepts/payments) — initiate and track payments against a payable. * [Contacts](/concepts/contacts) — understand the contact associated with each payable. # Payments: Send and Track ACH Transfers in Cleo Pay Source: https://docs.cleo-pay.com/concepts/payments Understand Cleo Pay payment statuses, clearing speeds, scheduling, partial payments, and how payments relate to payables and contacts. Payments in Cleo Pay execute as ACH (Automated Clearing House) transfers — the standard electronic funds transfer network used for US bank-to-bank payments. When you create a payment, Cleo Pay initiates a debit from your verified bank account and a credit to the recipient's account. You can send a payment against a specific payable (invoice) or directly to a contact without referencing an invoice. ## Two payment modes Every payment must reference **exactly one** of the following — but not both: | Mode | Required field | Use case | | -------------------------- | -------------- | ---------------------------------------------------------------------------------------- | | **Pay a payable** | `payableId` | Apply the payment to an approved invoice. Tracks progress against the payable's balance. | | **Pay a contact directly** | `contactId` | Send funds to a vendor without an associated invoice. | You cannot specify both `payableId` and `contactId` in the same request. Choose the mode that matches your workflow. ## Payment status lifecycle | Status | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | The payment has been created and is being prepared for submission. | | `scheduled` | The payment is confirmed and queued for the next ACH processing window. | | `processing` | The ACH transfer has been submitted to the network. Funds are in transit. | | `held` | The payment is waiting because the contact does not have a bank account on file. It will proceed automatically once the contact's bank account is added. | | `completed` | The funds have settled successfully. | | `failed` | The transfer failed (e.g. insufficient funds, invalid account, or bank rejection). | | `cancelled` | The payment was cancelled before processing. | | `refunded` | The payment was processed but the funds have been returned. | The standard happy path is: `pending` → `scheduled` → `processing` → **`completed`** If a contact doesn't have a bank account on file when you send a payment, the payment enters `held` status rather than failing immediately. As soon as the contact's bank account is added, the payment proceeds automatically. ## Clearing speeds The `clearing` field controls how quickly the ACH transfer is processed. | Value | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `STANDARD` | Standard ACH timing. Funds typically settle within 1–3 business days. Available to all accounts. | | `NEXT_AVAILABLE` | Expedited processing using the next available ACH window. **Requires a feature flag** to be enabled on your account — contact Cleo Pay to enable this. | ## Scheduling payments By default, payments are processed as soon as possible. To send a payment on a future date, include the `scheduledAt` field: * **Format:** `YYYY-MM-DD` * **Minimum value:** Tomorrow's date (same-day scheduling is not supported) * **Execution time:** `9:00 AM New York time` on the scheduled date * **Constraint:** Must fall within a valid business window ```json theme={null} { "contactId": "con_01hq9r2m7tuvwxyz", "amountCents": 100000, "bankAccountId": "ba_01hq2k5r8e4vabc123", "clearing": "STANDARD", "scheduledAt": "2025-04-01" } ``` Scheduling payments for a specific date is useful for aligning disbursements with your payroll cycle, invoice due dates, or cash flow planning. ## Partial payments When paying a payable (`payableId`), the `amountCents` field is optional: * **If omitted:** Cleo Pay defaults to the payable's full `remainingAmountCents`, paying the invoice in full. * **If specified:** Only that amount is paid, and the payable's `remainingAmountCents` is reduced accordingly. You can send additional payments later until the balance reaches zero. When paying a contact directly (`contactId`), `amountCents` is required. ## Large payment splitting For very large payment amounts, Cleo Pay may automatically split the transfer into multiple `Payment` objects whose amounts sum to your requested total. This is handled transparently — your single API request may result in an array of payments in the response. Always iterate over the `payments` array in the response rather than assuming a single payment object. ## Example: pay a payable ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "payableId": "pbl_01hrx3t7k2abcdef", "amountCents": 175000, "bankAccountId": "ba_01hq2k5r8e4vabc123", "clearing": "STANDARD" }' ``` ## Example: pay a contact directly ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "contactId": "con_01hq9r2m7tuvwxyz", "amountCents": 50000, "bankAccountId": "ba_01hq2k5r8e4vabc123", "clearing": "STANDARD", "scheduledAt": "2025-04-15" }' ``` ## CreatePaymentResponse The response always includes a `payableId` (if applicable) and a `payments` array. Even when a single payment is created, the result is wrapped in this array. ```json theme={null} { "payableId": "pbl_01hrx3t7k2abcdef", "payments": [ { "id": "pay_01hrx5t9k2mnopqr", "status": "scheduled", "amountCents": 100000, "contactId": "con_01hq9r2m7tuvwxyz", "payableId": "pbl_01hrx3t7k2abcdef", "bankAccountId": "ba_01hq2k5r8e4vabc123", "scheduledAt": "2025-03-18T14:00:00Z", "processedAt": null, "failureReason": null, "createdAt": "2025-03-17T11:05:00Z", "updatedAt": "2025-03-17T11:05:00Z" }, { "id": "pay_01hrx5t9k2mnopqs", "status": "scheduled", "amountCents": 75000, "contactId": "con_01hq9r2m7tuvwxyz", "payableId": "pbl_01hrx3t7k2abcdef", "bankAccountId": "ba_01hq2k5r8e4vabc123", "scheduledAt": "2025-03-18T14:00:00Z", "processedAt": null, "failureReason": null, "createdAt": "2025-03-17T11:05:00Z", "updatedAt": "2025-03-17T11:05:00Z" } ] } ``` In this example, a $1,750.00 payment was split into two transfers of $1,000.00 and \$750.00, both scheduled for processing. ## Next steps * [Payables](/concepts/payables) — understand how payment status maps back to invoice tracking. * [Contacts](/concepts/contacts) — ensure your contact is `paymentReady` before initiating a payment. * [Bank Accounts](/concepts/bank-accounts) — verify your funding account before sending funds. # Link and Verify a Bank Account in Cleo Pay Source: https://docs.cleo-pay.com/guides/link-bank-account Add a business bank account to Cleo Pay via the API using manual entry, then verify it with two micro-deposits before sending payments. Before you can send ACH payments, you need at least one verified bank account linked to your Cleo Pay account. The API uses a **micro-deposit verification** flow: you submit your account details, Cleo Pay sends two small test deposits to the account, and you confirm those amounts to complete verification. `accountNumber` and `routingNumber` are **write-only** fields. After submission, the API never returns the full account number or routing number — only the last 4 digits of the account number are included in responses. Store your account credentials securely before submitting them. *** ## Link and verify a bank account Call `POST /v1/bank-accounts` with your account information. All five fields are required. | Field | Type | Description | | --------------- | ------ | ----------------------------------------------------------------------- | | `holderName` | string | Legal name of the account holder (business or individual). | | `name` | string | A friendly label for this account (e.g., "Main Operating Account"). | | `accountNumber` | string | Full account number. Write-only — not returned after submission. | | `routingNumber` | string | 9-digit ABA routing number. Write-only — not returned after submission. | | `type` | string | Account type: `checking` or `savings`. | ```bash title="Add a bank account" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "holderName": "Acme Corp", "name": "Main Operating Account", "accountNumber": "123456789012", "routingNumber": "021000021", "type": "checking" }' ``` The response shows the new bank account with a status of `pending_verification`. Note that `accountNumber` and `routingNumber` are write-only and are not returned: ```json title="Bank account created" theme={null} { "id": "ba_01hwxyz456", "holderName": "Acme Corp", "name": "Main Operating Account", "accountNumberLast4": "9012", "type": "checking", "status": "pending_verification", "isDefault": false, "createdAt": "2024-06-01T10:00:00Z" } ``` Save the `id` — you'll need it for the verification step and for all payment requests. After the account is created, Cleo Pay initiates two small ACH deposits to your bank account. These typically appear within **1–2 business days**. Check your bank statement for two separate deposits from Cleo Pay, each less than $1.00 (for example, `$0.05`and`\$0.07\`). You'll need the exact amounts for the next step. Once the deposits appear, call `POST /v1/bank-accounts/{id}/verify-micro-deposits` with the two amounts as decimal strings in dollars. You have a **maximum of 3 verification attempts**. If all three attempts fail (amounts don't match), the bank account status permanently becomes `unverified` and the account cannot be used. You will need to add a new bank account. ```bash title="Verify micro-deposits" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts/ba_01hwxyz456/verify-micro-deposits \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "amount1": "0.05", "amount2": "0.07" }' ``` On success, the account status updates to `verified`: ```json title="Bank account verified" theme={null} { "id": "ba_01hwxyz456", "holderName": "Acme Corp", "name": "Main Operating Account", "accountNumberLast4": "9012", "type": "checking", "status": "verified", "isDefault": false, "createdAt": "2024-06-01T10:00:00Z" } ``` With `status: verified`, you can now supply this account's `id` as the `bankAccountId` field in any `POST /v1/payments` request. *** ## Bank account statuses | Status | Meaning | | ---------------------- | ------------------------------------------------------------------ | | `pending_verification` | Account has been added; waiting for micro-deposit confirmation. | | `verified` | Micro-deposits confirmed; account is ready to use. | | `unverified` | All 3 verification attempts were exhausted with incorrect amounts. | | `removed` | Account has been deleted via `DELETE /v1/bank-accounts/{id}`. | *** ## Remove a bank account To remove a bank account, send a `DELETE` request. A successful deletion returns `204 No Content` and sets the account's status to `removed`. ```bash title="Remove a bank account" theme={null} curl --request DELETE \ --url https://api.cleo-pay.com/v1/bank-accounts/ba_01hwxyz456 \ --header 'Authorization: Bearer ' ``` Removing a bank account that is currently the default or is linked to pending payments may affect in-flight transactions. Confirm no payments are in `pending` or `scheduled` status against the account before deleting it. # Manage Vendors and Payees in Cleo Pay Source: https://docs.cleo-pay.com/guides/manage-contacts Create, update, archive, and restore vendor contacts in Cleo Pay. Learn how to set up payees with bank accounts and tax information. Contacts in Cleo Pay represent the vendors, suppliers, and payees you send money to. Each contact stores identity information, bank account details for ACH delivery, and optional tax information for 1099 reporting. Before you can send a payment to a contact, the contact must be **payment-ready**. *** ## Contact types Cleo Pay supports two contact types: | Type | Description | | ---------- | -------------------------------------------------------------------------------------------- | | `payee` | An external vendor or payee you manage directly. You supply their bank account details. | | `business` | Another business that uses the Cleo Pay platform. Their account details are managed by them. | When creating contacts via the API, you will most commonly work with the `payee` type. *** ## Create a contact Submit a `POST /v1/contacts` request with at minimum a `displayName`. Provide as much additional detail as possible — particularly `bankAccount` and `taxInfo` — so the contact is payment-ready right away. | Field | Type | Description | | ----------------- | --------- | ----------------------------------------------- | | `displayName` | string | Human-readable name for the contact (required). | | `emails` | string\[] | One or more email addresses. | | `phone` | string | Contact phone number. | | `address` | object | Mailing address. | | `bankAccount` | object | Bank account details for ACH payments. | | `taxInfo` | object | Tax information for 1099 reporting. | | `allowDuplicates` | boolean | Set to `true` to bypass duplicate detection. | The `address` object supports the following fields: | Field | Type | Description | | ----------- | ------ | -------------------------------------- | | `streetOne` | string | Primary street address line. | | `streetTwo` | string | Secondary address line (optional). | | `city` | string | City. | | `stateCode` | string | Two-letter US state code (e.g., `NY`). | | `zipCode` | string | ZIP code. | The `bankAccount` object accepts `accountNumber` (write-only), `routingNumber` (write-only), and `accountType` (`checking` or `savings`). The `taxInfo` object accepts `legalName`, `taxIdType` (`ein` or `ssn`), and `taxId` (write-only). ```bash title="Create a fully populated contact" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/contacts \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "displayName": "Acme Supplies LLC", "emails": ["ap@acmesupplies.com", "billing@acmesupplies.com"], "phone": "+15555550100", "address": { "streetOne": "123 Main St", "streetTwo": "Suite 400", "city": "New York", "stateCode": "NY", "zipCode": "10001" }, "bankAccount": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxId": "123456789" } }' ``` By default, Cleo Pay **blocks duplicate contacts** — if a contact with the same name or email already exists, the request returns a `409 Conflict`. To intentionally create a duplicate, include `"allowDuplicates": true` in your request body. A successful response returns the full `Contact` object: ```json title="Contact created" theme={null} { "id": "contact_01hwxyz111", "displayName": "Acme Supplies LLC", "type": "payee", "status": "active", "paymentReady": true, "emails": ["ap@acmesupplies.com", "billing@acmesupplies.com"], "phone": "+15555550100", "address": { "streetOne": "123 Main St", "streetTwo": "Suite 400", "city": "New York", "stateCode": "NY", "zipCode": "10001" }, "taxInfo": { "legalName": "Acme Supplies LLC", "taxIdType": "ein", "taxId": "**-***6789" }, "netTerms": null, "createdAt": "2024-06-01T10:00:00Z", "updatedAt": "2024-06-01T10:00:00Z" } ``` After creation, verify that `paymentReady: true` in the response. A contact is payment-ready when it has both a bank account and tax information on file. If `paymentReady` is `false`, any payment to this contact will be created in `held` status and will not process until the missing information is added. Use `PATCH /v1/contacts/{id}` to update mutable fields. Only the fields you include in the request body are changed — all other fields remain unchanged. The following fields are updatable for all contact types: | Field | Type | Notes | | ------------- | ------ | ------------------- | | `displayName` | string | | | `netTerms` | number | Minimum value: `0`. | The following fields are updatable for `payee` contacts only: | Field | Type | Notes | | --------- | --------- | -------------------------------------- | | `emails` | string\[] | Replaces all existing email addresses. | | `phone` | string | | | `address` | object | | ```bash title="Update a contact's net terms" theme={null} curl --request PATCH \ --url https://api.cleo-pay.com/v1/contacts/contact_01hwxyz111 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "netTerms": 30 }' ``` *** ## Archive and unarchive contacts Archiving removes a contact from default list queries without permanently deleting them. This is useful for vendors you no longer work with but want to retain for historical records. **Archive a contact:** ```bash title="Archive a contact" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/contacts/contact_01hwxyz111/archive \ --header 'Authorization: Bearer ' ``` **Unarchive a contact:** ```bash title="Unarchive a contact" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/contacts/contact_01hwxyz111/unarchive \ --header 'Authorization: Bearer ' ``` Both endpoints require no request body and return the updated `Contact` object with the new `status` value. | Status | Description | | ---------- | ---------------------------------------------------------------------------- | | `active` | Contact is active and appears in default list queries. | | `archived` | Contact is hidden from default list queries. Can be unarchived. | | `revoked` | Contact has been revoked by the Cleo Pay platform. **Cannot be unarchived.** | A contact with `status: revoked` has been suspended at the platform level. You cannot unarchive or send payments to a revoked contact. Reach out to Cleo Pay support for more information. *** ## Filter the contacts list When listing contacts with `GET /v1/contacts`, use query parameters to narrow your results. By default, the endpoint returns both `active` and `archived` contacts; revoked contacts are excluded unless explicitly requested. | Parameter | Values | Description | | --------- | -------------------- | ----------------------------------------------------------------------------- | | `type` | `payee`, `business` | Filter by contact type. | | `status` | `active`, `archived` | Filter by contact status. Defaults to returning active and archived contacts. | | `search` | string | Free-text search across name and email fields. | ```bash title="List active payee contacts matching 'acme'" theme={null} curl --request GET \ --url 'https://api.cleo-pay.com/v1/contacts?type=payee&status=active&search=acme' \ --header 'Authorization: Bearer ' ``` See the [Pagination reference](/reference/pagination) for details on `page` and `pageSize` parameters. # Connect AI Agents to Cleo Pay with the MCP Server Source: https://docs.cleo-pay.com/guides/mcp-server Connect Claude Code, Cursor, VS Code, or any MCP client to the Cleo Pay MCP server. Authenticate with an API key and manage payables, payments, contacts, and bank accounts. Cleo Pay runs a hosted **Model Context Protocol (MCP) server** that lets AI agents operate your Cleo Pay account with the same capabilities as the REST API. Connect Claude Code, Cursor, VS Code, or any MCP client that supports Streamable HTTP, and your agent can list and create payables, send ACH payments, manage vendor contacts, and link bank accounts — limited to exactly the scopes you grant its API key. The server is remote and stateless: there is nothing to install or host. Point your client at the endpoint, pass your API key in the `Authorization` header, and it's ready. New to Cleo Pay? Read the [Quickstart](/quickstart) first — the MCP tools operate on the same payables, payments, contacts, and bank accounts as the REST API. *** ## Environments | Environment | MCP endpoint | API key prefix | Money movement | | ----------- | ------------------------------ | -------------- | ------------------------------- | | **Sandbox** | `https://mcp.stg.cleo-pay.com` | `cleo_test_` | Simulated — no real money moves | | **Live** | `https://mcp.cleo-pay.com` | `cleo_live_` | Real ACH transfers | Both environments also accept an explicit path form: `https://api.stg.cleo-pay.com/v1/mcp` (Sandbox) and `https://api.cleo-pay.com/v1/mcp` (Live). API keys are **environment-bound**: a `cleo_test_` key only authenticates against Sandbox, and a `cleo_live_` key only against Live. Sandbox is a fully separate environment with its own data — nothing you create there appears in Live, and payments run against a simulated banking network. Start in Sandbox. Wire up your agent with a `cleo_test_` key, rehearse your flows end to end, then switch the endpoint and key to go live. *** ## Get an API key In the Cleo Pay dashboard, go to **Settings → Developers** and click **Create API key**. You need admin access on the business. Give the key a name, select the scopes it should have (see [Scopes and tool visibility](#scopes-and-tool-visibility) below), and optionally set an expiration date. The full key (for example `cleo_test_a1b2c3d4e5f6g7h8_...`) is shown **once**, at creation time. Store it in a secrets manager or environment variable — it cannot be retrieved again. If you lose it, rotate the key to mint a new secret. Treat API keys like passwords. Anyone holding the key can act on your business within its scopes. Never commit keys to version control, and rotate immediately if you suspect a leak — rotation atomically mints a new secret and revokes the old one. Your business must have completed identity verification before API keys will authenticate. Until verification is finished, requests are rejected with `401 Unauthorized`. *** ## Connect your client The examples below use the Sandbox endpoint and a `cleo_test_` key. For Live, swap in `https://mcp.cleo-pay.com` and a `cleo_live_` key. ```bash theme={null} claude mcp add --transport http cleo https://mcp.stg.cleo-pay.com \ --header "Authorization: Bearer cleo_test_YOUR_KEY" ``` Then ask Claude something like *"list my Cleo payables"* — it discovers and calls the tools automatically. Add to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "cleo": { "url": "https://mcp.stg.cleo-pay.com", "headers": { "Authorization": "Bearer cleo_test_YOUR_KEY" } } } } ``` Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "cleo": { "type": "http", "url": "https://mcp.stg.cleo-pay.com", "headers": { "Authorization": "Bearer cleo_test_YOUR_KEY" } } } } ``` The server speaks standard JSON-RPC over Streamable HTTP. List the tools available to your key: ```bash theme={null} curl -s -X POST https://mcp.stg.cleo-pay.com \ -H "Authorization: Bearer cleo_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` MCP client config files contain your API key in plain text. Keep them out of version control — in particular, add `.vscode/mcp.json` to `.gitignore` if you put a key in it, or use your client's secret-input mechanism. Any MCP client that supports **Streamable HTTP with custom headers** works. Clients that only support OAuth-based remote connectors (such as Claude Desktop's one-click connectors) are not yet supported. *** ## Scopes and tool visibility Each tool requires a scope. The server only advertises the tools your key is allowed to call — `tools/list` omits everything else, so your agent never sees a tool it can't use. Calling an out-of-scope tool directly returns a JSON-RPC *Access denied* error. | Scope | Tools unlocked | | --------------------- | -------------------------------------------------------------------------- | | `payables:read` | `list_payables`, `get_payable` | | `payables:write` | `create_payable` | | `payments:initiate` | `create_payment` | | `contacts:read` | `list_contacts`, `get_contact` | | `contacts:write` | `create_contact`, `update_contact`, `archive_contact`, `unarchive_contact` | | `bank-accounts:read` | `list_bank_accounts`, `get_bank_account` | | `bank-accounts:write` | `add_bank_account`, `verify_micro_deposits`, `remove_bank_account` | Grant the minimum scopes your integration needs. A read-only agent (reporting, reconciliation) needs only the `:read` scopes; keep `payments:initiate` on a separate, tightly-held key. Requests are rate-limited to **100 requests per minute per API key**. Exceeding the limit returns HTTP `429`. *** ## Tools All tools return JSON. Amounts are integers in **cents**; dates use `YYYY-MM-DD`. ### Payables | Tool | Scope | Description | | ---------------- | ---------------- | --------------------------------------------------------------------------- | | `list_payables` | `payables:read` | List payables (bills) with pagination, status filter, and free-text search. | | `get_payable` | `payables:read` | Fetch a single payable, including line items. | | `create_payable` | `payables:write` | Create a **draft** payable. Always a draft — never auto-sent or auto-paid. | | Parameter | Type | Required | Description | | ---------- | ------------- | -------- | ------------------------------------------------------------------------------ | | `page` | integer ≥ 1 | No | Page number. Default `1`. | | `pageSize` | integer 1–100 | No | Items per page. Default `25`. | | `status` | enum | No | One of `draft`, `pending_approval`, `approved`, `declined`, `blocked`, `void`. | | `search` | string | No | Free-text search. | Returns `items` (without line items) and `totalCount`. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------- | | `payableId` | string | Yes | The payable's ID. | Returns the payable with its full line items. | Parameter | Type | Required | Description | | ------------------ | ------------ | -------- | ------------------------------------------------------------------------------- | | `contact.name` | string | Yes | Vendor name shown on the draft (free text — not linked to a contact record). | | `contact.email` | string | No | Vendor email. | | `totalAmountCents` | integer ≥ 1 | Yes | Total amount in cents. | | `dueDate` | `YYYY-MM-DD` | No | Due date. | | `issueDate` | `YYYY-MM-DD` | No | Issue date. | | `customNumber` | string | No | Your invoice/bill number. | | `description` | string | No | Memo. | | `items` | array | No | Line items: `name`, `description`, `quantity` (default `1`), `unitAmountCents`. | **Not idempotent** — a retry creates a second draft. See [Idempotency and retries](#idempotency-and-retries). ### Payments | Tool | Scope | Description | | ---------------- | ------------------- | --------------------------------------------------------------------------------- | | `create_payment` | `payments:initiate` | Initiate an ACH payment — pay an existing payable, or send directly to a contact. | In the Live environment, `create_payment` **moves real money**. Scope keys carefully and rehearse in Sandbox first. | Parameter | Type | Required | Description | | ---------------- | ------------------ | ----------- | --------------------------------------------------------------------------------------------- | | `idempotencyKey` | string, 1–64 chars | Yes | Unique key for this payment attempt. Retrying with the same key is safe and never pays twice. | | `payableId` | UUID | One of | Pay an existing payable. Provide exactly one of `payableId` or `contactId`. | | `contactId` | UUID | One of | Send money directly to a contact. | | `amountCents` | integer ≥ 1 | Conditional | Required with `contactId`. Optional with `payableId` to make a partial payment. | | `bankAccountId` | UUID | Yes | Verified bank account that funds the payment. | | `clearing` | enum | No | `STANDARD` or `NEXT_AVAILABLE`. | | `scheduledAt` | `YYYY-MM-DD` | No | Schedule for a future date — executes at 9:00 AM New York time. | | `description` | string | No | Payment memo. | | `customNumber` | string ≤ 21 chars | No | Custom payment reference. | | `categoryId` | UUID | No | Accounting category (direct-to-contact payments only). | Returns the affected `payableId` and a `payments` array — large amounts may split into multiple ACH transfers. Payment statuses: `pending`, `scheduled`, `processing`, `held`, `completed`, `failed`, `cancelled`, `refunded`. Payments to a payee without a linked bank account are **held** until the payee provides their details; scheduling is not supported for held payments. ### Bank accounts | Tool | Scope | Description | | ----------------------- | --------------------- | --------------------------------------------------------------------------------------------- | | `list_bank_accounts` | `bank-accounts:read` | List linked bank accounts (masked to last 4 digits). | | `get_bank_account` | `bank-accounts:read` | Fetch one bank account, including removed ones. | | `add_bank_account` | `bank-accounts:write` | Link an account by account + routing number. Micro-deposit verification starts automatically. | | `verify_micro_deposits` | `bank-accounts:write` | Confirm the two micro-deposit amounts to finish verification. | | `remove_bank_account` | `bank-accounts:write` | Unlink a bank account. | | Parameter | Type | Required | Description | | --------------- | ------------------- | -------- | -------------------------------------------------------- | | `holderName` | string | Yes | Legal name of the account holder. | | `name` | string | Yes | Friendly label (alphanumeric, commas, periods, hyphens). | | `accountNumber` | string, digits only | Yes | Full account number. **Write-only** — never echoed back. | | `routingNumber` | string, 9 digits | Yes | ABA routing number. **Write-only**. | | `type` | enum | Yes | `checking` or `savings`. | The account starts in `pending_verification`; it becomes usable after `verify_micro_deposits` succeeds. **Not idempotent** — do not auto-retry on timeout. | Parameter | Type | Required | Description | | --------------- | --------------------- | -------- | --------------------------------------- | | `bankAccountId` | string | Yes | The pending bank account's ID. | | `amount1` | string, e.g. `"0.05"` | Yes | First micro-deposit amount in dollars. | | `amount2` | string, e.g. `"0.07"` | Yes | Second micro-deposit amount in dollars. | Maximum **3 attempts** — a wrong pair consumes one. After 3 failures the account becomes permanently `unverified`. `list_bank_accounts` takes no parameters and returns all linked accounts. `get_bank_account` and `remove_bank_account` take a single required `bankAccountId` (string). The business's **default verified account cannot be removed** — set another account as default in the Cleo Pay dashboard first. Account statuses: `pending_verification`, `verified`, `unverified`, `removed`. ### Contacts | Tool | Scope | Description | | ------------------- | ---------------- | --------------------------------------------------------------------------------- | | `list_contacts` | `contacts:read` | List contacts with pagination, type/status filters, and search. | | `get_contact` | `contacts:read` | Fetch one contact, including bank-account and tax-info summaries. | | `create_contact` | `contacts:write` | Create a vendor/payee, optionally with a receiving bank account and W-9 tax info. | | `update_contact` | `contacts:write` | Update a contact's editable fields. | | `archive_contact` | `contacts:write` | Archive a contact (idempotent). | | `unarchive_contact` | `contacts:write` | Restore an archived contact (idempotent). | | Parameter | Type | Required | Description | | ---------- | ------------- | -------- | ------------------------------------------------------------------------ | | `page` | integer ≥ 1 | No | Page number. Default `1`. | | `pageSize` | integer 1–100 | No | Items per page. Default `25`. | | `type` | enum | No | `business` or `payee`. | | `status` | enum | No | `active`, `archived`, or `revoked`. Default returns active and archived. | | `search` | string | No | Free-text search. | | Parameter | Type | Required | Description | | ----------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `displayName` | string | Yes | Contact display name. | | `emails` | array of emails, max 5 | No | Contact emails. | | `phone` | string | No | Phone number. | | `address` | object | No | `streetOne`, `streetTwo`, `city`, `stateCode` (2 letters), `zipCode`. | | `bankAccount` | object | No | Receiving account: `accountNumber` (digits, **write-only**), `routingNumber` (9 digits, **write-only**), `accountType` (`checking` or `savings`). | | `taxInfo` | object | No | W-9 info: `legalName`, `taxIdType` (`ein` or `ssn`), `taxId` (**write-only** — only the last 4 are ever returned). | | `allowDuplicates` | boolean | No | Default `false`. A likely duplicate (matching email or name) is rejected unless set to `true`. | **Not idempotent** — do not auto-retry on timeout. All three take a required `contactId` (string). `update_contact` additionally accepts: `displayName`, `emails` (replaces **all** emails, max 5), `phone`, `address` — these apply to **payee-type contacts only** — and `netTerms` (integer ≥ 0), which applies to any contact. `archive_contact` and `unarchive_contact` are idempotent — safe to call on an already-archived or already-active contact. Revoked contacts cannot be unarchived. *** ## Idempotency and retries Agents retry things. Here is what is safe: | Tools | Retry behavior | | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `create_payment` | **Safe** — the required `idempotencyKey` guarantees the same key never pays twice. Reuse the same key when retrying a timed-out call. | | `create_payable`, `add_bank_account`, `create_contact` | **Not safe** — a retry creates a second record. On timeout, check whether the record exists (e.g. `list_payables`) before trying again. | | `archive_contact`, `unarchive_contact` | **Safe** — idempotent by design. | | All `list_*` / `get_*` tools | **Safe** — read-only. | *** ## Errors | Scenario | What you get | | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Missing `Authorization` header | HTTP `401` — `Missing API key` | | Invalid, expired, revoked, or wrong-environment key | HTTP `401` — `Invalid API key` | | Rate limit exceeded | HTTP `429` (100 requests/minute per key) | | MCP server temporarily disabled | HTTP `503` — `Partner API disabled` | | Tool call without the required scope | JSON-RPC error `-32600` — `Access denied` | | Unknown tool name | JSON-RPC error `-32601` | | Invalid parameters or business error (validation, not found, duplicate contact) | Successful JSON-RPC response with `isError: true` and a message describing the problem | Remember that tools outside your key's scopes don't appear in `tools/list` at all — if a tool seems missing, check the key's scopes. *** ## Data protection * Bank account and routing numbers are **write-only**: responses only ever include the last 4 digits. * Tax IDs are **write-only**: responses include a mask (for example `***6789`). * Responses are explicit allowlists — internal fields are never serialized. * Every write is recorded in an append-only action log attributed to your API key, with request ID and IP address. *** ## Next steps * Rehearse a full flow in Sandbox: [link a bank account](/guides/link-bank-account), [create a contact](/guides/manage-contacts), then [send a payment](/guides/send-a-payment). * Subscribe to [webhooks](/reference/webhooks) to track payment and verification events your agent kicks off. * Prefer raw REST? The same capabilities are in the [API Reference](/api-reference/payables/list). # Schedule a Future Payment with Cleo Pay Source: https://docs.cleo-pay.com/guides/schedule-a-payment Schedule ACH payments for a future date using the Cleo Pay API. Payments execute at 9:00 AM New York time on the chosen date. Scheduling a payment lets you specify the exact date you want the ACH transfer to initiate. Instead of processing immediately, the payment sits in a `scheduled` state and automatically submits to the ACH network on the morning of the chosen date. Scheduling uses the same `POST /v1/payments` endpoint as immediate payments — you simply add the `scheduledAt` field to your request body. *** ## How scheduling works Add `scheduledAt` (formatted as `YYYY-MM-DD`) to any `POST /v1/payments` request: * The date must be **at least tomorrow** — you cannot schedule a payment for today or a past date. * On the scheduled date, the payment initiates at **9:00 AM New York time** (Eastern Time). * The payment status is `scheduled` from creation until the execution date, at which point it transitions to `pending` and then follows the normal processing flow. Cleo Pay always uses **New York time (ET/EST)** for scheduled payment execution. If your servers are in a different timezone, make sure you account for the offset when choosing a date — a payment scheduled for `2024-06-15` fires at 9:00 AM Eastern on June 15th regardless of where your application runs. *** ## Schedule a payment ```bash title="Schedule a payment for a future date" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "payableId": "payable_01hwxyz123", "bankAccountId": "ba_01hwxyz456", "scheduledAt": "2024-06-15" }' ``` The response includes a `scheduledAt` datetime (in ISO 8601 format) showing when the payment will execute: ```json title="Scheduled payment response" theme={null} { "payableId": "payable_01hwxyz123", "payments": [ { "id": "pmt_01hwxyz789", "status": "scheduled", "amountCents": 150000, "bankAccountId": "ba_01hwxyz456", "scheduledAt": "2024-06-15T13:00:00Z", "createdAt": "2024-06-01T10:00:00Z" } ] } ``` The `scheduledAt` timestamp in the response is stored in UTC. `2024-06-15T13:00:00Z` corresponds to 9:00 AM Eastern Daylight Time (EDT, UTC−4) on June 15, 2024. *** ## Clearing speed The optional `clearing` field controls how quickly the ACH transfer settles after it is submitted: | Value | Behavior | | ---------------- | --------------------------------------------------------- | | `STANDARD` | Standard ACH settlement timeline (default). | | `NEXT_AVAILABLE` | Uses the next available ACH window for faster settlement. | ```bash title="Schedule with NEXT_AVAILABLE clearing" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "contactId": "contact_01hwxyz111", "amountCents": 75000, "bankAccountId": "ba_01hwxyz456", "scheduledAt": "2024-06-20", "clearing": "NEXT_AVAILABLE" }' ``` `NEXT_AVAILABLE` clearing must be **enabled on your account** before you can use it. If your account does not have this feature enabled, the request will return an error. Contact your Cleo Pay account manager to enable faster clearing. *** ## Limitations * **Held payments cannot be scheduled.** Scheduling is not supported for contacts who have no bank account on file. If a contact's `paymentReady` flag is `false`, creating a scheduled payment will result in an error. Ensure the contact has a verified bank account before scheduling. * **Past dates are rejected.** Supplying a `scheduledAt` date in the past or equal to today's date returns a `422 Unprocessable Entity`. * **Cancellation window.** A `scheduled` payment can be cancelled before its execution date. Once the payment transitions to `pending` on the morning of the scheduled date, cancellation may no longer be possible. *** ## What happens if a scheduled payment fails? If a scheduled payment fails after it is submitted to the ACH network (for example, due to insufficient funds or an invalid account), the payment status transitions to `failed`. Cleo Pay will notify you via webhook. You will need to investigate the failure reason and, if appropriate, create a new payment. Common causes of failure after scheduling: * The source bank account was closed or lost verification between the time of scheduling and execution. * Insufficient funds in the source account at execution time. * The receiving contact's bank account details changed or became invalid. Subscribe to payment webhook events to receive real-time notifications when a scheduled payment fails. See the [Webhooks reference](/reference/webhooks) for details. # Send an ACH Payment with Cleo Pay Source: https://docs.cleo-pay.com/guides/send-a-payment Learn how to send ACH payments via the Cleo Pay API — pay an existing invoice or send directly to a contact with one API call. The Cleo Pay API gives you two ways to send an ACH payment: you can pay against an existing payable (invoice), or send funds directly to a contact without referencing an invoice. Both paths use the same `POST /v1/payments` endpoint — the only difference is which identifier you supply. `payableId` and `contactId` are **mutually exclusive**. Include one or the other in a single request — never both. Sending both fields returns a `400 Bad Request`. *** ## Path A — Pay an existing payable (invoice) Use this path when you have an outstanding invoice or payable tracked in Cleo Pay and want to apply a payment against it. Retrieve the payable you want to pay. You can find its ID either from the response when the payable was created (`POST /v1/payables`) or by listing your payables (`GET /v1/payables`). The `id` field on the payable object is the value you'll pass as `payableId`. Call `POST /v1/payments` with the `payableId` and the `bankAccountId` of the account you want to fund the payment from. ```bash title="Pay a payable in full" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "payableId": "payable_01hwxyz123", "bankAccountId": "ba_01hwxyz456" }' ``` By default, the payment amount equals the **remaining balance** on the payable. To pay only part of it, include `amountCents` in your request body. The minimum value is `1` (one cent). ```bash title="Partial payment of $250.00" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "payableId": "payable_01hwxyz123", "bankAccountId": "ba_01hwxyz456", "amountCents": 25000 }' ``` The response includes the `payableId` and a `payments` array. Each payment object has a `status` field. Immediately after creation the status will be `pending` (or `scheduled` if you supplied `scheduledAt`). ```json title="Payment response" theme={null} { "payableId": "payable_01hwxyz123", "payments": [ { "id": "pmt_01hwxyz789", "status": "pending", "amountCents": 25000, "bankAccountId": "ba_01hwxyz456", "createdAt": "2024-06-01T14:23:00Z" } ] } ``` *** ## Path B — Send directly to a contact Use this path when you want to push funds to a vendor or payee without attaching the payment to a specific invoice. Before sending, ensure the contact's `paymentReady` flag is `true`. A contact becomes payment-ready once it has a verified bank account on file. If `paymentReady` is `false`, the payment will be created with a **`held`** status and will not process until the contact adds a bank account. A payment in `held` status means the contact has no bank account yet. The payment will release automatically once the contact links and verifies their account. Call `POST /v1/payments` with `contactId`, `amountCents`, and `bankAccountId`. You can optionally add a human-readable `description` and your own reference number via `customNumber` (maximum 21 characters). ```bash title="Send $500.00 directly to a contact" theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "contactId": "contact_01hwxyz111", "amountCents": 50000, "bankAccountId": "ba_01hwxyz456", "description": "Consulting services — May 2024", "customNumber": "INV-2024-0045" }' ``` The response mirrors Path A — a `payments` array with the new payment object. When no `payableId` is involved, the `payableId` field in the response will be `null`. ```json title="Direct contact payment response" theme={null} { "payableId": null, "payments": [ { "id": "pmt_01hwxyz999", "status": "pending", "amountCents": 50000, "bankAccountId": "ba_01hwxyz456", "description": "Consulting services — May 2024", "customNumber": "INV-2024-0045", "createdAt": "2024-06-01T14:30:00Z" } ] } ``` *** ## Payment status flow After you create a payment, it moves through the following statuses: | Status | Meaning | | ------------ | ----------------------------------------------------------------------------------- | | `pending` | Payment has been created and is queued for processing. | | `scheduled` | Payment is set for a future date and has not yet been submitted to the ACH network. | | `processing` | Payment has been submitted to the ACH network. | | `held` | Payment is waiting for the contact to link a bank account. | | `completed` | Funds have been successfully delivered. | | `failed` | The payment was rejected or returned. Check the failure reason. | | `cancelled` | The payment was cancelled before processing. | | `refunded` | A completed payment was reversed. | The typical happy-path progression is: ``` pending → processing → completed ``` For scheduled payments the flow starts with `scheduled` before entering `pending` on the execution date: ``` scheduled → pending → processing → completed ``` # Cleo Pay API Overview: Automate B2B Payments End to End Source: https://docs.cleo-pay.com/introduction Learn how Cleo Pay's REST API works: manage payables, contacts, and bank accounts to automate your B2B payment operations end to end. Cleo Pay is a REST API that lets you automate business-to-business payments end to end. Connect your bank account, manage vendors as contacts, track invoices as payables, and send ACH payments — all programmatically, without leaving your own systems. Whether you're building an accounts-payable workflow, a vendor payout engine, or a finance automation tool, Cleo Pay gives you the primitives to do it. ## Key features Model invoices and bills as payables with a full document lifecycle (draft → approved) and independent payment tracking (none → settled). Attach line items, due dates, and contact associations. Represent vendors and payees as contacts. Control payment direction, store tax information, and track whether a contact is ready to receive funds. Link your business bank accounts manually via the API or through Plaid in the dashboard. Verify ownership with micro-deposits before sending any funds. Send ACH payments to a contact directly or against a payable. Choose clearing speed, schedule future payments, and track status from pending through completed. ## How it works Getting from zero to a completed payment takes three steps: Add your business checking or savings account via `POST /v1/bank-accounts`. Complete micro-deposit verification to unlock outbound payments. Add the vendor or payee you want to pay via `POST /v1/contacts`. Supply their bank account details so funds have somewhere to land. Initiate an ACH transfer via `POST /v1/payments`. Reference a payable to pay an invoice, or send directly to a contact. Track progress through the payment status lifecycle. ## API base URL All API requests are made against the staging base URL: ``` https://api.cleo-pay.com ``` Prefix every endpoint path with this URL, for example: `https://api.cleo-pay.com/v1/payments`. ## Data conventions | Convention | Detail | | -------------- | ----------------------------------------------------------------------------------- | | **Amounts** | All monetary amounts are integers expressed in **cents** (e.g. `10050` = \$100.50). | | **Dates** | All date fields use **YYYY-MM-DD** format (e.g. `"2025-03-15"`). | | **IDs** | All resources are identified by a UUID `id` field returned in every response. | | **Timestamps** | Full timestamps (e.g. `createdAt`) are ISO 8601 with UTC timezone. | ## Next steps * [Authenticate your requests](/authentication) — add your Bearer token to every call. * [Quickstart](/quickstart) — link a bank account, create a contact, and send your first payment in five steps. # Cleo Pay Quickstart: Send Your First ACH Payment via API Source: https://docs.cleo-pay.com/quickstart Follow five steps to send your first ACH payment with Cleo Pay: verify a bank account, add a vendor contact, and call POST /v1/payments. This guide walks you through the end-to-end flow from a fresh API token to a completed ACH payment. You'll link your business bank account, verify it with micro-deposits, create a vendor contact, and send your first payment — all via the REST API. Each step builds on the previous one, so work through them in order. Obtain a JWT from Cleo Pay and set it as an environment variable so you can reuse it across all the following commands. ```bash theme={null} export CLEO_TOKEN="" ``` Every request must include this token in the `Authorization` header: ``` Authorization: Bearer $CLEO_TOKEN ``` Keep your token in an environment variable or a secrets manager. Never hard-code it in your application source code or commit it to version control. Verify your token works by fetching your bank accounts (an empty list is a valid, successful response): ```bash theme={null} curl --request GET \ --url https://api.cleo-pay.com/v1/bank-accounts \ --header "Authorization: Bearer $CLEO_TOKEN" ``` Add your business checking or savings account. You'll need the full account number and routing number — these are accepted on write but never returned in responses (only the last 4 digits are returned). ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "holderName": "Acme Corp", "name": "Acme Main Checking", "accountNumber": "123456789012", "routingNumber": "021000021", "type": "checking" }' ``` **Response:** ```json theme={null} { "id": "ba_01hq2k5r8e4vabc123", "status": "pending_verification", "type": "checking", "connectionType": "manual", "name": "Acme Main Checking", "institutionName": null, "accountNumberLast4": "9012", "routingNumberLast4": "0021", "isDefault": false, "microDepositsInitiatedAt": "2025-03-15T14:00:00Z", "microDepositsCompletedAt": null, "verificationAttempts": 0 } ``` Save the `id` from the response — you'll need it in the next step. ```bash theme={null} export BANK_ACCOUNT_ID="ba_01hq2k5r8e4vabc123" ``` Two small micro-deposits (each under \$0.10) will be sent to the account within 1–2 business days. Check the account statement to find the exact amounts before proceeding to the next step. Once the two micro-deposits appear in your bank statement, submit both amounts to verify ownership. Amounts are provided as decimal dollar strings (e.g. `"0.05"` for five cents). ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/bank-accounts/$BANK_ACCOUNT_ID/verify-micro-deposits \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "amount1": "0.05", "amount2": "0.08" }' ``` **Response:** ```json theme={null} { "id": "ba_01hq2k5r8e4vabc123", "status": "verified", "type": "checking", "connectionType": "manual", "name": "Acme Main Checking", "accountNumberLast4": "9012", "routingNumberLast4": "0021", "isDefault": false, "microDepositsInitiatedAt": "2025-03-15T14:00:00Z", "microDepositsCompletedAt": "2025-03-17T10:22:00Z", "verificationAttempts": 1 } ``` When `status` changes to `"verified"`, the account is ready to fund payments. You have a maximum of **3 verification attempts**. If you enter incorrect amounts 3 times, the account will move to `unverified` and you will need to link a new account. Create the vendor or payee you want to pay. Supply their bank account details so Cleo Pay knows where to send funds. ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/contacts \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "displayName": "Globex Supplies LLC", "type": "payee", "emails": ["billing@globex-supplies.com"], "bankAccount": { "accountNumber": "987600001234", "routingNumber": "021000021", "type": "checking" } }' ``` **Response:** ```json theme={null} { "id": "con_01hq9r2m7tuvwxyz", "type": "payee", "status": "active", "displayName": "Globex Supplies LLC", "emails": ["billing@globex-supplies.com"], "phone": null, "paymentDirection": "can_be_paid", "paymentReady": true, "netTerms": null, "bankAccount": { "accountNumberLast4": "1234", "routingNumberLast4": "0021" }, "taxInfo": null, "createdAt": "2025-03-17T11:00:00Z", "updatedAt": "2025-03-17T11:00:00Z", "archivedAt": null } ``` Save the contact `id`: ```bash theme={null} export CONTACT_ID="con_01hq9r2m7tuvwxyz" ``` Check that `paymentReady` is `true` before sending a payment. A `false` value means the contact is missing required details (such as a bank account) and the payment will enter a `held` status. Send an ACH payment directly to your contact. Specify the amount in cents and reference your verified bank account as the funding source. ```bash theme={null} curl --request POST \ --url https://api.cleo-pay.com/v1/payments \ --header "Authorization: Bearer $CLEO_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "contactId": "'$CONTACT_ID'", "amountCents": 250000, "bankAccountId": "'$BANK_ACCOUNT_ID'", "clearing": "STANDARD" }' ``` **Response:** ```json theme={null} { "payments": [ { "id": "pay_01hrx5t9k2mnopqr", "status": "scheduled", "amountCents": 250000, "contactId": "con_01hq9r2m7tuvwxyz", "bankAccountId": "ba_01hq2k5r8e4vabc123", "clearing": "STANDARD", "scheduledAt": null, "createdAt": "2025-03-17T11:05:00Z" } ] } ``` Your payment is now `scheduled` and will move to `processing` once the ACH window opens, and finally to `completed` when funds settle. To pay a specific invoice instead of sending to a contact directly, replace `contactId` and `amountCents` with `payableId`. The payment will be applied against the payable's remaining balance automatically. ## What's next? * **[Payables](/concepts/payables)** — model invoices and track payment progress against them. * **[Contacts](/concepts/contacts)** — manage vendor details, payment direction, and tax info. * **[Bank Accounts](/concepts/bank-accounts)** — understand the full verification lifecycle. * **[Payments](/concepts/payments)** — explore clearing speeds, scheduling, and partial payments. # Cleo Pay API Error Codes and Troubleshooting Source: https://docs.cleo-pay.com/reference/errors Reference for Cleo Pay API HTTP error codes. Understand what each status code means and how to handle errors in your integration. The Cleo Pay API uses standard HTTP status codes to communicate success or failure. All error responses include a JSON body with a machine-readable `error` code and a human-readable `message` to help you diagnose the issue. A typical error response looks like this: ```json title="Error response shape" theme={null} { "error": "INVALID_REQUEST", "message": "amountCents must be at least 1" } ``` *** ## HTTP status code reference **What it means:** The request body is malformed, contains invalid field types, is missing a required field, or contains a logically invalid combination of fields. **Common causes:** * Missing a required field (e.g., omitting `bankAccountId` on a payment request). * Sending both `contactId` and `payableId` in the same `POST /v1/payments` request — these fields are mutually exclusive. * Sending malformed JSON (e.g., unquoted string values, trailing commas). **How to fix:** Read the `message` field in the error response — it will identify the specific field or conflict. Double-check your request body against the [API spec](/reference). **What it means:** The request did not include a valid Bearer token, or the token is expired or malformed. **Common causes:** * Forgetting to include the `Authorization` header entirely. * Passing the token without the `Bearer ` prefix (e.g., sending only the raw token string). * Using an expired token. **How to fix:** Ensure every request includes the header: ```bash theme={null} Authorization: Bearer ``` If your token has expired, obtain a new one from your authentication provider and retry. **What it means:** Your token is valid and authenticated, but your account or role does not have permission to perform the requested action. **Common causes:** * Attempting to access a resource that belongs to a different organization. * Trying to use a feature (e.g., `NEXT_AVAILABLE` clearing) that has not been enabled for your account. **How to fix:** Verify that the resource ID in the request belongs to your organization. If you believe you should have access, contact your Cleo Pay account manager. **What it means:** The resource you referenced does not exist, or your account does not have access to it. Cleo Pay returns 404 (rather than 403) for resources you cannot access, to avoid leaking information about resource existence. **Common causes:** * Typo in a resource ID (e.g., `payable_01hwxyz123` vs. `payable_01hwxyz124`). * Referencing a resource that was deleted or belongs to another organization. **How to fix:** Double-check the resource ID. List the relevant resources (e.g., `GET /v1/contacts`) to confirm the ID is correct and accessible. **What it means:** The request conflicts with the current state of a resource. The most common cause is duplicate detection. **Common causes:** * Creating a contact that shares a name or email with an existing contact, without setting `allowDuplicates: true`. **How to fix:** If the duplicate is intentional, resend the request with `"allowDuplicates": true` in the request body. Otherwise, retrieve the existing contact and update it instead of creating a new one. **What it means:** The request is well-formed and authenticated, but the data is semantically invalid — it fails a business rule. **Common causes:** * `amountCents` is less than `1` (minimum payment is 1 cent). * `scheduledAt` is today's date or in the past. * `totalAmountCents` on a payable is less than `1`. * A field value is outside its allowed range or format. **How to fix:** Read the `message` field carefully. It will describe the specific rule that was violated (e.g., `"scheduledAt must be a future date"`). **What it means:** You have exceeded the API rate limit. **Common causes:** * Sending too many requests in a short window (e.g., unbounded parallel requests without throttling). **How to fix:** Back off and retry after a delay. Implement exponential backoff in your integration. Check the `Retry-After` response header if present — it indicates how many seconds to wait before retrying. **What it means:** An unexpected error occurred on the Cleo Pay server. This is not caused by your request. **How to fix:** Retry the request after a short delay. If the issue persists, contact [Cleo Pay support](mailto:support@cleo-pay.com) and provide the request ID from the response headers if available. *** ## Common integration mistakes The following mistakes account for the majority of errors seen in new integrations: | Mistake | Resulting error | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Forgetting the `Authorization` header | `401 Unauthorized` | | Sending `contactId` AND `payableId` in the same payment request | `400 Bad Request` | | Setting `amountCents` to `0` or a negative value | `422 Unprocessable Entity` | | Setting `scheduledAt` to today's date or a past date | `422 Unprocessable Entity` | | Exceeding 3 micro-deposit verification attempts | Account status becomes `unverified` (no HTTP error on the attempt itself, but subsequent attempts are rejected) | | Creating a duplicate contact without `allowDuplicates: true` | `409 Conflict` | | Using a raw token string without the `Bearer ` prefix | `401 Unauthorized` | *** ## Debugging tips * **Always read the `message` field.** Cleo Pay error responses include a human-readable description that directly identifies the problem. * **Log request IDs.** Include request/response logging in your integration so you can correlate errors with specific API calls. * **Use the staging environment** (`https://api.cleo-pay.com`) for development and testing to avoid affecting production data. # Paginate Cleo Pay API List Endpoints Source: https://docs.cleo-pay.com/reference/pagination Cleo Pay list endpoints use page and pageSize query parameters. Learn how to paginate results and use search filters efficiently. List endpoints in the Cleo Pay API return paginated results so you can retrieve large datasets efficiently without overwhelming your application or the API. All paginated endpoints use a consistent set of query parameters and return a uniform response shape. *** ## Endpoints that support pagination | Endpoint | Description | | ------------------ | ----------------------------------- | | `GET /v1/payables` | List payables (invoices). | | `GET /v1/contacts` | List contacts (vendors and payees). | `GET /v1/bank-accounts` does **not** use pagination — it returns all of your bank accounts in a single response, regardless of count. *** ## Query parameters | Parameter | Type | Default | Constraints | Description | | ---------- | ------ | ------- | ------------------ | ---------------------------- | | `page` | number | `1` | min `1` | The page number to retrieve. | | `pageSize` | number | `25` | min `1`, max `100` | Number of items per page. | *** ## Response shape All paginated endpoints return a consistent envelope: ```json title="Paginated response envelope" theme={null} { "items": [ /* ... array of resource objects ... */ ], "totalCount": 142 } ``` * **`items`** — the array of resource objects for the current page. * **`totalCount`** — the total number of records matching your query (across all pages). Use this to compute the total number of pages: `Math.ceil(totalCount / pageSize)`. *** ## Basic pagination example Retrieve the second page of contacts, with 10 contacts per page: ```bash title="Fetch page 2 with 10 items per page" theme={null} curl --request GET \ --url 'https://api.cleo-pay.com/v1/contacts?page=2&pageSize=10' \ --header 'Authorization: Bearer ' ``` ```json title="Paginated contacts response" theme={null} { "items": [ { "id": "contact_01hwxyz111", "displayName": "Acme Supplies LLC", "type": "payee", "status": "active", "paymentReady": true, "createdAt": "2024-05-15T08:00:00Z" }, { "id": "contact_01hwxyz222", "displayName": "Beta Logistics Inc", "type": "payee", "status": "active", "paymentReady": false, "createdAt": "2024-05-20T09:30:00Z" } ], "totalCount": 142 } ``` *** ## Search and filter parameters Combine pagination with search and filter parameters to narrow results. ### Contacts (`GET /v1/contacts`) | Parameter | Values | Description | | --------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `search` | string | Free-text search across contact name and email fields. | | `status` | `active`, `archived` | Filter by contact status. Defaults to returning both `active` and `archived` contacts; revoked contacts are excluded unless explicitly requested. | | `type` | `payee`, `business` | Filter by contact type. | ```bash title="Search active payees matching 'acme'" theme={null} curl --request GET \ --url 'https://api.cleo-pay.com/v1/contacts?type=payee&status=active&search=acme&page=1&pageSize=25' \ --header 'Authorization: Bearer ' ``` ### Payables (`GET /v1/payables`) | Parameter | Values | Description | | --------- | ---------------- | --------------------------------------- | | `search` | string | Free-text search across payable fields. | | `status` | `approved`, etc. | Filter by payable status. | ```bash title="List approved payables" theme={null} curl --request GET \ --url 'https://api.cleo-pay.com/v1/payables?status=approved&page=1&pageSize=50' \ --header 'Authorization: Bearer ' ``` *** ## Iterate through all pages When you need to retrieve every record matching a query, loop through pages until you've collected `totalCount` items. Here's a JavaScript example: ```javascript title="Fetch all contacts (JavaScript)" theme={null} async function getAllContacts(token) { const pageSize = 100; let page = 1; let allContacts = []; while (true) { const response = await fetch( `https://api.cleo-pay.com/v1/contacts?page=${page}&pageSize=${pageSize}`, { headers: { Authorization: `Bearer ${token}` }, } ); const data = await response.json(); allContacts = allContacts.concat(data.items); // Stop when we've collected all records if (allContacts.length >= data.totalCount) { break; } page++; } return allContacts; } ``` Use the maximum `pageSize` of `100` when iterating all pages to minimize the number of API calls and stay well within rate limits. The same pattern works for any paginated endpoint — just swap the URL and adjust your filters as needed. # Cleo Pay Webhooks: Events, Delivery, and Signature Verification Source: https://docs.cleo-pay.com/reference/webhooks How Cleo Pay webhooks work: the event payload envelope, all event types, HMAC-SHA256 signature verification via the Cleo-Signature header, and retry behavior. Webhooks let Cleo Pay push real-time notifications to your server when important events occur — such as a payable being approved, a payment completing or failing, or a bank account becoming verified. Instead of polling the API, you receive an HTTP `POST` request at your configured endpoint the moment an event fires. Webhook endpoints (URL, event subscriptions, and signing secrets) are managed through the **Cleo Pay dashboard**. Your signing secret is shown **once** — when the endpoint is created or the secret is rotated — so store it securely. *** ## How webhooks work When a subscribed event occurs, Cleo Pay sends an HTTP `POST` request with a JSON body to your endpoint. Every delivery is signed — see [Verifying deliveries](#verifying-deliveries). Your endpoint must respond with any `2xx` status code within 10 seconds to acknowledge receipt; the response body is ignored. If your endpoint does not return a `2xx` response — due to a timeout, connection error, or non-2xx status — Cleo Pay retries the delivery with exponential backoff. The same event may be delivered more than once. Process events idempotently by deduplicating on the event `id`. *** ## Payload structure Every delivery uses the same JSON envelope: ```json title="Example delivery" theme={null} { "id": "evt_x2f8kq0m3vjw9r41ptbn7c5d", "type": "payable.approved", "sequence": "10427", "createdAt": "2026-07-15T12:00:00.000Z", "businessId": "0d2c7a3e-9f41-4b8a-b6c2-5e8f13a9d7e0", "data": { "object": "payable", "id": "b1e2c3d4-5f6a-7b8c-9d0e-1f2a3b4c5d6e" } } ``` | Field | Type | Description | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique event ID (`evt_…`). Stable across retries **and** manual redeliveries — deduplicate on this. Also sent as the `Cleo-Event-Id` header. | | `type` | string | The event type, e.g. `payable.approved`. | | `sequence` | string | Global, monotonically increasing ordering signal. Serialized as a string because it can exceed `Number.MAX_SAFE_INTEGER` — compare it as a big integer. | | `createdAt` | string | ISO 8601 instant the event was generated. Fixed at emission — not the time of the current delivery attempt. | | `businessId` | string | ID of the business the event belongs to. | | `data.object` | string | Resource type: `payable`, `bank_account`, or `contact`. | | `data.id` | string | ID of the resource the event refers to. | Payloads are **thin references** — they never contain a resource snapshot. Fetch the resource's current state from the API using `data.id` (for example, `GET /v1/payables/{id}`). A delivery that arrives late or out of order still leads you to the latest state. Events are dispatched in emission order on a best-effort basis, but retries and concurrent delivery mean **arrival order is not guaranteed**. Order events by `sequence` (or `createdAt`), never by arrival. *** ## Event types Subscribe to individual event types, or to the wildcard `*` to receive all of them. ### Payable events | Event | Fires when | | -------------------------------- | ---------------------------------------------------------------- | | `payable.created` | A payable is created. | | `payable.approved` | A payable is approved. | | `payable.declined` | A payable's approval is declined. | | `payable.cancelled` | A payable is cancelled. | | `payable.approval_reset` | A payable's approval state is reset and it requires re-approval. | | `payable.payment.scheduled` | A payment for the payable is scheduled. | | `payable.payment.partially_paid` | A payment settles that covers part of the payable's balance. | | `payable.paid` | The payable is fully paid. | | `payable.payment.failed` | A payment attempt fails. | | `payable.payment.reversed` | A completed payment is reversed or returned. | ### Bank account events | Event | Fires when | | ---------------------------------- | ----------------------------------------------------------------------- | | `bank_account.created` | A bank account is added. | | `bank_account.verified` | A bank account passes verification. | | `bank_account.verification_failed` | Verification fails — for example, micro-deposit attempts are exhausted. | | `bank_account.removed` | A bank account is removed. | ### Contact events | Event | Fires when | | -------------------- | --------------------------------------- | | `contact.created` | A contact is created. | | `contact.updated` | A contact's details change. | | `contact.archived` | A contact is archived. | | `contact.unarchived` | A contact is restored from the archive. | *** ## Delivery headers Every delivery includes these HTTP headers: | Header | Value | | ----------------- | -------------------------------------------------------------------------------------------- | | `Cleo-Signature` | `t=,v1=` — see [Verifying deliveries](#verifying-deliveries). | | `Cleo-Event-Id` | The event ID (`evt_…`). Identical across retries and redeliveries of the same event. | | `Cleo-Webhook-Id` | Unique ID for this delivery. A manual redelivery of the same event gets a new value. | | `Content-Type` | `application/json` | | `User-Agent` | `Cleo-Webhooks/1.0` | *** ## Verifying deliveries Every delivery is signed with **HMAC-SHA256** so you can confirm it came from Cleo Pay and was not tampered with in transit. The `Cleo-Signature` header carries a Unix timestamp and a hex-encoded signature: ```text theme={null} Cleo-Signature: t=1751328000,v1=fc4197d20c24733ce3bc083109ec596a6e7db714839c35fe02f6777f1fe6cb0c ``` The signature is the HMAC-SHA256 of the string `{t}.{raw request body}` — the timestamp, a literal dot, then the exact body bytes — keyed with your endpoint's signing secret (`whsec_…`). Verify against the exact bytes Cleo Pay sent. Do not JSON-parse and re-serialize the body first — any change in key order or whitespace invalidates the signature. Extract `t` (Unix timestamp in seconds) and `v1` (hex-encoded signature). If `t` is more than 5 minutes from the current time, reject the request. This bounds replay attacks. The timestamp is computed fresh for every delivery attempt, so legitimate retries always carry a current `t`. Compute the HMAC-SHA256 of `{t}.{rawBody}` with your signing secret and compare it to `v1` using a constant-time comparison. ```typescript verify-cleo-signature.ts theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 300; export function verifyCleoSignature(secret: string, rawBody: string, header: string): boolean { let t: number | undefined; let v1: string | undefined; for (const part of header.split(",")) { const eqIndex = part.indexOf("="); if (eqIndex === -1) continue; const key = part.slice(0, eqIndex).trim(); const value = part.slice(eqIndex + 1).trim(); if (key === "t") t = Number(value); else if (key === "v1") v1 = value; } if (t === undefined || !Number.isFinite(t)) return false; if (!v1 || v1.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(v1)) return false; const nowSeconds = Math.floor(Date.now() / 1000); if (Math.abs(nowSeconds - t) > TOLERANCE_SECONDS) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); const actual = Buffer.from(v1, "hex"); if (expected.length !== actual.length) return false; return timingSafeEqual(expected, actual); } ``` ```typescript Express endpoint theme={null} import express from "express"; import { verifyCleoSignature } from "./verify-cleo-signature"; const app = express(); app.post("/webhooks/cleo", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); const header = req.header("Cleo-Signature") ?? ""; if (!verifyCleoSignature(process.env.CLEO_WEBHOOK_SECRET!, rawBody, header)) { return res.status(401).send("invalid signature"); } const event = JSON.parse(rawBody); // Deduplicate on event.id, enqueue for processing, then acknowledge. res.status(200).send("ok"); }); ``` ### Test your implementation Use this known-good vector to unit-test the HMAC computation (bypass the timestamp-freshness check for the fixed `t`): | Input | Value | | ------------- | ------------------------------------------------------------------ | | Secret | `whsec_test_known_vector` | | Timestamp `t` | `1751328000` | | Raw body | `{"id":"evt_test123","type":"payable.approved"}` | | Expected `v1` | `fc4197d20c24733ce3bc083109ec596a6e7db714839c35fe02f6777f1fe6cb0c` | ### Signing secrets * Each endpoint has its own secret, prefixed with `whsec_` (32 bytes of entropy). * The full secret is shown **once**, when the endpoint is created or its secret is rotated. Only the last 4 characters are visible afterwards. * Rotation takes effect immediately, with no overlap window. Deliveries already in flight at rotation time were signed with the old secret and may fail your verification — redeliver them from the dashboard if needed. Treat your signing secret like a password. If it is ever exposed, rotate it from the dashboard immediately — anyone holding the secret can forge deliveries that pass verification. *** ## Retry behavior A delivery succeeds only when your endpoint returns a `2xx` status within **10 seconds**. Anything else — a timeout, a connection failure, or a non-2xx response — is retried: * Up to **10 attempts** per delivery. * **Exponential backoff** starting at 60 seconds and doubling each attempt (1m, 2m, 4m, …), spanning roughly **8.5 hours** in total. * The body and event `id` are identical on every attempt; the signature timestamp is computed fresh per attempt. ### Automatic pausing An endpoint is automatically paused after **20 consecutive failed deliveries**, or after failing continuously for **24 hours** — whichever comes first. When that happens: * The endpoint stops receiving deliveries and shows as **Paused** in the dashboard. * Your business admins are notified by email. * Once your endpoint is fixed, re-enable it from the dashboard and redeliver anything you missed from the delivery log. A manual redelivery reuses the same event `id` and body — your dedupe logic treats it as the same event — but carries a new `Cleo-Webhook-Id` and a freshly computed signature. *** ## Endpoint requirements * **HTTPS only** — `http://` URLs are rejected. * The URL must resolve to a **publicly routable address**. Localhost, private-network, and internal addresses are rejected. * Redirects are **not followed** — the endpoint must respond directly. * Respond within **10 seconds** — acknowledge first, process asynchronously. *** ## Best practices Reject anything that fails signature verification before touching the payload. Your webhook URL is reachable by anyone on the internet — the signature is what makes a request trustworthy. Retries and redeliveries reuse the same event `id`. Store processed IDs and skip duplicates instead of processing them twice. Return `2xx` immediately and hand the event to a queue or background job. Slow handlers hit the 10-second timeout and trigger retries. Events can arrive out of order. Order by `sequence`, and fetch the resource's current state from the API rather than reconstructing it from events.