Customers API
Create, find, update, and delete team-scoped customer records through public API v1.
Summary
Customer operations use customers.read or customers.write and return records only from the credential's team.
The current single-customer and write responses have a schema mismatch. They can return 500 Response validation failed, including after a customer has been saved or deleted. Check the customer list or application before retrying a write. This page describes that limitation rather than promising successful CRUD responses.
Capabilities
Endpoints
| Method | Endpoint | Scope | Purpose |
|---|---|---|---|
| GET | /customers | customers.read | List and filter customers |
| POST | /customers | customers.write | Create or upsert a customer |
| GET | /customers/{id} | customers.read | Retrieve one customer |
| PATCH | /customers/{id} | customers.write | Upsert by path ID with name and email |
| DELETE | /customers/{id} | customers.write | Delete a customer |
Prefix each endpoint with the environment base URL, such as https://api.eigenn.io/v1.
Prerequisites
Use a bearer credential for the intended team with the scope required by the operation. User-linked credentials also depend on current team membership; Viewer membership removes write scopes. Use an isolated test customer before connecting a production import.
Concepts
Important Field Rules
- Email fields use email format validation.
- Use an ISO 3166-1 alpha-2 countryCode, such as US. The request field is a string; it does not validate membership in the ISO list.
- peppolId requires four digits, a colon, and an identifier containing letters, numbers, hyphens, periods, underscores, or tildes.
- Tags are objects with an existing team tag's UUID and a name. Supplying a name does not create or rename a tag.
- customFields accepts a map of string, number, boolean, or null values. Sending this map replaces the stored map; include values you want to retain.
- Cursor values are opaque.
Workflow
List Customers
GET /customers supports:
- q text search
- sort tuple
- cursor
- pageSize from 1 to 100
- tag IDs
- country codes
- tab filters: all, first-time, repeat, recent, unpaid, or paid
- start and end creation dates in YYYY-MM-DD format, inclusive in UTC
Country filters match a stored country name or code. First-time and repeat use invoice counts including drafts, scheduled, and canceled invoices. The recent tab covers customers created in the last 30 days. These filters do not describe a customer's payment history by themselves.
The response contains data and cursor metadata. It does not include a total count.
curl --request GET \
--url "https://api.eigenn.io/v1/customers?pageSize=25&q=acme" \
--header "Authorization: Bearer $EIGENN_API_TOKEN"Create or Upsert
POST /customers needs:
- name
Optional fields include billing email, contact, phone, website, address, country code, VAT number, Peppol ID, note, and tags.
curl --request POST \
--url https://api.eigenn.io/v1/customers \
--header "Authorization: Bearer $EIGENN_API_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: customer-acme-2026-07-11" \
--data '{
"name": "Acme Corporation",
"email": "[email protected]",
"billingEmail": "[email protected]",
"countryCode": "US"
}'Upsert matches a supplied id, not a name or email address. Without an ID, repeated creates can produce separate customers with the same name and email. An unused supplied UUID creates a record; an ID owned by another team is rejected. Keep your external-to-Eigenn ID mapping.
OpenAPI advertises 201 for POST, but the current handler does not explicitly set that status. It also validates its response after saving, which currently encounters the response mismatch described below. Do not use a failed response as evidence that no record was created.
The public request schema does not advertise an arbitrary externalId or metadata object. Store joins with documented fields and the returned Eigenn customer ID.
Retrieve a Customer
GET /customers/{id} selects the team's customer details, tags, custom fields, counts, and overdue amount. Its result currently omits required activity and engagement fields from the response schema, causing response validation to fail. The list endpoint has a broader computed result; do not assume the single-record route provides the same finance summary.
Missing and other-team IDs are not reliably mapped to 404 by this route. A failed lookup must not be treated as proof that a customer was deleted.
Update a Customer
PATCH /customers/{id} requires name and email, just like POST. The path ID takes precedence over a body ID. An unused path UUID can create a customer; this is not an update-only operation.
Omitted optional contact and address fields stay unchanged on an existing record. Tags are different: send the complete desired tag list. Omitting tags or sending an empty array removes existing tag associations. A supplied customFields map replaces the map rather than merging individual keys. Explicit null clears nullable fields.
Use a new Idempotency-Key for each logical change. A reused key does not compare request bodies and can replay an earlier successful response. Failed responses are not cached, so the key does not prevent repeating a write that succeeded before response validation failed.
Delete a Customer
DELETE /customers/{id} permanently deletes the customer before validating the returned details. The current response mismatch can therefore report failure after deletion.
Associated invoices remain, but their customer link is cleared. Customer notes, tasks, file records, contacts, projects, and recovery records can be deleted through dependent relationships. Stored file objects are not removed by this operation. Review the customer deletion guidance before removal.
Behavior specification
Common Errors
| Status | Cause |
|---|---|
| 401 | Missing or invalid bearer token |
| 403 | Missing customers.read or customers.write |
| 400 or 422 | Request validation failed; inspect the returned body |
| 409 | Another keyed request is still in progress |
| 429 | Resource limit exceeded |
| 500 | Response validation, missing-customer handling, or another server failure |
| 503 | The idempotency guard is unavailable |
Current response limitation
The response schema requires lastActivityAt and engagementScore, but the single-record, upsert, and deletion queries do not supply them. A write can persist before validation returns 500. Upsert responses also omit customFields, even when the values were saved. Use the list or customer screen to reconcile state, retain the request ID, and avoid an automatic create retry. These are source-identified limitations; runtime acceptance remains pending.
Diagrams
Rendering diagram…
Screenshots
Not applicable: this page describes an API or a non-visual workflow rather than an application screen.
Verification
- List seeded customers and check data plus meta.cursor, meta.hasNextPage, and meta.hasPreviousPage. Exercise the last page and creation-date filters.
- Create one uniquely named synthetic customer. Record the HTTP outcome, then inspect the list or application for persisted state even if the response is 500. Do not retry blindly.
- PATCH that known ID with name, email, and the complete desired tags and customFields. Check persistence independently of the write response. Confirm omitted tags are removed on a disposable fixture.
- Compare GET-by-ID with the list result and record the current response-validation failure. Test missing and other-team IDs without assuming a 404 contract is implemented.
- Delete only the disposable customer. Check list membership and related records after the call, including when it returns 500.
- Confirm missing credentials return 401 and a read-only credential cannot write. Keep runtime verification pending until these checks have repeatable evidence.