# Developers > dafexpert API documentation: authentication, companies, accounting events, FNE invoices, MCP server. ## Getting started - [API overview](https://developers.stg.dafexpert.com/en/article/getting-started.txt): What the dafexpert API does, who it is for, the environments and your first call. - [Choose the right integration](https://developers.stg.dafexpert.com/en/article/choose-integration.txt): Partner API, MCP server, SSO: compare the integration surfaces and pick the one that fits your need. - [Get your credentials](https://developers.stg.dafexpert.com/en/article/credentials.txt): How a workspace admin connects your application, sends you a client_id and client_secret, rotates the secret or revokes access. - [OAuth 2.0 authentication](https://developers.stg.dafexpert.com/en/article/authentication.txt): Get a token with OAuth 2.0 client credentials, cache it, renew it and handle 401 responses. - [User sign-in (SSO)](https://developers.stg.dafexpert.com/en/article/sso.txt): Let your users sign in once to access both dafexpert and your application, with OpenID Connect. - [Error handling & status codes](https://developers.stg.dafexpert.com/en/article/errors.txt): The HTTP status codes returned by the API, the error body format, partial results in batches and when to retry. - [Idempotency & retries](https://developers.stg.dafexpert.com/en/article/idempotency.txt): Why replaying a call never creates a duplicate, which key each resource uses, and how resends with different content behave. - [Webhooks](https://developers.stg.dafexpert.com/en/article/webhooks.txt): Webhooks are not available yet: how to track your events' status today with cursor-based sync. ## Tutorials - [Supported use cases](https://developers.stg.dafexpert.com/en/article/use-cases.txt): The integrations the dafexpert API supports today: cooperative management, field apps, invoicing and AI assistants. - [Create or update a company](https://developers.stg.dafexpert.com/en/article/create-company.txt): Create a cooperative and one of its farmers with PUT /v1/partners/companies/{external_id}, then keep them up to date. - [Create companies in bulk](https://developers.stg.dafexpert.com/en/article/bulk-companies.txt): Migrate a whole portfolio of cooperatives and farmers with POST /v1/partners/companies, then track the batch. - [Manage users](https://developers.stg.dafexpert.com/en/article/manage-users.txt): Give a manager or an accountant access to their companies with PUT /v1/partners/users/{external_id}, then remove it with DELETE. - [Send accounting events](https://developers.stg.dafexpert.com/en/article/send-events.txt): A complete flow: a season advance, a cocoa purchase from a producer, then a mobile money payment that deducts the advance. - [Track event processing](https://developers.stg.dafexpert.com/en/article/track-events.txt): Read the status of an event, sync every change with a cursor and handle rejections. - [Issue an FNE document](https://developers.stg.dafexpert.com/en/article/fne-documents.txt): Issue an FNE-certified agricultural produce purchase slip and customer invoice, then retrieve their PDF. - [Connect an AI assistant (MCP)](https://developers.stg.dafexpert.com/en/article/connect-ai-assistant.txt): Add dafexpert's MCP server to Claude to query your companies in natural language, read-only. ## Concepts & best practices - [Workspaces and companies](https://developers.stg.dafexpert.com/en/article/workspaces-companies.txt): How dafexpert organizes your customers: one workspace per organization, one company per set of books, and the link with your external identifiers. - [Accounting frameworks](https://developers.stg.dafexpert.com/en/article/accounting-frameworks.txt): SYSCOHADA, SYCEBNL and PCG: how a company's country sets its framework, its currency and the accounts used for your events. - [Event catalog](https://developers.stg.dafexpert.com/en/article/event-catalog.txt): Every version 2 event type: what it is for, its key fields, its checks and what it creates in dafexpert. - [v1 and v2 events](https://developers.stg.dafexpert.com/en/article/event-versions.txt): The two versions of the event contract, successive versions of the same event, per-company effects and discrepancies. - [Amounts and currencies](https://developers.stg.dafexpert.com/en/article/amounts-currencies.txt): The expected amount format (whole FCFA, no decimals), the company currency, rounding and total checks. - [Field limits](https://developers.stg.dafexpert.com/en/article/field-limits.txt): Maximum lengths, formats and list sizes accepted by the API: identifiers, dates, NCC, phone numbers, emails, batches. ## Advanced guides - [Rate limiting](https://developers.stg.dafexpert.com/en/article/rate-limiting.txt): The API's volume limits today, fair-use rules and how to get ready for upcoming limits. - [Versioning](https://developers.stg.dafexpert.com/en/article/versioning.txt): How the API evolves without breaking your integration: version in the URL, version field in bodies, the v2 event contract and compatibility rules. ## Migration & release notes - [Migrate to v2 events](https://developers.stg.dafexpert.com/en/article/migrate-events-v2.txt): Move from the v1 event contract (configured mappings) to the v2 contract (type catalog, business documents, per-company effects, versions). - [Public roadmap](https://developers.stg.dafexpert.com/en/article/roadmap.txt): Planned changes to the dafexpert API. ## Resources - [Glossary](https://developers.stg.dafexpert.com/en/article/glossary.txt): dafexpert, OHADA accounting and Ivorian e-invoicing terms used throughout this documentation, with their French API names. - [Need support?](https://developers.stg.dafexpert.com/en/article/support.txt): How to reach the dafexpert team and what to include to get a fast answer. ## Introduction - [Introduction](https://developers.stg.dafexpert.com/en/article/api-introduction.txt): Base URL, authentication, and the format of requests, identifiers, dates, amounts and errors. ## Partner - [Check credentials](https://developers.stg.dafexpert.com/en/article/get-partner-me.txt): Checks end to end that a token is valid and that the partner access is active. ## Companies - [Create or update a company](https://developers.stg.dafexpert.com/en/article/put-company.txt): Creates a cooperative, union, buyer or farmer with its first fiscal year, or updates its identity. - [Create companies in bulk](https://developers.stg.dafexpert.com/en/article/post-companies.txt): Sends up to 500 companies in one call; they are created or updated in the background. - [Track a company batch](https://developers.stg.dafexpert.com/en/article/get-company-lot.txt): Progress of a bulk creation batch: counters by status and the result for each company. ## Users - [Create or update a user](https://developers.stg.dafexpert.com/en/article/put-user.txt): Creates or links a user's login account and sets the list of companies they can access. - [Remove a user](https://developers.stg.dafexpert.com/en/article/delete-user.txt): Removes all of a user's access to your workspace's companies, without deleting their account. ## Events - [Send events](https://developers.stg.dafexpert.com/en/article/post-events.txt): Sends one event or a batch of up to 500 events to post; they are processed in the background. - [Retrieve an event](https://developers.stg.dafexpert.com/en/article/get-event.txt): Posting status of an event, with its documents and journal entries per company. - [Sync events](https://developers.stg.dafexpert.com/en/article/list-events.txt): Lists events changed since a cursor, oldest first, so you catch every status change. ## Tax documents (FNE) - [Issue a purchase slip](https://developers.stg.dafexpert.com/en/article/post-bordereau.txt): Issues a purchase slip for farm produce, certifies it with the FNE, renders its PDF and posts it, in a single call. - [Retrieve a purchase slip](https://developers.stg.dafexpert.com/en/article/get-bordereau.txt): Current state of a purchase slip: FNE certification, tax number, posting and mirror sale. - [Download a purchase slip PDF](https://developers.stg.dafexpert.com/en/article/get-bordereau-pdf.txt): Returns a temporary download link (5 minutes) for a purchase slip PDF. - [Issue an invoice](https://developers.stg.dafexpert.com/en/article/post-invoice.txt): Issues a customer invoice numbered by dafexpert, certifies it with the FNE and posts it, in a single call. - [Retrieve an invoice](https://developers.stg.dafexpert.com/en/article/get-invoice.txt): Current state of an invoice: number, FNE certification and posting. - [Download an invoice PDF](https://developers.stg.dafexpert.com/en/article/get-invoice-pdf.txt): Returns a temporary download link (5 minutes) for an invoice PDF. ## MCP server - [MCP server](https://developers.stg.dafexpert.com/en/article/mcp-overview.txt): Connect an AI assistant (Claude and others) to a user's dafexpert companies, read-only. - [List my companies](https://developers.stg.dafexpert.com/en/article/mcp-list-companies.txt): The companies the user can access, with currency, country, access level and balance. - [List fiscal years](https://developers.stg.dafexpert.com/en/article/mcp-list-exercises.txt): A company's fiscal years: dates, status and accounting framework. - [Trial balance](https://developers.stg.dafexpert.com/en/article/mcp-get-balance.txt): The trial balance of a fiscal year, by account, with prior-year comparison. - [Trial balance anomalies](https://developers.stg.dafexpert.com/en/article/mcp-get-balance-anomalies.txt): Review checks on the trial balance of a fiscal year, with a conclusion. - [Income statement](https://developers.stg.dafexpert.com/en/article/mcp-get-income-statement.txt): The income statement of a fiscal year, by line item of the company's accounting framework. - [List customer invoices](https://developers.stg.dafexpert.com/en/article/mcp-list-invoices.txt): A company's customer invoices, with an optional status filter. - [Invoice details](https://developers.stg.dafexpert.com/en/article/mcp-get-invoice.txt): The details of a customer invoice: lines, totals, customer, payments and linked transactions. - [Cash position](https://developers.stg.dafexpert.com/en/article/mcp-get-treasury-overview.txt): A company's cash overview: balance, flows, upcoming due dates and latest transactions. ## Changelog - [FNE tax documents via the API](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-fne-documents.txt): Two synchronous routes issue FNE-certified purchase slips and invoices, with tax number, QR code and PDF. - [Events version 2](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-events-v2.txt): Events sent with `"version": 2` get a type catalog, versions, per-company effects and likely-duplicate detection. - [Complete company profile](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-company-fields.txt): `PUT /v1/partners/companies/{external_id}` accepts new company types and tax, location and farmer fields. - [Bulk company creation](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-bulk-companies.txt): `POST /v1/partners/companies` loads up to 500 companies per call, processed in the background with batch tracking. - [User management](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-09-users.txt): `PUT /v1/partners/users/{id}` creates or links a dafexpert account and gives it access to companies. - [Single sign-on (SSO)](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-sso.txt): Your users sign in once to access both dafexpert and your application. - [Automatic event posting](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-event-accounting.txt): Received events are processed in the background and turned into accounting entries using configurable mappings. - [Sending and tracking events](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-events.txt): `POST /v1/partners/events` receives business events, one at a time or in batches of 500, and `GET /v1/partners/events` syncs them. - [Company creation](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-companies.txt): `PUT /v1/partners/companies/{external_id}` creates or updates a cooperative or farmer, with its first fiscal year. - [Partner API launch](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-partner-api.txt): OAuth 2.0 client credentials per workspace, and `GET /v1/partners/me` to check your credentials. - [MCP server launch](https://developers.stg.dafexpert.com/en/article/changelog-2026-10-06-mcp-server.txt): dafexpert customers can connect Claude or another AI assistant to their companies, read-only. --- # API overview URL: https://developers.stg.dafexpert.com/en/article/getting-started > What the dafexpert API does, who it is for, the environments and your first call. dafexpert is the accounting and management platform for SMEs and cooperatives in the OHADA zone and France. The **dafexpert API** lets your software feed your customers' accounting directly: create their companies, give their users access, send the operations to be booked and issue FNE-certified tax documents. Your application remains the business software (weighings, purchases, sales, payroll, farm inputs…). dafexpert keeps the official books (SYSCOHADA, SYCEBNL or PCG depending on the country) and produces the legal documents. ## Who this documentation is for - **Business software vendors** (cooperative management, field apps, vertical ERPs) that want to push their data to dafexpert. - **Integrators** connecting the information system of a federation or a group to dafexpert. - **dafexpert users** who want to query their books from an AI assistant (Claude, etc.) through the MCP server. ## What the API does | Area | Routes | Use | |---|---|---| | Partner | `GET /v1/partners/me` | Check your credentials and the workspace they give access to. | | Companies | `PUT /v1/partners/companies/{external_id}`, `POST /v1/partners/companies` | Create or update a cooperative, a farmer or an SME, one at a time or in bulk. | | Users | `PUT` and `DELETE /v1/partners/users/{id}` | Create a login account and grant or revoke access to companies. | | Events | `POST /v1/partners/events`, `GET /v1/partners/events` | Send operations to be booked (payments, expenses, payroll…) and track their status. | | Tax documents | `POST /v1/partners/companies/{external_id}/bordereaux` and `/invoices` | Issue an FNE-certified purchase slip or invoice, then fetch its PDF. | Every route is detailed in the [API reference](/en/article/api-introduction). ## Environments | Environment | API URL | Token URL (OAuth 2.0) | |---|---|---| | Staging | `https://api.stg.dafexpert.com/v1` | `https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token` | | Production | provided at go-live | provided at go-live | > **info** Build and test your whole integration in **staging**. Staging and production credentials are separate: a staging `client_id` does not work in production, and vice versa. All partner API routes are prefixed with `/v1`. Request and response bodies are JSON (`Content-Type: application/json`) and dates use the `YYYY-MM-DD` format. ## Integration surfaces ### Partner API **Server-to-server** calls, with no user involved, authenticated with OAuth 2.0 *client credentials*. This is the main surface: companies, users, events, tax documents. Your credentials are bound to **one** dafexpert workspace (for example a federation of cooperatives). ### MCP server A [Model Context Protocol](/en/article/mcp-overview) server lets an AI assistant read a dafexpert user's books: companies, fiscal years, trial balance, income statement, invoices, cash. It acts **on behalf of the signed-in user** and is read-only. Staging address: `https://api.stg.dafexpert.com/mcp`. ### Single sign-on (SSO) Your users sign in **once** to access both dafexpert and your application, using OpenID Connect. See [User sign-in (SSO)](/en/article/sso). > **info** **Coming soon**: a public API for dafexpert's customer companies (access to their own data, without going through a partner). See [Choose the right integration](/en/article/choose-integration). ## Key principles - **One workspace per set of credentials.** The workspace is derived from your credentials, never read from the request. A credential can only act within its workspace. - **Your IDs, not ours.** Every company, user, event or document is addressed by **your** identifier (`external_id`, `id`). You don't need to store dafexpert IDs. - **Idempotency.** Replaying a call never creates a duplicate. See [Idempotency & retries](/en/article/idempotency). - **French contract.** Field names (`nom`, `pays`, `statut`…) and validation messages are in French. The [glossary](/en/article/glossary) translates the main terms. - **Explicit versioning.** Every body carries a `version` field. See [Versioning](/en/article/versioning). ## Your first call in 2 minutes **1. Get your credentials** The workspace admin connects your application and sends you a `client_id` and a `client_secret`. See [Get your credentials](/en/article/credentials). **2. Request a token** Exchange your credentials for an access token valid for 5 minutes. **3. Call `GET /v1/partners/me`** This route confirms that the token is valid and that the connection to the workspace is active. **cURL** ```bash # 1. Token TOKEN=$(curl -s -X POST "https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token" \ -d grant_type=client_credentials \ -d client_id=partner-acme-coop \ -d client_secret="$DAFEXPERT_CLIENT_SECRET" | jq -r .access_token) # 2. First call curl https://api.stg.dafexpert.com/v1/partners/me \ -H "Authorization: Bearer $TOKEN" ``` **Response** ```json { "partner": "acme", "workspaceId": "3f6b2c1e-8a4d-4f1a-9b7e-2d5c8e9f0a12", "apiVersion": 1 } ``` ## Next steps - [Choose the right integration](/en/article/choose-integration) : Partner API, MCP or SSO: which surface for which need. - [Authentication](/en/article/authentication) : OAuth 2.0 client credentials, token renewal, handling 401s. - [Create a company](/en/article/create-company) : Your first business call: a cooperative and its first fiscal year. - [Send events](/en/article/send-events) : Payments, expenses, payroll: the operations to book. - [Error handling & status codes](/en/article/errors) : Error format and when to retry. - [API reference](/en/article/api-introduction) : Every route, its parameters and responses. --- # Choose the right integration URL: https://developers.stg.dafexpert.com/en/article/choose-integration > Partner API, MCP server, SSO: compare the integration surfaces and pick the one that fits your need. dafexpert offers several ways to connect. They are not mutually exclusive: a business software vendor typically uses the **partner API** for data and **SSO** for its users. This page helps you choose. ## Comparison | | Partner API | MCP server | SSO (OpenID Connect) | Public company API | |---|---|---|---|---| | For | Software vendors and integrators | dafexpert users and their AI assistants | Vendors whose users also use dafexpert | dafexpert's customer companies | | Who acts | Your server, no user | The signed-in user | The signed-in user | Coming soon | | Authentication | OAuth 2.0 client credentials | OAuth 2.0 Authorization Code + PKCE | OAuth 2.0 Authorization Code (PKCE optional) | Coming soon | | Scope | One workspace: its companies, users, events, tax documents | The books the user has access to | User identity (`openid email profile`) | Coming soon | | Read / write | Write and tracking | Read-only | Sign-in only | Coming soon | | Address (staging) | `https://api.stg.dafexpert.com/v1/partners/...` | `https://api.stg.dafexpert.com/mcp` | `https://auth.stg.dafexpert.com/realms/dafexpert` | Coming soon | | Status | Available | Available | Available | Coming soon | ## Partner API The **server-to-server** API. Your application creates your customers' companies in dafexpert, gives them users, sends the operations to be booked and issues FNE-certified tax documents. ### Choose it if - you build business software (cooperatives, agricultural collection, vertical ERP) and your customers want official accounting without double entry; - you need to create companies in bulk, for example every cooperative of a federation; - you need to issue FNE-certified purchase slips or invoices (Côte d'Ivoire) from your application. To get started: [Get your credentials](/en/article/credentials), then [OAuth 2.0 authentication](/en/article/authentication). Typical scenarios are described in [Use cases](/en/article/use-cases). ## MCP server The MCP server exposes a dafexpert user's books to AI assistants that support the [Model Context Protocol](/en/article/mcp-overview): Claude (custom connector, Claude Desktop, Claude Code) and other MCP clients. The assistant sees exactly what the user sees in dafexpert, read-only. ### Choose it if - you want to query your accounting in plain language ("What is my net income at the end of September?"); - you are building an agent that needs a company's trial balance, income statement, invoices or cash position; - you don't need to write anything. To get started: [Connect an AI assistant](/en/article/connect-ai-assistant). ## Single sign-on (SSO) Your users sign in once to access both dafexpert and your application. After signing in, a user with access to both picks the application to open and can switch between them. ### Choose it if - your users (cooperative managers, accountants) work both in your application and in dafexpert; - you create their accounts through the partner API (`PUT /v1/partners/users/{id}`) and want to spare them a second password. To get started: [User sign-in (SSO)](/en/article/sso). ## Public company API > **info** **Coming soon.** An API for dafexpert's customer companies, to access their own data without going through a partner. It is not available yet. In the meantime, the [MCP server](/en/article/mcp-overview) gives read access to a user's books. ## Common combinations | Your situation | Recommended surfaces | |---|---| | Cooperative management software whose users also look at their accounting | Partner API + SSO | | One-off migration of a federation's companies | Partner API (bulk creation) | | AI assistant for an accounting firm or a business owner | MCP server | | Field app issuing certified purchase slips | Partner API (tax documents) + event tracking | ## Next steps - [Get your credentials](/en/article/credentials) : Connect your application to a workspace. - [User sign-in](/en/article/sso) : Set up SSO. - [MCP server](/en/article/mcp-overview) : The tools exposed to AI assistants. --- # Get your credentials URL: https://developers.stg.dafexpert.com/en/article/credentials > How a workspace admin connects your application, sends you a client_id and client_secret, rotates the secret or revokes access. The partner API uses one `client_id` and one `client_secret` **per workspace**. You don't create them yourself: the **workspace admin** of your customer's dafexpert workspace connects your application, then sends them to you. This way your customer stays in control of who accesses their data. ## Before you start - Your application must be **listed** by dafexpert and made visible to the workspace. Email [support@dafexpert.com](mailto:support@dafexpert.com) to become a partner. - Your contact at the customer must be an **owner or admin** of the workspace. Other members don't see the connected apps screen. ## Connecting your application to the workspace **1. Open connected apps** In dafexpert, the workspace admin opens *Paramètres › Applications connectées* (Settings › Connected apps). Your application is listed there. **2. Click "Connecter" (Connect)** dafexpert creates your technical credentials for this workspace. **3. Copy the credentials** The `client_id` and `client_secret` are shown **only once**. The admin copies them right away. **4. Send them to you over a secure channel** Shared password manager, secrets vault, encrypted message. Never in a plain email or a ticket. **5. Check the credentials** Request a token, then call `GET /v1/partners/me` (see below). > **warning** The `client_secret` is **never shown again** after the connection. If it is lost, the admin must generate a new one. ## What you receive | Item | Example | Notes | |---|---|---| | `client_id` | `partner-acme-coop` | Specific to the pair "your application × this workspace". Not secret, and never changes. | | `client_secret` | `••••••••` | Secret. Keep it in a vault, never in code or in a mobile app. | | Token URL | `https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token` | Staging. The production URL is provided at go-live. | | API URL | `https://api.stg.dafexpert.com/v1` | Staging. | ### Several workspaces, several sets of credentials If your application serves several customers (for example several federations), each one connects your application to **its own** workspace and you receive **one set of credentials per workspace**. Store them per customer and always use the right one: the workspace is derived from the credentials, and there is no request parameter to pick it. ## Checking your credentials **cURL** ```bash TOKEN=$(curl -s -X POST "https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token" \ -d grant_type=client_credentials \ -d client_id=partner-acme-coop \ -d client_secret="$DAFEXPERT_CLIENT_SECRET" | jq -r .access_token) curl https://api.stg.dafexpert.com/v1/partners/me -H "Authorization: Bearer $TOKEN" ``` **200 OK** ```json { "partner": "acme", "workspaceId": "3f6b2c1e-8a4d-4f1a-9b7e-2d5c8e9f0a12", "apiVersion": 1 } ``` A `200` confirms both that the token is valid and that the connection to the workspace is active. A `401` means wrong credentials, or an application that has been disconnected from the workspace. ## Rotating the secret In *Paramètres › Applications connectées*, the admin can **generate a new secret**, for example after a team member leaves or a suspected leak. The new secret is shown only once, like the first one. The `client_id` does not change. > **warning** **The old secret stops working immediately.** Token requests with the old secret fail as soon as the new one is generated. Agree on a time slot with the admin and update your configuration right away. A grace period during which both secrets stay valid is planned but not available yet. Tokens already issued remain valid until they expire (5 minutes at most). Make sure your code can reload the secret from your vault without restarting the service. ## Disconnecting the application The admin can also **disconnect** your application. Access is cut off: subsequent calls get a `401`, including with a token issued before the disconnection (within 30 seconds at most). On your side, a `401` that persists after requesting a fresh token means access has been revoked. Stop sending, keep the data queued and notify the workspace admin. See [Authentication](/en/article/authentication). The admin also sees the **date of your last call** on this screen, which helps spot a stalled integration. ## Security best practices - Keep the `client_secret` **server-side** only: secrets vault, encrypted environment variable. Never in a mobile app, a browser or a Git repository. - Never write the secret or tokens to your logs. - Use **separate** credentials for staging and production. - Ask for a new secret as soon as a leak is possible. ## Next steps - [OAuth 2.0 authentication](/en/article/authentication) : Request, cache and renew the token. - [GET /v1/partners/me](/en/article/get-partner-me) : The credential check route. --- # OAuth 2.0 authentication URL: https://developers.stg.dafexpert.com/en/article/authentication > Get a token with OAuth 2.0 client credentials, cache it, renew it and handle 401 responses. The partner API uses the OAuth 2.0 **client credentials** flow: the standard way for one server to authenticate with another, with no user involved. Your server exchanges its `client_id` and `client_secret` for a **short-lived access token** (5 minutes), which it then sends with every call. > **info** No credentials yet? See [Get your credentials](/en/article/credentials). ## The flow at a glance 1. Your server sends `client_id` and `client_secret` to the token URL. 1. It receives an `access_token` valid for `expires_in` seconds (300). 1. It calls the API with the `Authorization: Bearer ` header. 1. Shortly before expiry, it requests a new token with the same credentials. ## Token URL | Environment | Token URL | API URL | |---|---|---| | Staging | `https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token` | `https://api.stg.dafexpert.com/v1` | | Production | provided at go-live | provided at go-live | ## Requesting a token `POST` to the token URL with an `application/x-www-form-urlencoded` body (not JSON). - `grant_type` (string, body, required) : Always `client_credentials`. - `client_id` (string, body, required) : Your identifier for this workspace, for example `partner-acme-coop`. - `client_secret` (string, body, required) : The secret sent to you by the workspace admin. ### Response - `access_token` (string) : The token (JWT) to put in the `Authorization` header. Treat it as an opaque value: don't decode it to drive your logic. - `expires_in` (integer) : Validity in seconds: `300` (5 minutes). - `token_type` (string) : `Bearer`. The response may contain other standard fields from the identity provider: ignore them. **cURL** ```bash curl -X POST "https://auth.stg.dafexpert.com/realms/dafexpert/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=client_credentials \ -d client_id=partner-acme-coop \ -d client_secret="$DAFEXPERT_CLIENT_SECRET" ``` **Node.js** ```javascript const res = await fetch(process.env.DAFEXPERT_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.DAFEXPERT_CLIENT_ID, client_secret: process.env.DAFEXPERT_CLIENT_SECRET, }), }); if (!res.ok) throw new Error('Token refused: ' + res.status); const { access_token, expires_in } = await res.json(); ``` **Python** ```python import os, requests r = requests.post( os.environ["DAFEXPERT_TOKEN_URL"], data={ "grant_type": "client_credentials", "client_id": os.environ["DAFEXPERT_CLIENT_ID"], "client_secret": os.environ["DAFEXPERT_CLIENT_SECRET"], }, timeout=10, ) r.raise_for_status() token = r.json()["access_token"] ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => getenv('DAFEXPERT_CLIENT_ID'), 'client_secret' => getenv('DAFEXPERT_CLIENT_SECRET'), ]), ]); $body = json_decode(curl_exec($ch), true); $token = $body['access_token']; ``` **Response** ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ...", "expires_in": 300, "token_type": "Bearer" } ``` If the credentials are wrong, or the application has been disconnected from the workspace, the token request fails with a standard OAuth error (usually `invalid_client`): check the credentials with the workspace admin before retrying. ## Calling the API with the token **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/me \ -H "Authorization: Bearer $TOKEN" ``` **Node.js** ```javascript const me = await fetch('https://api.stg.dafexpert.com/v1/partners/me', { headers: { Authorization: 'Bearer ' + accessToken }, }).then((r) => r.json()); ``` **Python** ```python me = requests.get( "https://api.stg.dafexpert.com/v1/partners/me", headers={"Authorization": f"Bearer {token}"}, timeout=10, ).json() ``` **PHP** ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], ]); $me = json_decode(curl_exec($ch), true); ``` ## Renewing the token There is **no refresh token**: when the token expires, just request a new one with the same credentials. - **Keep the token in memory** and reuse it for every call during its lifetime. Don't request a token per call. - **Renew it about 30 seconds before** `expires_in`, so you never send a token that expires in flight. - **One token per workspace**: if you serve several workspaces, keep one cache per `client_id`. - With several processes or threads, avoid having each one request its own token at the same moment: share the cache or guard renewal with a lock. > **tips** Common OAuth 2.0 libraries handle all of this out of the box: `openid-client` (Node.js), `requests-oauthlib` or `authlib` (Python), `league/oauth2-client` (PHP), Spring Security (Java). ## Handling 401s A `401` has two possible causes: the token has expired, or access has been revoked (application disconnected from the workspace). What to do: 1. On a `401`, drop the cached token, request a **new one once** and replay the request. This is safe: every write in the API is [idempotent](/en/article/idempotency). 1. If the `401` comes back with a fresh token, access has been **revoked**. Stop sending, keep the data queued and notify the workspace admin. > **warning** Never loop on `401`s: repeated token requests won't help if the application has been disconnected. ### Full example: cache and retry **Node.js** ```javascript const TOKEN_URL = process.env.DAFEXPERT_TOKEN_URL; const API_URL = 'https://api.stg.dafexpert.com/v1'; let cached = null; async function getToken() { if (cached && Date.now() < cached.expiresAt - 30_000) return cached.token; const res = await fetch(TOKEN_URL, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.DAFEXPERT_CLIENT_ID, client_secret: process.env.DAFEXPERT_CLIENT_SECRET, }), }); if (!res.ok) throw new Error('Token refused: ' + res.status); const body = await res.json(); cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 }; return cached.token; } async function callApi(method, path, body) { const send = async (token) => fetch(API_URL + path, { method, headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); let res = await send(await getToken()); if (res.status === 401) { cached = null; res = await send(await getToken()); if (res.status === 401) { throw new Error('Access revoked: contact the workspace admin'); } } return res; } ``` **Python** ```python import os, time, requests TOKEN_URL = os.environ["DAFEXPERT_TOKEN_URL"] API_URL = "https://api.stg.dafexpert.com/v1" _cache = {"token": None, "expires_at": 0.0} def get_token(): if _cache["token"] and time.time() < _cache["expires_at"] - 30: return _cache["token"] r = requests.post(TOKEN_URL, data={ "grant_type": "client_credentials", "client_id": os.environ["DAFEXPERT_CLIENT_ID"], "client_secret": os.environ["DAFEXPERT_CLIENT_SECRET"], }, timeout=10) r.raise_for_status() body = r.json() _cache.update(token=body["access_token"], expires_at=time.time() + body["expires_in"]) return _cache["token"] def call_api(method, path, json=None): def send(token): return requests.request(method, API_URL + path, json=json, timeout=30, headers={"Authorization": f"Bearer {token}"}) res = send(get_token()) if res.status_code == 401: _cache["token"] = None res = send(get_token()) if res.status_code == 401: raise RuntimeError("Access revoked: contact the workspace admin") return res ``` **PHP** ```php token && time() < $this->expiresAt - 30) { return $this->token; } $ch = curl_init($this->tokenUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, ]), ]); $body = json_decode(curl_exec($ch), true); $this->token = $body['access_token']; $this->expiresAt = time() + $body['expires_in']; return $this->token; } private function send(string $method, string $path, ?array $json, string $token): array { $ch = curl_init($this->apiUrl . $path); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => $json === null ? null : json_encode($json), ]); $body = curl_exec($ch); return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), json_decode($body, true)]; } public function call(string $method, string $path, ?array $json = null): array { [$status, $body] = $this->send($method, $path, $json, $this->token()); if ($status === 401) { $this->token = null; [$status, $body] = $this->send($method, $path, $json, $this->token()); if ($status === 401) { throw new RuntimeException('Access revoked: contact the workspace admin'); } } return [$status, $body]; } } ``` ## What the token can and can't do - The token gives access to the `/v1/partners/*` routes, **within the workspace bound to your credentials** only. - It is refused on routes reserved for dafexpert users. Conversely, a user token is refused on partner routes. - A revocation by the workspace admin takes effect within 30 seconds at most, even for a token that is still valid. ## Next steps - [Error handling & status codes](/en/article/errors) : The error format and when to retry. - [Idempotency & retries](/en/article/idempotency) : Why replaying a request is safe. - [Create a company](/en/article/create-company) : Your first business call. --- # User sign-in (SSO) URL: https://developers.stg.dafexpert.com/en/article/sso > Let your users sign in once to access both dafexpert and your application, with OpenID Connect. With single sign-on (SSO), your users sign in **once** to access both dafexpert and your application. The session is shared: a cooperative manager signed in to your application opens dafexpert without typing their password again, and vice versa. SSO is based on **OpenID Connect** (OAuth 2.0 Authorization Code), with dafexpert's identity provider. > **info** SSO is **separate** from the partner API: you get a `client_id` dedicated to SSO, different from the one used for your server-to-server calls. Don't mix them up. ## What you provide Setup is done with the dafexpert team ([support@dafexpert.com](mailto:support@dafexpert.com)). Prepare: - your **post-login redirect URIs**, for example `https://app.acme-agri.com/*`; - your **post-logout redirect URIs**; - if you have one, your **front-channel logout URL**, for single logout (signing out of dafexpert also signs the user out of your application). ## What we send you | Item | Value | |---|---| | Issuer | `https://auth.stg.dafexpert.com/realms/dafexpert` (staging) | | Discovery | `https://auth.stg.dafexpert.com/realms/dafexpert/.well-known/openid-configuration` | | `client_id` | your application code, for example `acme-agri` | | `client_secret` | shown only once, when the client is created | | Flow | Authorization Code. PKCE (`S256`) is accepted and recommended, but optional. | | Scopes | `openid email profile` | > **warning** The SSO client is **confidential**: its `client_secret` stays on your server. The code-for-tokens exchange happens server-side, never in the browser or the mobile app. ## The sign-in flow **1. Read the configuration** Fetch the issuer's OpenID configuration. It gives the authorization, token and logout URLs, and the signing keys. Don't hard-code these URLs. **2. Redirect the user** Send the user to `authorization_endpoint` with your `client_id`, your redirect URI, the scopes and a `state` (and preferably a PKCE `code_challenge`). **3. Exchange the code** dafexpert sends the user back to your redirect URI with a `code`. Your server exchanges it for an `id_token` and an `access_token` at `token_endpoint`. **4. Identify the user** Validate the `id_token` (signature, issuer, audience) and read the `email`: it is the login identifier, the same one you sent to `PUT /v1/partners/users/{id}`. **1. Discovery** ```bash curl https://auth.stg.dafexpert.com/realms/dafexpert/.well-known/openid-configuration ``` **2. Authorization** ```text GET {authorization_endpoint} ?response_type=code &client_id=acme-agri &redirect_uri=https://app.acme-agri.com/auth/callback &scope=openid%20email%20profile &state=8f3c1a... &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 ``` **3. Code exchange** ```bash curl -X POST "{token_endpoint}" \ -d grant_type=authorization_code \ -d code="$CODE" \ -d redirect_uri=https://app.acme-agri.com/auth/callback \ -d client_id=acme-agri \ -d client_secret="$SSO_CLIENT_SECRET" \ -d code_verifier="$CODE_VERIFIER" ``` **Node.js (openid-client)** ```javascript import * as client from 'openid-client'; const config = await client.discovery( new URL('https://auth.stg.dafexpert.com/realms/dafexpert'), 'acme-agri', process.env.SSO_CLIENT_SECRET, ); // Sign-in: redirect to dafexpert const codeVerifier = client.randomPKCECodeVerifier(); const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); const state = client.randomState(); const url = client.buildAuthorizationUrl(config, { redirect_uri: 'https://app.acme-agri.com/auth/callback', scope: 'openid email profile', code_challenge: codeChallenge, code_challenge_method: 'S256', state, }); // ... store codeVerifier and state in the session, then redirect to url // Callback: code exchange const tokens = await client.authorizationCodeGrant(config, currentUrl, { pkceCodeVerifier: codeVerifier, expectedState: state, }); const { email } = tokens.claims(); ``` ## Creating your users' accounts SSO doesn't create accounts. First create each user with `PUT /v1/partners/users/{id}`: their login account is created (or linked if one already exists for that email) and they get access to the listed companies. They can then sign in from your application or from dafexpert. See [Manage users](/en/article/manage-users). ## Choosing the application after sign-in A user who signs in **from dafexpert** and has access to your application picks which one to open: DAF Expert or your application. They can tick "Always open this application". A switcher in the dafexpert header then lets them move from one to the other. A user coming **from your application** is sent straight back to it, with no choice screen. ## Single logout For signing out on one side to sign the user out on the other: 1. send us your front-channel logout URL; 1. to sign out, redirect the user to `end_session_endpoint` (from the configuration) with one of your post-logout redirect URIs. ## Best practices - Use a proven OpenID Connect library rather than a home-made implementation. - Always check the `state` on callback, and use PKCE even though it is optional. - Identify the user by their `email` (or the `id_token` `sub`), never by a parameter of the callback URL. ## Next steps - [Manage users](/en/article/manage-users) : Create accounts and grant access to companies. - [Choose the right integration](/en/article/choose-integration) : Combine SSO and the partner API. --- # Error handling & status codes URL: https://developers.stg.dafexpert.com/en/article/errors > The HTTP status codes returned by the API, the error body format, partial results in batches and when to retry. The API uses standard HTTP status codes: `2xx` for success, `4xx` for a request you need to fix, `5xx` for an incident on our side. Every error comes with a JSON body holding a readable message and a stable code, `error_code`, that your code can rely on. ## Status codes | Code | Meaning | What to do | |---|---|---| | `200 OK` | Successful read, or update (for example a `PUT` on a company that already existed). | Mark as sent. | | `201 Created` | Resource created (company, user, tax document). | Mark as sent, keep the returned ID if useful. | | `202 Accepted` | Received and queued: events, bulk company creation. Processing happens in the background. | Read the result **per item** (see below), then track the status. | | `400 Bad Request` | Unreadable request, for example malformed JSON. | Fix the request. | | `401 Unauthorized` | Missing, expired or invalid token, or revoked access. | Request a new token **once** and replay. If the error persists, contact the workspace admin. | | `403 Forbidden` | The resource belongs to another workspace (company, user). | Don't resend. Check the identifier and report the issue. | | `404 Not Found` | Unknown resource in your workspace (event, batch, document, user). | Check the identifier. Don't retry in a loop. | | `409 Conflict` | Business conflict: NCC already used, email already taken, document being issued… | Read the message. Only "document being issued" should be retried as is (see below). | | `413 Payload Too Large` | Request body too large. | Split the payload into several batches. | | `422 Unprocessable Entity` | Invalid data or data refused by a business rule, with per-field details. | Fix the data. **Don't resend it unchanged.** | | `429 Too Many Requests` | Too many requests. | Retry later with increasing delays. See [Rate limiting](/en/article/rate-limiting). | | `500` / `502` / `503` / `504` | Incident or temporary unavailability. | Retry later with increasing delays. | ## Error format All errors share the same base shape: - `statusCode` (integer) : The HTTP status, repeated in the body. - `message` (string) : Readable message. For validation errors and business rules of the partner API, it is in French and can be shown to a user. - `error_code` (string) : Stable code for your logic (see the table below). It can be missing on errors raised before the API itself: unreadable JSON, oversized body, unexpected `500`. - `erreurs` (array) : For a validation `422`: the list of invalid fields, each with `champ` (field path, for example `adresse.ville` or `lignes.0.montant`) and `message`. ### Validation error (422) ```json { "statusCode": 422, "error_code": "validation_failed", "message": "Données invalides.", "erreurs": [ { "champ": "pays", "message": "Code pays ISO à deux lettres attendu (ex. CI)." }, { "champ": "debut_exercice", "message": "Date attendue au format AAAA-MM-JJ." } ] } ``` ### Business rule (409) ```json { "statusCode": 409, "message": "Le NCC 1234567A est déjà utilisé par une autre entreprise de l'espace.", "error_code": "conflict" } ``` ### Unknown resource (404) ```json { "statusCode": 404, "message": "Événement inconnu.", "error_code": "not_found" } ``` ### Authentication (401) ```json { "statusCode": 401, "message": "Unauthorized", "error_code": "unauthenticated" } ``` When the token is valid but the application has been disconnected from the workspace, the message says so: `Partner access is not connected or has been revoked.` ## `error_code` values | `error_code` | Status | Case | |---|---|---| | `validation_failed` | `422` | One or more invalid fields. Details in `erreurs`. | | `error` | `422` | Refused by a business rule (unknown country, legal form outside the catalog, fiscal year that cannot be opened, unknown role…). Details in `message`. | | `unauthenticated` | `401` | Missing, expired or invalid token, or revoked access. | | `forbidden` | `403` | Resource from another workspace. | | `not_found` | `404` | Unknown resource in your workspace. | | `conflict` | `409` | Business conflict. | | `too_many_requests` | `429` | Too many requests. | | `internal_error` | `5xx` | Incident on our side. | > **tips** Base your logic on the **HTTP status** and `error_code`, not on the `message` text, which may be reworded. New codes may appear: handle an unknown code according to its HTTP class. ## Partial results in batches Batch endpoints (`POST /v1/partners/events`, `POST /v1/partners/companies`) answer `202` **even if some items are refused**. Each item in the batch is validated on its own: an invalid item doesn't block the others. Always read the `results` array, item by item. ```json { "results": [ { "id": "PAY-2b77e0", "statut": "recu", "doublon": false, "version": 1 }, { "index": 1, "id": "DEP-33ab", "statut": "invalide", "erreurs": [{ "champ": "montant", "message": "montant (45000) différent de la somme des lignes (40000)." }] }, { "id": "AVA-11c0", "statut": "refuse", "erreur": "L'exploitation de cet événement est rattachée à un autre espace." } ] } ``` | Item status | Meaning | What to do | |---|---|---| | `recu` (or the current status) | Recorded. | Mark as sent, then track the status. | | `invalide` | Not recorded: invalid data. `index` gives its position in the batch. | Fix it, then resend that item alone. | | `refuse` | Not recorded: the company or the `id` belongs to another workspace. | Don't resend. Check the identifiers. | > **warning** An event sent **on its own** (not in a batch) that is invalid gets a `422` directly, not a `202` with an `invalide` item. ## When to retry | Situation | Retry? | How | |---|---|---| | Network failure, timeout, no response | Yes | Resend the **same** request, with the same identifiers. [Idempotency](/en/article/idempotency) prevents duplicates. | | `429`, `500`, `502`, `503`, `504` | Yes | Increasing delays with jitter (for example 1 s, 2 s, 4 s, 8 s…), with a cap on attempts. | | `401` | Once | With a fresh token. Then stop and notify the admin. | | `409` "document being issued" | Yes | After a few seconds, with the **same** `id`. | | Other `409`, `403`, `404`, `422` | No | Fix the cause first. Resending as is will return the same error. | ### Example: retry with exponential backoff **Node.js** ```javascript const RETRYABLE = new Set([429, 500, 502, 503, 504]); async function withRetry(doRequest, maxAttempts = 6) { for (let attempt = 1; ; attempt++) { let res; try { res = await doRequest(); } catch (networkError) { if (attempt >= maxAttempts) throw networkError; } if (res && !RETRYABLE.has(res.status)) return res; if (attempt >= maxAttempts) return res; const delay = Math.min(2 ** attempt * 500, 60_000) * (0.5 + Math.random()); await new Promise((r) => setTimeout(r, delay)); } } ``` **Python** ```python import random, time, requests RETRYABLE = {429, 500, 502, 503, 504} def with_retry(do_request, max_attempts=6): for attempt in range(1, max_attempts + 1): try: res = do_request() if res.status_code not in RETRYABLE: return res except requests.ConnectionError: if attempt == max_attempts: raise if attempt == max_attempts: return res time.sleep(min(2 ** attempt * 0.5, 60) * (0.5 + random.random())) ``` ## Next steps - [Idempotency & retries](/en/article/idempotency) : Replay a request without creating duplicates. - [Rate limiting](/en/article/rate-limiting) : Volume best practices. - [Track events](/en/article/track-events) : Find rejections and their reasons. --- # Idempotency & retries URL: https://developers.stg.dafexpert.com/en/article/idempotency > Why replaying a call never creates a duplicate, which key each resource uses, and how resends with different content behave. Networks drop, servers restart, mobile apps send their operations when they get back online. The dafexpert API is designed so you can **resend without fear**: replaying a call never creates a duplicate. There is no `Idempotency-Key` header to manage: **your own identifier** for the resource is the key. That's why it must be stable. ## The key for each resource | Resource | Route | Key | Behavior on replay | |---|---|---|---| | Company | `PUT /v1/partners/companies/{external_id}` | `external_id` (URL) | The first call creates (`201`), later calls update (`200`). Concurrent calls on the same `external_id` create a single company. | | Bulk companies | `POST /v1/partners/companies` | each company's `external_id` | Each company is handled like the `PUT`: create or update. | | User | `PUT /v1/partners/users/{id}` | `id` (URL) | Create (`201`) then update (`200`). The `acces` list you send is the reference. | | Event | `POST /v1/partners/events` | the event's `id` | Recorded only once. A resend answers `doublon: true` with the current status. | | Purchase slip | `POST /v1/partners/companies/{external_id}/bordereaux` | the slip's `id`, per company | A replay returns the **same document**, with the same number and tax number (`200` instead of `201`). | | Invoice | `POST /v1/partners/companies/{external_id}/invoices` | the invoice's `id`, per company | Same: same invoice, same tax number. | ## Choosing good identifiers - **Stable**: the same object in your application always keeps the same identifier. Derive it from your internal key, for example `ACH-` followed by the weighing code, `PAY-` followed by the payment code. - **Unique** within your application, for a given resource type. - **Never generated randomly at send time**: an identifier generated on each attempt turns every replay into a new object, so a duplicate. - Allowed characters: letters, digits, `.` `_` `:` `-`, up to 100 characters. > **warning** The same operation sent under **two different `id`s** is seen as two operations. dafexpert flags probable duplicates, but your real protection is a stable `id`. ## Resending the same event ### Same content The event is not changed. The response says `doublon: true` and gives the **current status**, so you know where processing stands. ```json { "results": [ { "id": "PAY-2b77e0", "statut": "comptabilise", "doublon": true, "version": 1 } ] } ``` ### Different content: a new version If you resend a known `id` with **different** content, dafexpert records it as a **new version** of the event. What happens to it depends on the event's state: | Event state | Effect of the new version | Response | |---|---|---| | Not booked yet (`recu`, `en_attente_entreprise`, `en_attente_piece`, `rejete`) | It **replaces** the content and the event is reprocessed. | `contenu_different: true`, `version_appliquee: true`, `version` incremented | | Being processed (`en_cours`) | It is kept, and applied only if processing fails. | `version_appliquee: false` and a `message` | | Already booked or closed | It is kept for reference, **with no effect**, and flagged in dafexpert. | `version_appliquee: false` and a `message` | ```json { "results": [ { "id": "PAY-2b77e0", "statut": "comptabilise", "doublon": true, "version": 1, "contenu_different": true, "version_appliquee": false, "message": "Nouvelle version reçue après comptabilisation : elle n'est pas appliquée. Pour corriger, envoyer une annulation puis un nouvel événement." } ] } ``` Every version received is kept. Details in [Event versions](/en/article/event-versions). > **tips** To correct an event that is **already booked** with the v2 contract: send an `annulation` event that references the original `id`, then resend the correct operation with a **new** `id`. ### Duplicates within one batch If the same `id` appears twice in a batch, it is recorded only once: the second occurrence answers `doublon: true`. For bulk company creation, however, an `external_id` that appears twice in the batch makes the second entry `invalide`. ## Tax documents Issuing a purchase slip or an invoice is **synchronous**: the response contains the document, its number and its tax number. If the response is lost, resend the same request with the same `id`: you get the same document, without issuing a second one. - If FNE certification or booking had not completed, the replay **retries** them. - If the same document is already being issued (two concurrent calls), the second one gets a `409` "Pièce en cours d'émission : réessayer dans quelques secondes avec le même id." (document being issued, retry in a few seconds with the same id). Retry after a few seconds with the same `id`. > **warning** Never change a document's `id` to "force" a new issue: you would issue a second tax document, with its own tax number. ## Recommended resend strategy 1. Store the operation in a local queue **before** sending it, with its final identifier. 1. Send it. On success (`200`, `201`, `202` with an accepted item), mark it "sent". 1. On a network failure, a timeout, a `429` or a `5xx`: resend **the same request** later, with increasing delays. 1. On a `422` or an `invalide` item: fix the data before any resend. 1. Then track the events' status until `comptabilise`. See [Track events](/en/article/track-events). ## Next steps - [Error handling & status codes](/en/article/errors) : When to retry, when to fix. - [Event versions](/en/article/event-versions) : Replacement, kept versions, cancellation. - [Send events](/en/article/send-events) : Batches, statuses and first sends. --- # Webhooks URL: https://developers.stg.dafexpert.com/en/article/webhooks > Webhooks are not available yet: how to track your events' status today with cursor-based sync. > **info** **Coming soon.** dafexpert will notify you by webhook when an event is booked (`event.posted`) or rejected (`event.rejected`). This feature is not available yet. ## In the meantime: cursor-based sync Today, your application polls dafexpert. The `GET /v1/partners/events` route returns the events **modified** since your last pass, oldest first. This way you get every status change, without missing any and without duplicates. 1. On the first call, don't send a cursor. 1. Process the `evenements` you receive (status, document produced, rejection reason). 1. Keep the `suivant` value and pass it as `apres` on the next call. 1. When `suivant` is `null`, you have read everything: wait before the next pass. **cURL** ```bash curl "https://api.stg.dafexpert.com/v1/partners/events?limite=100&apres=$CURSOR" \ -H "Authorization: Bearer $TOKEN" ``` **Node.js** ```javascript async function sync(store) { let cursor = await store.readCursor(); for (;;) { const qs = new URLSearchParams({ limite: '500' }); if (cursor) qs.set('apres', cursor); const res = await callApi('GET', '/partners/events?' + qs); const page = await res.json(); for (const e of page.evenements) await store.update(e.id, e.statut, e); if (page.suivant) { cursor = page.suivant; await store.saveCursor(cursor); } if (!page.suivant) return; } } ``` **Response** ```json { "evenements": [ { "id": "PAY-2b77e0", "type": "paiement_producteur", "exploitation": "COOP-SOUBRE", "date": "2026-10-13", "montant": 1035000, "devise": "XOF", "statut": "comptabilise", "erreur": null, "ecriture": "BQ-2026-000187", "recu_le": "2026-10-13T09:12:04.000Z", "mis_a_jour_le": "2026-10-13T09:12:07.000Z", "version": 1 } ], "suivant": "MjAyNi0xMC0xM1QwOToxMjowNy4wMDBafDk..." } ``` ## Best practices - **Save the cursor** after each page you process: if you stop, you resume exactly where you left off. - **Treat the cursor as an opaque value**: don't build it or decode it. - To follow problems only, filter by status: `statut=rejete`. - Tune the sync frequency to your needs (for example every few minutes) rather than polling each event one by one. - For a single event, `GET /v1/partners/events/{id}` gives its detailed state. ## Next steps - [Track events](/en/article/track-events) : The full sync tutorial. - [GET /v1/partners/events](/en/article/list-events) : Parameters and response format. --- # Supported use cases URL: https://developers.stg.dafexpert.com/en/article/use-cases > The integrations the dafexpert API supports today: cooperative management, field apps, invoicing and AI assistants. dafexpert keeps the official books of each company and issues its tax documents. Your application remains the business software: it records operations (weighings, purchases, payments, sales, payroll) and sends them to dafexpert, which creates the documents, certifies them with the FNE when the law requires it, and posts them to the ledger. This page helps you find the use case closest to yours and the tutorials to follow. Every use case relies on the same routes and the same [credentials](/en/article/credentials). ## Overview | Use case | What you send | What dafexpert produces | Tutorials | |---|---|---|---| | Cooperative and agricultural supply chain management | Companies, users, accounting events | Books for each cooperative, documents, journal entries | [Create a company](/en/article/create-company), [Send events](/en/article/send-events) | | Field app (mobile, offline) | Farm-gate purchases, payments, advances, expenses | Certified purchase slips, settlements, journal entries | [Issue an FNE document](/en/article/fne-documents), [Track processing](/en/article/track-events) | | Invoicing or sales tool | Customer invoices | Numbered, FNE-certified, posted invoice with PDF | [Issue an FNE document](/en/article/fne-documents) | | AI assistant (Claude…) | Nothing: the user signs in | Read access to their companies: trial balance, P&L, invoices, cash | [Connect an AI assistant](/en/article/connect-ai-assistant) | ## Cooperative and agricultural supply chain management This is the API's original use case: a federation (for example an interprofessional body or a supply chain programme) follows cooperatives and their producers in your software, and wants up-to-date SYSCOHADA books for each of them. ### Typical flow - **Once**: create each cooperative, union or tracked farmer with `PUT /v1/partners/companies/{external_id}`, or in bulk during the initial migration with `POST /v1/partners/companies`. - **Once per person**: give managers and accountants access with `PUT /v1/partners/users/{external_id}`. They then sign in through [single sign-on](/en/article/sso). - **Every day**: send each validated operation as an [event](/en/article/event-catalog) (producer payment, advance, input transfer, supplier purchase, expense, payroll, membership fee, shares). - **Continuously**: sync statuses with `GET /v1/partners/events` to show, inside your app, what is posted and what is a discrepancy. > **tips** A dafexpert workspace matches your customer (the federation). All the cooperatives you create with the same credentials live in it. See [Workspaces and companies](/en/article/workspaces-companies). ## Field apps A mobile app used by field agents records purchases from producers, often without network coverage. In Côte d'Ivoire, the producer must receive an FNE-certified **agricultural produce purchase slip** (bordereau d'achat). - When the weighing is validated, the app (or its server) calls `POST /v1/partners/companies/{external_id}/bordereaux`. The synchronous response contains the number, the tax number, the QR code and the PDF link to hand to the producer. - The producer payment (cash or mobile money), with any deductions, is then sent as a `paiement_producteur` event that references the slip. - Offline, the app keeps operations and sends them once the network is back, with their real date. The same `id` is never processed twice: see [Idempotency](/en/article/idempotency). ## Invoicing or sales tools Your tool manages sales (for example cocoa deliveries to an exporter). dafexpert issues the legal invoice: numbering, FNE certification, PDF and journal entry, in a single `POST /v1/partners/companies/{external_id}/invoices` call. The customer payment is then sent as an `encaissement_client` event. > **info** You no longer produce your own invoices or slips: the tax document comes from dafexpert, which avoids any double numbering. ## AI assistants dafexpert users can connect their AI assistant (Claude today) to their companies through the MCP server. The assistant reads the trial balance, income statement, invoices and cash position, only for the companies the user can access. The server is read-only. ## Not available yet - **Webhooks**: status changes are not pushed to your server yet. Meanwhile, use the cursor-based sync described in [Track event processing](/en/article/track-events). - **Credit notes on certified documents**: a dedicated route is planned. Meanwhile, credit notes for a certified invoice or slip are issued in dafexpert. - **Write access through the MCP server**: the tools exposed to AI assistants are read-only. > **note** Coming soon: these features will be announced in the Changelog. ## Tutorials - [Create a company](/en/article/create-company) : Cooperative, union, buyer or farmer. - [Send events](/en/article/send-events) : A farm-gate purchase, then its payment. - [Issue an FNE document](/en/article/fne-documents) : Certified purchase slips and invoices. - [Connect an AI assistant](/en/article/connect-ai-assistant) : The MCP server in Claude. --- # Create or update a company URL: https://developers.stg.dafexpert.com/en/article/create-company > Create a cooperative and one of its farmers with PUT /v1/partners/companies/{external_id}, then keep them up to date. ## Goal By the end of this tutorial, you will have created an Ivorian cooperative in dafexpert and a farmer attached to it. Each one gets its own books: a first fiscal year opened, with monthly periods, journals and a chart of accounts. You will also know how to update their record and read what is missing before the first tax document. Call this route **when a cooperative or a farmer is created** in your application, then **every time its identity changes**. It is idempotent: the first call creates the company (`201`), the next ones update it (`200`). ## Prerequisites - Partner credentials (`client_id` and `client_secret`) for your customer's workspace: see [Get your credentials](/en/article/credentials). - A valid access token: see [Authentication](/en/article/authentication). The examples use the `$TOKEN` variable. - A successful call to `GET /v1/partners/me`, which confirms the connection to the workspace is active. > **info** All URLs in this tutorial point to staging: `https://api.stg.dafexpert.com/v1`. ## Steps **1. Choose the external identifier** The `external_id` is the company's identifier **in your system**. It appears in the URL and acts as the key: dafexpert never creates two companies for the same `external_id`. Pick a stable value that will never change (UUID, internal code). Format: 1 to 100 characters among letters, digits and `. _ : -`. It is also the value of the `entreprise` field in your events. **2. Prepare the cooperative's record** Only `type`, `nom` and `pays` are required at creation. Still, send everything you know: tax and legal details are required before the first tax document. ```json { "version": 1, "type": "cooperative", "nom": "Société Coopérative Agricole de Soubré", "sigle": "SCAS", "pays": "CI", "ncc": "2034567K", "forme_juridique": "SCOOP-CA", "numero_immatriculation": "CI-SBR-2019-COOP-0142", "regime_fiscal": "Réel simplifié", "assujetti_tva": false, "centre_impots": "Centre des impôts de Soubré", "agrement_filiere": { "numero": "AGR-CC-2026-0457", "valide_jusqu_au": "2027-09-30" }, "capital_social": 5000000, "date_creation": "2019-03-12", "adresse": { "ligne1": "Quartier Commerce, lot 12", "ville": "Soubré", "commune": "Soubré" }, "localisation": { "region": "Nawa", "departement": "Soubré", "commune": "Soubré", "village": "Okrouyo", "gps": { "lat": 5.7856, "lng": -6.6083 } }, "etiquettes": { "filiere": "cacao", "zone": "Sud-Ouest", "projets": ["PRO-CACAO", "AGRI-2026"] }, "debut_exercice": "2026-01-01", "contact": { "nom": "Awa Koné", "email": "gerance@scas-soubre.ci", "telephone": "+225 07 08 12 34 56" } } ``` **3. Send the request** Call `PUT /v1/partners/companies/{external_id}` with your token. **cURL** ```bash curl -X PUT "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @coop-soubre.json ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE', { method: 'PUT', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(coopSoubre), }, ); const company = await res.json(); // 201 on creation, 200 afterwards ``` `201 Created` response: ```json { "external_id": "COOP-SOUBRE", "company_id": "ec6a85db-3f1e-4c55-9a0e-2b7d1f0c9a41", "statut": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 0, "a_completer": ["connexion_fne"] } ``` **4. Check the response** Check these fields and store `company_id` next to your `external_id`. | Field | What to check | |---|---| | `statut` | `cree` (created) on creation, `mis_a_jour` (updated) afterwards. | | `referentiel` | The accounting framework deduced from the country: `SYSCOHADA` in the OHADA zone, `PCG` in France. See [Accounting frameworks](/en/article/accounting-frameworks). | | `devise` | The company currency, deduced from the country (`XOF` for Côte d'Ivoire). Your amounts must use this currency. | | `evenements_liberes` | Number of events received **before** the company existed (status `en_attente_entreprise`) and released by its creation. | | `a_completer` | What is missing before the first tax document. Empty list: the company is ready. | Possible values of `a_completer`: | Value | Applies to | How to complete it | |---|---|---| | `ncc` | cooperative, union, buyer | Send the record again with `ncc`. | | `numero_immatriculation` | cooperative, union, buyer | Send the record again with `numero_immatriculation`. | | `regime_fiscal` | cooperative, union, buyer | Send the record again with `regime_fiscal`. | | `assujetti_tva` | cooperative, union, buyer | Send the record again with `assujetti_tva` (`true` or `false`). | | `connexion_fne` | company in Côte d'Ivoire | The company connects its FNE account in dafexpert (Settings). This is not done through the API. | | `exploitant.telephone` | farmer | Send the record again with `exploitant.telephone`. | **5. Create an attached farmer** A farmer tracked in dafexpert is a company of type `exploitant`. `parent_id` holds the `external_id` of their cooperative. `nom` holds the family name and the `exploitant` block the rest of their identity. By default, their books use the minimal cash-basis system (`smt`). ```bash curl -X PUT "https://api.stg.dafexpert.com/v1/partners/companies/EXPL-0457" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 1, "type": "exploitant", "nom": "KOUASSI", "pays": "CI", "forme_juridique": "Entreprenant", "parent_id": "COOP-SOUBRE", "exploitant": { "prenoms": "Yao Bernard", "telephone": "0707123456", "cni": "C0012345678", "id_producteur": "CCC-123456", "sexe": "M", "date_naissance": "1978-04-02", "superficie_ha": 4.5, "cultures": ["cacao", "hévéa"] }, "localisation": { "region": "Nawa", "village": "Okrouyo" } }' ``` ```json { "external_id": "EXPL-0457", "company_id": "5b2d0f7a-91c4-4f0e-b6d1-7a3e2c9f8b10", "statut": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 0, "a_completer": [] } ``` > **tips** Order does not matter: a `parent_id` that does not exist yet is accepted. If it exists, its type must follow the union → cooperative → farmer hierarchy. **6. Update the record** Call the same route with the same `external_id` every time something changes in your app. The response is `200` with `"statut": "mis_a_jour"`. A field you leave out is not modified. ```bash curl -X PUT "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "cooperative", "nom": "Société Coopérative Agricole de Soubré", "pays": "CI", "contact": { "nom": "Ibrahim Traoré", "email": "gerance@scas-soubre.ci", "telephone": "+225 05 44 21 09 87" } }' ``` ### What changes and what is frozen | Can change on every call | Frozen after creation | |---|---| | `nom`, `forme_juridique`, `ncc`, `numero_immatriculation`, `regime_fiscal`, `assujetti_tva` | `pays` (and therefore the currency and the framework) | | `adresse`, `contact` (the block you send replaces the previous one) | `debut_exercice` (the first fiscal year is already open) | | `parent_id`, `sigle`, `date_creation`, `localisation`, `centre_impots`, `agrement_filiere`, `capital_social`, `exploitant` block | `systeme_comptable` | | `etiquettes`: your tags are replaced, tags added in dafexpert are kept | | > **warning** `type`, `nom` and `pays` stay required on every call, including updates. ## Field reference Lengths are maximums, after leading and trailing spaces are removed. The full table is in [Field limits](/en/article/field-limits). | Field | Type | Required | Rule | |---|---|---|---| | `version` | integer | no | `1` (default). | | `type` | string | yes | `cooperative`, `union`, `acheteur` (buyer) or `exploitant` (farmer). | | `nom` | string | yes | 1 to 200 characters. For a farmer: the family name. | | `pays` | string | yes | Two-letter ISO code (`CI`, `SN`, `FR`…), among supported countries. | | `sigle` | string | no | Short name, 40 characters. | | `ncc` | string | before 1st document | Taxpayer number, 30 characters. Unique within the workspace. | | `forme_juridique` | string | no | Code from the country catalog (see below). 60 characters. | | `numero_immatriculation` | string | before 1st document | Trade or cooperative register number. 60 characters. | | `regime_fiscal` | string | before 1st document | Tax regime, 60 characters. | | `assujetti_tva` | boolean | before 1st document | VAT registered: `true` or `false`. | | `centre_impots` | string | no | Tax office, 120 characters. | | `agrement_filiere` | object | no | Supply chain licence: `numero` (1 to 60 characters), `valide_jusqu_au` (`YYYY-MM-DD`). | | `capital_social` | number | no | Share capital in currency units, zero or positive. | | `date_creation` | date | no | `YYYY-MM-DD`. | | `parent_id` | string | no | `external_id` of the parent: a cooperative's union, a farmer's cooperative. Replaces `cooperative_id`, which is still accepted. | | `adresse` | object | no | `ligne1` (200), `ville` (100), `commune` (100). | | `localisation` | object | no | `region`, `departement`, `commune`, `village` (100 each), `gps` (`lat` from -90 to 90, `lng` from -180 to 180). | | `etiquettes` | object | no | Up to 20 keys (60 characters), value is a string (100) or a list of 20 strings. Keys are normalized: lowercase, no accents, `_` between words. | | `exploitant` | object | no | `prenoms` (120), `telephone` (40), `cni` (40), `id_producteur` (60), `sexe` (`M` or `F`), `date_naissance`, `superficie_ha`, `cultures` (20 strings of 60). | | `systeme_comptable` | string | no | `normal` or `smt`. Defaults to `smt` for a farmer, `normal` otherwise. | | `debut_exercice` | date | no | The 1st of a month (`YYYY-MM-01`). Defaults to January 1st of the current year. The first fiscal year lasts 12 months. | | `contact` | object | no | `nom` (120), `email` (valid address), `telephone` (40). | ### Fields by type A field sent for a type that does not allow it is rejected (`422`, "Champ non prévu pour le type"). | Field | cooperative | union | acheteur | exploitant | |---|---|---|---|---| | `sigle`, `date_creation`, `numero_immatriculation`, `regime_fiscal`, `assujetti_tva`, `centre_impots`, `capital_social` | yes | yes | yes | no | | `agrement_filiere` | yes | no | yes | no | | `parent_id`, `cooperative_id` | a union | no | no | a cooperative | | `exploitant` block | no | no | no | yes | ### Accepted legal forms `forme_juridique` must be a code from the catalog of the country's zone. Case, accents and punctuation are tolerated (`scoop ca` becomes `SCOOP-CA`). When rejected, the `422` message lists the codes accepted for that country. | Zone | Codes | |---|---| | OHADA (Côte d'Ivoire, Senegal, Cameroon…) | `SARL`, `SARLU`, `SA`, `SAS`, `SASU`, `SNC`, `SCS`, `GIE`, `SCI`, `SCOOPS`, `SCOOP-CA`, `Entreprise individuelle`, `Entreprenant`, `Association`, `ONG`, `Fondation`, `Église / Congrégation` | | France | `SARL`, `EURL`, `SAS`, `SASU`, `SA`, `SNC`, `SCI`, `SELARL`, `GIE`, `Entreprise individuelle`, `Micro-entreprise`, `Association`, `Fondation`, `Église / Congrégation` | > **tips** Cooperative: `SCOOPS` (simplified cooperative society) or `SCOOP-CA` (with a board of directors). Farmer: `Entreprenant`. ## Errors | Code | Case | What to do | |---|---|---| | `422` | Invalid data: the body contains `erreurs`, one entry per field. Also: unknown country, legal form outside the catalog, `debut_exercice` not on the 1st of a month, `parent_id` of the wrong type, legacy `utilisateurs` field sent. | Fix and send again. | | `409` | NCC already used by another company in the workspace, or workspace without an active owner or administrator. | Check the NCC; for the workspace, contact its administrator. | | `403` | This `external_id` is attached to another workspace. | Do not retry: check which credentials you are using. | ```json { "statusCode": 422, "error_code": "validation_failed", "message": "Données invalides.", "erreurs": [ { "champ": "pays", "message": "Code pays ISO à deux lettres attendu (ex. CI)." }, { "champ": "agrement_filiere", "message": "Champ non prévu pour le type « union »." } ] } ``` Business refusals (legal form, NCC, workspace) return a `message` without an `erreurs` array. Messages are in French. See [Errors](/en/article/errors). ## Common pitfalls **Changing the external_id** A new `external_id` creates a new company, with new books. Always keep the same identifier for a given cooperative, even if its name changes. **Sending users in the company record** The `utilisateurs` field from early versions has been removed: it triggers a `422`. Create users after the company with [Manage users](/en/article/manage-users). **Wrong country at creation** The country sets the currency and the framework, then never changes, even if you send another `pays`. Check it before the first call and, if it is wrong, contact [support](/en/article/support). **Unknown fields** A field that is not part of the contract is ignored without error. Check field spelling (for example `assujetti_tva`, not `assujettie_tva`): a typo raises no error, the value is simply lost. **Fiscal year not starting in January** Set `debut_exercice` to the first day of the opening month (`2026-10-01` for an October to September season). A date in the middle of a month is rejected. ## Next steps 1. [Create companies in bulk](/en/article/bulk-companies) to migrate your portfolio. 1. [Manage users](/en/article/manage-users) for each company. 1. [Send accounting events](/en/article/send-events). Full route reference: [PUT /v1/partners/companies/{external_id}](/en/article/put-company). --- # Create companies in bulk URL: https://developers.stg.dafexpert.com/en/article/bulk-companies > Migrate a whole portfolio of cooperatives and farmers with POST /v1/partners/companies, then track the batch. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. ## Goal You are starting the integration and your customers already follow hundreds of cooperatives and farmers. Instead of calling `PUT /v1/partners/companies/{external_id}` once per company, you send them in batches of up to 500. dafexpert stores them immediately, creates them in the background and gives you a batch identifier to track progress. > **info** Each company is processed exactly like the single `PUT`: same fields, same checks, same result. For day-to-day changes, the `PUT` remains the normal route; batches are for migration. ## Prerequisites - A valid access token: see [Authentication](/en/article/authentication). - Having read [Create or update a company](/en/article/create-company): fields and per-type rules are the same. ## Steps **1. Prepare the batch** The body contains `entreprises`, a list of 1 to 500 companies. Each one carries, on top of the usual fields, its `external_id` (it is no longer in the URL). ```json { "version": 1, "entreprises": [ { "external_id": "COOP-SOUBRE", "type": "cooperative", "nom": "Société Coopérative Agricole de Soubré", "pays": "CI", "forme_juridique": "SCOOP-CA", "ncc": "2034567K" }, { "external_id": "EXPL-0457", "type": "exploitant", "nom": "KOUASSI", "pays": "CI", "forme_juridique": "Entreprenant", "parent_id": "COOP-SOUBRE", "exploitant": { "prenoms": "Yao Bernard", "telephone": "0707123456" } }, { "external_id": "COOP-DALOA", "type": "cooperative", "nom": "Coopérative Agricole de Daloa", "pays": "Côte d'Ivoire" } ] } ``` **2. Send the batch** Call `POST /v1/partners/companies`. The response comes back immediately with `202 Accepted`. **cURL** ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/companies" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @migration-batch-01.json ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/companies', { method: 'POST', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 1, entreprises }), }); const { lot, recues, invalides, results } = await res.json(); // 202 ``` ```json { "lot": "3b0c8f9e-6a2d-4e71-9f0b-2c5d8a1e7b44", "recues": 2, "invalides": 1, "results": [ { "index": 0, "external_id": "COOP-SOUBRE", "id": "a1f3c2d4-0b5e-4c6f-8a9b-1c2d3e4f5a6b", "statut": "recu" }, { "index": 1, "external_id": "EXPL-0457", "id": "b2e4d3c5-1c6f-4d7a-9b0c-2d3e4f5a6b7c", "statut": "recu" }, { "index": 2, "external_id": "COOP-DALOA", "statut": "invalide", "erreurs": [{ "champ": "pays", "message": "Code pays ISO à deux lettres attendu (ex. CI)." }] } ] } ``` **3. Read the immediate response** There is one result per company, in the order sent (`index` starts at 0). Keep the `lot` value. | Field | Meaning | |---|---| | `lot` | Batch identifier, to pass to the tracking route. `null` if no company was valid. | | `recues` | Valid companies, queued. | | `invalides` | Companies rejected on read: they are **not** stored. | | `results[].statut` | `recu` (queued) or `invalide` (with `erreurs`, one entry per field). | > **tips** Each company is validated on its own: an invalid company does not block the others. Fix it and send it again in a later batch, or with the single `PUT`. **4. Track the batch** Poll `GET /v1/partners/companies/lots/{lot}` every few seconds until `"termine": true`. ```bash curl "https://api.stg.dafexpert.com/v1/partners/companies/lots/3b0c8f9e-6a2d-4e71-9f0b-2c5d8a1e7b44" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "lot": "3b0c8f9e-6a2d-4e71-9f0b-2c5d8a1e7b44", "total": 2, "termine": true, "compteurs": { "recu": 0, "en_cours": 0, "traite": 2, "rejete": 0 }, "entreprises": [ { "index": 0, "external_id": "COOP-SOUBRE", "id": "a1f3c2d4-0b5e-4c6f-8a9b-1c2d3e4f5a6b", "statut": "traite", "essais": 1, "company_id": "ec6a85db-3f1e-4c55-9a0e-2b7d1f0c9a41", "resultat": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 0, "erreur": null, "traite_le": "2026-10-12T09:14:03.512Z" }, { "index": 1, "external_id": "EXPL-0457", "id": "b2e4d3c5-1c6f-4d7a-9b0c-2d3e4f5a6b7c", "statut": "traite", "essais": 1, "company_id": "5b2d0f7a-91c4-4f0e-b6d1-7a3e2c9f8b10", "resultat": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 3, "erreur": null, "traite_le": "2026-10-12T09:14:03.871Z" } ] } ``` **5. Handle each status** Once the batch is finished, each company is either `traite` (processed) or `rejete` (rejected). | Status | Meaning | What to do | |---|---|---| | `recu` | Queued, not processed yet. | Wait. | | `en_cours` | Being created. | Wait. | | `traite` | Company created (`resultat: cree`) or updated (`resultat: mis_a_jour`). `company_id`, `referentiel` and `devise` are filled in. | Store `company_id` next to your `external_id`. | | `rejete` | Business refusal, with the reason in `erreur`: unknown country, legal form outside the catalog, NCC already taken, `external_id` from another workspace… | Fix, then send the company again. | On a technical error, dafexpert retries automatically: the company stays `recu` or `en_cours`, `essais` (attempts) increases and `erreur` shows the last error encountered. This is not a refusal. A business refusal is never retried. ## Tracking route responses | Code | Case | |---|---| | `200` | The batch exists: progress as above. | | `404` | Unknown batch, or batch from another workspace. | | `422` | Malformed batch identifier (it must be a UUID). | ## Errors when sending | Code | Case | |---|---| | `202` | Batch accepted, even if some companies are `invalide`. | | `422` | The body itself is invalid: `entreprises` missing, empty or longer than 500 items. | | `401` | Token expired or access revoked: see [Authentication](/en/article/authentication). | ## Best practices 1. Split your portfolio into batches of 500 at most and send them one after the other. 1. Send unions first, then cooperatives, then farmers if you can. It is not mandatory: a `parent_id` not created yet is accepted. 1. Track each batch until `termine`, then handle the `rejete` ones before moving on. 1. Sending a batch again is safe: a company that already exists is simply updated (`resultat: mis_a_jour`). ## Common pitfalls **The same external_id twice in a batch** The second occurrence comes back as `invalide` ("présent plusieurs fois dans le lot") and is not stored. Deduplicate before sending. **Treating recu as created** `recu` only means "stored in the queue". The company exists in dafexpert only once it is `traite`. Send its users only then; its events, however, can go first: they wait for the company with status `en_attente_entreprise`. **Forgetting invalid companies** Companies marked `invalide` in the immediate response do not appear in the batch tracking, since they were never stored. Keep the `POST` response to fix them. References: [POST /v1/partners/companies](/en/article/post-companies) and [GET /v1/partners/companies/lots/{lot}](/en/article/get-company-lot). --- # Manage users URL: https://developers.stg.dafexpert.com/en/article/manage-users > Give a manager or an accountant access to their companies with PUT /v1/partners/users/{external_id}, then remove it with DELETE. ## Goal When you create a user in your application (cooperative manager, accountant, farmer), you also create them in dafexpert with a single call. dafexpert creates their sign-in account, or attaches the one they already have, and gives them access to the companies you list. The user can then sign in right away, from your application through [single sign-on](/en/article/sso) or directly on dafexpert. ## Prerequisites - The companies involved already exist: see [Create or update a company](/en/article/create-company). A user referencing an unknown company is rejected. - A valid access token: see [Authentication](/en/article/authentication). ## Steps **1. Choose the external identifier** `{external_id}` is the user's identifier **in your system** (1 to 100 characters among letters, digits and `. _ : -`). The call is idempotent: calling the route again with the same identifier updates the user. **2. Prepare the record** `email` is the only required field: it is the sign-in identifier. `acces` lists the companies (by their `external_id`) and the user's role on each one. ```json { "version": 1, "email": "awa.kone@scas-soubre.ci", "prenom": "Awa", "nom": "Koné", "telephone": "+225 07 08 12 34 56", "mot_de_passe": "Cacao-Soubre-2026!", "mot_de_passe_temporaire": true, "acces": [ { "exploitation": "COOP-SOUBRE", "role": "dirigeant" }, { "exploitation": "EXPL-0457", "role": "lecture-seule" } ] } ``` **3. Send the request** Call `PUT /v1/partners/users/{external_id}`. **cURL** ```bash curl -X PUT "https://api.stg.dafexpert.com/v1/partners/users/USR-AKONE" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @awa-kone.json ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/users/USR-AKONE', { method: 'PUT', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(awaKone), }); const user = await res.json(); // 201 on creation, 200 afterwards ``` `201 Created` response: ```json { "external_id": "USR-AKONE", "user_id": "7c1e9a52-4b0d-4f3a-8e6c-5d2b1a0f9e83", "statut": "cree", "compte": "cree", "mot_de_passe": "defini", "acces": [ { "exploitation": "COOP-SOUBRE", "company_id": "ec6a85db-3f1e-4c55-9a0e-2b7d1f0c9a41", "role": "dirigeant" }, { "exploitation": "EXPL-0457", "company_id": "5b2d0f7a-91c4-4f0e-b6d1-7a3e2c9f8b10", "role": "lecture-seule" } ], "acces_retires": 0 } ``` **4. Check the response** The `compte` and `mot_de_passe` fields tell you what actually happened on the sign-in side. | Field | Values | Meaning | |---|---|---| | `statut` | `cree`, `mis_a_jour` | First call for this `external_id`, or an update. | | `compte` | `cree`, `existant` | `cree`: dafexpert created the sign-in account, active, with the email treated as verified. `existant`: an account already existed for this email and was attached. | | `mot_de_passe` | `defini`, `ignore`, `null` | `defini`: your password was applied. `ignore`: the account already existed, its password is never changed. `null`: you did not send one. | | `acces` | list | Access in place after the call, with the dafexpert `company_id`. | | `acces_retires` | number | Access entries removed because they are no longer in your list. | **5. Change access** The `acces` list is **the reference**: on every call, the user's access to the companies you created is aligned with the list. To remove access to a company, send the record again without it. ```bash curl -X PUT "https://api.stg.dafexpert.com/v1/partners/users/USR-AKONE" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "awa.kone@scas-soubre.ci", "prenom": "Awa", "nom": "Koné", "acces": [{ "exploitation": "COOP-SOUBRE", "role": "dirigeant" }] }' ``` ```json { "external_id": "USR-AKONE", "user_id": "7c1e9a52-4b0d-4f3a-8e6c-5d2b1a0f9e83", "statut": "mis_a_jour", "compte": "cree", "mot_de_passe": null, "acces": [ { "exploitation": "COOP-SOUBRE", "company_id": "ec6a85db-3f1e-4c55-9a0e-2b7d1f0c9a41", "role": "dirigeant" } ], "acces_retires": 1 } ``` **6. Remove the user** When the user leaves the cooperative, call `DELETE /v1/partners/users/{external_id}`. All their access to the companies you created is removed. Their sign-in account is kept, since they may have other access in dafexpert. ```bash curl -X DELETE "https://api.stg.dafexpert.com/v1/partners/users/USR-AKONE" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "external_id": "USR-AKONE", "acces_retires": 1 } ``` A new `PUT` with the same `external_id` attaches them again. ## Field reference | Field | Type | Required | Rule | |---|---|---|---| | `version` | integer | no | `1` (default). | | `email` | string | yes | Valid email address. Sign-in identifier. | | `prenom`, `nom` | string | no | First and last name, 80 characters each. | | `telephone` | string | no | 40 characters. | | `mot_de_passe` | string | no | 8 to 128 characters, and compliant with dafexpert's security policy. Applied only to accounts you created. Never stored by dafexpert. | | `mot_de_passe_temporaire` | boolean | no | `true`: the user must change it at first sign-in. Defaults to `false`. | | `acces` | list | no | Up to 200 items. Empty list by default. | | `acces[].exploitation` | string | yes | `external_id` of a company you created in this workspace. | | `acces[].role` | string | no | Role from the dafexpert catalog, 40 characters. Defaults to `collaborateur`. | ### Roles | Role | Access on the company | |---|---| | `dirigeant` | Manager of **this** company, full access. | | `admin` | Full access to all features and to management. | | `comptable` | Keeps the books, accesses statements and exports. | | `expert-comptable` | Accounting firm access: review and closings. | | `collaborateur` | Limited access (default). | | `lecture-seule` | Read only, no changes. | > **info** Whatever the role, the user stays a **plain member** of the workspace: they never become its owner or administrator, and only see the companies in their `acces` list. dafexpert's onboarding flow is marked as completed: they land directly in their company. ## Errors | Code | Case | What to do | |---|---|---| | `422` | Invalid data, unknown company (create it first), unknown role, password rejected by the security policy. | Fix and send again. If the account had just been created, it is deleted: retrying is safe. | | `409` | Email already used by another account (email change), or account already attached to another user of your application. | Check that you do not have two `external_id` values for the same person. | | `403` | This user is attached to another workspace. | Do not retry: check which credentials you are using. | | `404` | `DELETE` of an unknown user. | Nothing to do. | | `503` | The account service is temporarily unavailable. | Retry later, with an increasing delay. | ## Common pitfalls **Sending a partial access list** A call whose `acces` only contains a new company **removes** the others. Always send the full list of the user's companies. **Relying on the password for an existing account** If the email already has a dafexpert account (`compte: existant`), its name and password are never changed (`mot_de_passe: ignore`). The user keeps their current password. **Creating the user before the company** A company from a batch only exists once `traite`. Before that, the call fails with `422` ("Exploitation(s) inconnue(s)"). **Logging the password in clear text** dafexpert never stores the password, but it travels in your request. Exclude the `mot_de_passe` field from your own logs. ## Next steps 1. Set up [single sign-on (SSO)](/en/article/sso) so your users move from your app to dafexpert without signing in again. 1. [Send accounting events](/en/article/send-events). References: [PUT /v1/partners/users/{external_id}](/en/article/put-user) and [DELETE /v1/partners/users/{external_id}](/en/article/delete-user). --- # Send accounting events URL: https://developers.stg.dafexpert.com/en/article/send-events > A complete flow: a season advance, a cocoa purchase from a producer, then a mobile money payment that deducts the advance. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. ## Goal An event describes an operation validated in your application: a payment, an advance, an expense, a payroll run… dafexpert receives it, creates the matching business document (settlement, supplier invoice, journal entry) and posts it in the company concerned. In this tutorial, you follow a typical cocoa season case in Soubré: 1. In June, the cooperative COOP-SOUBRE pays a **200,000 FCFA** season advance in cash to producer Yao Bernard KOUASSI. 1. On October 12, it buys **1,200 kg** of cocoa from him at **1,100 FCFA/kg**, i.e. **1,320,000 FCFA**. 1. On October 13, it pays him through Orange Money, deducting the advance: **1,120,000 FCFA** paid out. > **info** The purchase from the producer (`achat_produit`) is no longer an event: it is a **tax document** issued synchronously through the purchase slip route, so the certified slip can be handed to the producer right away. Step 2 issues it; the payment in step 3 references it by its `id`. ## Prerequisites - The cooperative exists: see [Create or update a company](/en/article/create-company). Here, `COOP-SOUBRE`, in Côte d'Ivoire (currency `XOF`). - To issue the slip in Côte d'Ivoire, the cooperative has connected its FNE account in dafexpert (`a_completer` no longer contains `connexion_fne`). - A valid access token: see [Authentication](/en/article/authentication). ## Steps **1. Send the season advance** Every event carries `"version": 2`, an `id` that is stable and unique in your system, its `type`, the operation `date`, the `entreprise` that performs it (your `external_id`) and the `montant` (amount) in whole FCFA. The `tiers` is the counterparty: here, the producer. **cURL** ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/events" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 2, "id": "AVA-11c0", "type": "avance_producteur", "date": "2026-06-02", "entreprise": "COOP-SOUBRE", "tiers": { "role": "producteur", "id": "PROD-8c41d0", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao Bernard", "telephone": "0707123456", "cni": "C0012345678" }, "montant": 200000, "devise": "XOF", "mode": "especes", "caisse": "CAISSE-SOUBRE-1", "remboursement": "retenue_sur_achats", "analytique": { "campagne": "2026-2027" } }' ``` `202 Accepted` response: the event is stored and will be posted in the background. ```json { "results": [ { "id": "AVA-11c0", "statut": "recu", "doublon": false, "version": 1 } ] } ``` dafexpert records the advance entry: 4091 supplier advances on the debit side, cash on the credit side (default SYSCOHADA accounts). **2. Issue the purchase slip** When the weighing is validated, issue the slip with `POST /v1/partners/companies/{external_id}/bordereaux`. The response is synchronous: number, FNE tax number, QR code and PDF link. Field details are in [Issue an FNE document](/en/article/fne-documents). ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "ACH-6f1c2a9e", "date": "2026-10-12", "tiers": { "role": "producteur", "id": "PROD-8c41d0", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao Bernard", "telephone": "0707123456" }, "lignes": [{ "produit": "CACAO", "qualite": "grade_1", "quantite_pesee": 1250, "refaction_pct": 4, "quantite": 1200, "unite": "kg", "nombre_sacs": 20, "humidite_pct": 7.5, "prix_unitaire": 1100, "montant": 1320000 }], "montant": 1320000, "paiement_prevu": "mobile_money", "reference_origine": "L-S12/10/2026", "analytique": { "campagne": "2026-2027", "parcelle": "SURF-2231" } }' ``` ```json { "id": "ACH-6f1c2a9e", "type": "bordereau_achat", "piece_id": "0f9d2c1b-7a6e-4d5c-8b3a-2e1f0d9c8b7a", "statut": "certifie", "numero": "BA-ACH-6f1c2a9e", "numero_fiscal": "FNE-2026-000184512", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-6f1c2a9e/pdf", "comptabilisation": "comptabilise", "montant": 1320000, "miroir": { "entreprise": "EXPL-0457", "statut": "comptabilise" } } ``` The slip is posted in the cooperative's books (601 Purchases on the debit side, 4011 producer on the credit side). Since the producer is also a tracked company (`tiers.entreprise`), the mirror sale is created in their books (`miroir` block). **3. Send the payment with a deduction** The payment references the paid slip in `lie_a` (gross amount allocated) and the repaid advance in `retenues` (deductions). `montant` is the **net amount actually paid**: sum of `lie_a` minus sum of `retenues`. For mobile money, `reference_transaction` is required. ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/events" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 2, "id": "PAY-2b77e0", "type": "paiement_producteur", "date": "2026-10-13", "entreprise": "COOP-SOUBRE", "tiers": { "role": "producteur", "id": "PROD-8c41d0", "nom": "KOUASSI", "prenoms": "Yao Bernard", "telephone": "0707123456" }, "lie_a": [{ "id": "ACH-6f1c2a9e", "montant": 1320000 }], "retenues": [{ "nature": "avance", "lie_a": "AVA-11c0", "montant": 200000 }], "montant": 1120000, "mode": "mobile_money", "operateur": "orange_money", "reference_transaction": "MP261013.1542.A12345", "caisse": "CAISSE-SOUBRE-1" }' ``` ```json { "results": [ { "id": "PAY-2b77e0", "statut": "recu", "doublon": false, "version": 1 } ] } ``` dafexpert creates a 1,120,000 FCFA supplier settlement matched with the slip, and an offsetting entry for the deduction: 4011 producer on the debit side, 4091 advances on the credit side, for 200,000 FCFA. The slip becomes "paid". **4. Check processing** Read the event with `GET /v1/partners/events/{id}`. The `effets` (effects) block details, per company, the document created and its status. ```bash curl "https://api.stg.dafexpert.com/v1/partners/events/PAY-2b77e0" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "id": "PAY-2b77e0", "type": "paiement_producteur", "exploitation": "COOP-SOUBRE", "date": "2026-10-13", "montant": 1120000, "devise": null, "statut": "comptabilise", "erreur": null, "ecriture": null, "recu_le": "2026-10-13T08:42:10.114Z", "mis_a_jour_le": "2026-10-13T08:42:12.873Z", "version": 1, "effets": [ { "entreprise": "COOP-SOUBRE", "role": "payeur", "action": "creee", "statut": "comptabilise", "piece": { "type": "supplier_settlement", "numero": "MP261013.1542.A12345", "numero_fiscal": null }, "ecriture": null, "message": "Payé 1120000 FCFA (mobile money), retenues 200000 FCFA." } ] } ``` To follow thousands of events without reading them one by one, use cursor-based sync: see [Track event processing](/en/article/track-events). **5. Send in batches** You can send up to 500 events per call: `{ "version": 2, "events": [...] }`. The batch `version` applies to every event that does not carry its own. Each event is validated on its own: an invalid event does not block the others. ```json { "version": 2, "events": [ { "id": "DEP-33ab", "type": "depense", "date": "2026-10-12", "entreprise": "COOP-SOUBRE", "tiers": { "role": "prestataire", "id": "PRE-0042", "nom": "Transport Koné", "telephone": "0101010101" }, "lignes": [{ "nature": "transport", "libelle": "Transport cacao Okrouyo vers magasin", "montant": 45000 }], "montant": 45000, "mode": "especes", "caisse": "CAISSE-SOUBRE-1" }, { "id": "COT-0912", "type": "cotisation", "date": "2026-10-12", "entreprise": "COOP-SOUBRE", "tiers": { "role": "producteur", "id": "PROD-8c41d0", "nom": "KOUASSI" }, "nature": "cotisation_annuelle", "periode": "2026-2027", "montant": 10000, "mode": "mobile_money", "caisse": "CAISSE-SOUBRE-1" } ] } ``` ```json { "results": [ { "id": "DEP-33ab", "statut": "recu", "doublon": false, "version": 1 }, { "index": 1, "id": "COT-0912", "statut": "invalide", "erreurs": [ { "champ": "reference_transaction", "message": "reference_transaction obligatoire pour un paiement mobile money." } ] } ] } ``` ## Reading the response There is one result per event, in the order sent. | Field | Meaning | |---|---| | `statut` | **Current** status of the event (`recu`, `en_attente_entreprise`…), or `invalide`, or `refuse`. See [statuses](/en/article/track-events). | | `doublon` | `true`: this `id` was already stored. Nothing is created again: you can safely resend after a network failure. | | `version` | Number of the applied version (1 on first send). | | `contenu_different` | Present when this `id` was sent again with different content: it is a new version. | | `version_appliquee` | Did the new version replace the old one? `false` if the event was already posted or being processed, with the explanation in `message`. | | `erreurs` | Only for `invalide`: details per field. The event is not stored. | | `erreur` | For `refuse` (the company or the `id` belongs to another workspace), or reason of a rejection already known. | > **warning** An event sent **on its own** that is invalid gets a `422` for the whole call. In a batch, it comes back as `invalide` and the others are stored. ## Correcting an event ### Before posting Send the same `id` again with the corrected content. As long as the event is not posted (`recu`, `en_attente_entreprise`, `en_attente_piece` or `rejete`), the new version **replaces** the old one and the event is processed again (`version_appliquee: true`). ### After posting The new version is kept for reference and flagged in dafexpert, but **not applied** (`version_appliquee: false`). To correct it: send an `annulation` (cancellation) that references the event, then send the correct operation with a **new `id`**. ```json { "version": 2, "id": "ANN-PAY-2b77e0", "type": "annulation", "date": "2026-10-14", "entreprise": "COOP-SOUBRE", "montant": 0, "lie_a": ["PAY-2b77e0"], "motif": "Erreur sur le montant de la retenue" } ``` The cancellation reverses the original event's entries and deletes its settlements. The original event becomes `annule`. Details are in [v1 and v2 events](/en/article/event-versions). ## Common pitfalls **A random id** The `id` is the deduplication key. If it changes on every attempt (UUID generated at send time), a resend after a network failure creates a second event, which dafexpert blocks as a "probable duplicate". Derive the `id` from the source object (`PAY-` + payment code). **The payment arrives before the slip** The payment becomes `en_attente_piece` (field `en_attente_de`). It is released as soon as the slip is posted. After 48 hours without the document, it becomes `rejete` with the reason "Pièce citée introuvable après 48 h". **An uncertified slip** In Côte d'Ivoire, a slip is posted only once it is FNE-certified. Until then, the payment that references it waits. Replay the slip issuance with the same `id` to retry certification. **An amount that does not add up** `montant` is checked: sum of `lie_a` minus `retenues` for a payment, sum of `lignes` for an expense, and so on. Any gap, even one franc, returns a `422`. See [Amounts and currencies](/en/article/amounts-currencies). **Decimals in FCFA** In version 2, amounts are whole FCFA: `1320000`, never `1320000.00` as a string nor `1320000.5`. **Forgetting the cash register** `caisse` is required for every type that carries a payment (`paiement_producteur`, `avance_producteur`, `remboursement_producteur`, `encaissement_client`, `depense`, `cotisation`). ## Next steps 1. Browse the [event catalog](/en/article/event-catalog) for every type and its fields. 1. Set up [status sync](/en/article/track-events). Reference: [POST /v1/partners/events](/en/article/post-events). --- # Track event processing URL: https://developers.stg.dafexpert.com/en/article/track-events > Read the status of an event, sync every change with a cursor and handle rejections. ## Goal The response to `POST /v1/partners/events` comes back immediately (`202`): it confirms receipt, not posting, which happens in the background. This tutorial sets up reliable tracking in your application: knowing for each operation whether it is posted, waiting or rejected, and showing the reason to your users. > **info** Webhooks (`event.posted`, `event.rejected`) are planned but not available yet. Meanwhile, the cursor-based sync described here is the recommended method. See [Webhooks](/en/article/webhooks). ## Prerequisites - Having sent events: see [Send accounting events](/en/article/send-events). - A valid access token: see [Authentication](/en/article/authentication). ## Statuses | Status | Meaning | Can it still change? | |---|---|---| | `recu` | Received, waiting to be processed. | Yes | | `en_attente_entreprise` | The company (`entreprise` or `exploitation`) does not exist yet. The event is processed automatically once it is created. | Yes | | `en_attente_piece` | Version 2: a document or event referenced in `lie_a` or `retenues` has not arrived or is not posted yet. See `en_attente_de`. After 48 hours, the event becomes `rejete`. | Yes | | `en_cours` | Being processed: documents being created, FNE certification. | Yes | | `comptabilise` | All documents are created and posted. | Yes: `annule` after a cancellation | | `partiel` | Version 2: only some of the effects are posted. | Yes | | `rejete` | Rejected, with the reason in `erreur`. It can be retried in dafexpert once the cause is fixed, or replaced by a new version. | Yes | | `ignore` | Deliberately dismissed in dafexpert. | Rarely | | `annule` | Version 2: cancelled by an `annulation` event. | No | | `non_comptable` | Version 2: stored for information only (`recolte`, `inventaire`), never a discrepancy. | No | > **warning** Strictly speaking, no status is final: a `rejete` event can become `comptabilise` after a fix in dafexpert, a `comptabilise` event can become `annule`. Keep syncing. ## Steps **1. Read one event** `GET /v1/partners/events/{id}` returns the state of an event from **your** `id`. Handy for a detail screen or a one-off check. ```bash curl "https://api.stg.dafexpert.com/v1/partners/events/PAY-2b77e0" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "id": "PAY-2b77e0", "type": "paiement_producteur", "exploitation": "COOP-SOUBRE", "date": "2026-10-13", "montant": 1120000, "devise": null, "statut": "en_attente_piece", "erreur": "En attente de : ACH-6f1c2a9e.", "ecriture": null, "recu_le": "2026-10-13T08:42:10.114Z", "mis_a_jour_le": "2026-10-13T08:42:11.020Z", "version": 1, "en_attente_de": ["ACH-6f1c2a9e"] } ``` | Field | Description | |---|---| | `id`, `type`, `date`, `montant`, `devise` | What you sent (applied version). | | `exploitation` | `external_id` of the company (`entreprise` in version 2). | | `statut` | Current status, see the table above. | | `erreur` | Reason for a rejection or a wait, otherwise `null`. In French. | | `ecriture` | Version 1: document number of the journal entry. In version 2, look at `effets`. | | `recu_le`, `mis_a_jour_le` | Received and updated dates, ISO 8601 in UTC. | | `version` | Number of the applied version. | | `version_non_appliquee` | Present when a received version was not applied: `{ version, message }`. | | `en_attente_de` | Version 2: referenced `id` values still missing. | | `effets` | Version 2: one effect per company and role, with `action` (`creee` created, `rattachee` matched, `ecriture` entry), `statut` (`comptabilise`, `en_cours`, `rejete`), `piece` (`type`, `numero`, `numero_fiscal`), `ecriture` and `message`. | An `id` unknown in your workspace returns `404`. **2. Sync changes** `GET /v1/partners/events?apres=&limite=100` returns the events **changed** after the cursor, oldest first. On the first call, leave out `apres`: you start from the beginning. ```bash curl "https://api.stg.dafexpert.com/v1/partners/events?limite=100" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "evenements": [ { "id": "AVA-11c0", "type": "avance_producteur", "statut": "comptabilise", "mis_a_jour_le": "2026-06-02T07:10:04.221Z", "…": "…" }, { "id": "PAY-2b77e0", "type": "paiement_producteur", "statut": "comptabilise", "mis_a_jour_le": "2026-10-13T08:42:12.873Z", "…": "…" } ], "suivant": "MjAyNi0xMC0xM1QwODo0MjoxMi44NzNafDdmM2E…" } ``` | Parameter | Rule | |---|---| | `apres` | Opaque cursor received in `suivant` (next), passed back as is. 200 characters at most. A malformed cursor returns `422`. | | `limite` | Events per page: 1 to 500, 100 by default. | | `statut` | Optional: only return one status (for example `rejete`). | **3. Paginate and store the cursor** As long as `suivant` is not `null`, the page was full: call again right away with `apres=suivant`. When `suivant` is `null`, you have read everything: **keep the last cursor you used** and try again later. **Node.js** ```javascript // Persistent cursor (database, file…): null on the very first run. let cursor = await store.get('dafexpert_events_cursor'); async function syncEvents() { for (;;) { const url = new URL('https://api.stg.dafexpert.com/v1/partners/events'); url.searchParams.set('limite', '500'); if (cursor) url.searchParams.set('apres', cursor); const res = await callApi(url); // handles the token and 401 (see Authentication) const { evenements, suivant } = await res.json(); for (const e of evenements) { // Idempotent: the same event may come back several times. await upsertLocalStatus(e.id, e.statut, e.erreur, e.mis_a_jour_le, e.effets); } if (!suivant) return; // all read: keep the current cursor cursor = suivant; await store.set('dafexpert_events_cursor', cursor); } } setInterval(syncEvents, 60_000); ``` > **tips** When `suivant` is `null`, the events of the last page come back on the next call with the same cursor. Process them idempotently: update your local status by `id`, ignoring any version whose `mis_a_jour_le` is older than the one already stored. **4. Handle rejections** A `rejete` event carries its reason in `erreur` (and, in version 2, in `effets[].message`). Show it to your users and fix according to the cause. | Common reason | What to do | |---|---| | Currency different from the company's | Send the same `id` again with the right currency (or without `devise`). | | Inconsistent amount, missing data | Send the same `id` again, corrected: a rejected event accepts a new version. | | Referenced document not found after 48 hours | Issue the missing document (slip, invoice), then send the event again. | | Probable duplicate of event … | The same operation was already received under another `id`. If it really is a second operation, it must be confirmed in dafexpert. | | No open fiscal year, closed period, account not set up | Fixed in dafexpert by the company's accountant; the event is then retried in dafexpert. | | FNE not connected, phone or email missing | Complete the counterparty or company record, then replay the document. | To only fetch rejections, filter: `GET /v1/partners/events?statut=rejete`. Use a cursor separate from the full sync. ## Polling frequency - Full sync: every minute is enough for most integrations. - Right after sending, if your screen waits for the result: poll `GET /v1/partners/events/{id}` a few times, with an increasing delay (1 s, 2 s, 5 s…). - On a `429` or `5xx`, wait before retrying. See [Rate limiting](/en/article/rate-limiting). ## Common pitfalls **Starting from scratch every time** Without a stored cursor, every sync reads the whole history again. Store the cursor durably, next to your data. **Overwriting the cursor with null** When `suivant` is `null`, do not replace your cursor: you would lose your position and read everything again from the start. **Treating comptabilise as final** An `annulation` sent later turns the event into `annule`. Keep your sync running continuously. **Polling each event one by one** For bulk tracking, the list route is far cheaper than thousands of `GET /v1/partners/events/{id}` calls. References: [GET /v1/partners/events/{id}](/en/article/get-event) and [GET /v1/partners/events](/en/article/list-events). --- # Issue an FNE document URL: https://developers.stg.dafexpert.com/en/article/fne-documents > Issue an FNE-certified agricultural produce purchase slip and customer invoice, then retrieve their PDF. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. ## Goal In Côte d'Ivoire, every registered company must deliver documents certified by the tax authority's standardized electronic invoicing platform (FNE). With the API, dafexpert issues these documents for your companies in a single synchronous call: numbering, FNE certification, PDF with tax number and QR code, and posting. Two documents are available: - the **agricultural produce purchase slip** (bordereau d'achat), issued by the buyer (the cooperative) when it buys from a producer (FNE "purchase" model); - the **customer invoice**, issued by the company when it sells (cocoa sold to an exporter, inputs sold to a producer…). ## Prerequisites - The issuing company exists and its response no longer lists anything in `a_completer`: see [Create or update a company](/en/article/create-company). - It has connected its FNE account in dafexpert (establishment and point of sale filled in). This is done in dafexpert, by the company, not through the API. - A valid access token: see [Authentication](/en/article/authentication). > **info** Outside Côte d'Ivoire, the same routes issue the document without certification: its `statut` is `emise` (issued) and it is posted right away. ## Part 1: the purchase slip **1. Prepare the slip** The counterparty is the producer. The FNE needs an Ivorian 10-digit phone number and an email address. If the producer has no email, dafexpert uses the email of the buyer's contact. ```json { "id": "ACH-6f1c2a9e", "date": "2026-10-12", "tiers": { "role": "producteur", "id": "PROD-8c41d0", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao Bernard", "telephone": "0707123456", "cni": "C0012345678", "id_producteur": "CCC-123456" }, "lignes": [ { "produit": "CACAO", "qualite": "grade_1", "quantite_pesee": 1250, "refaction_pct": 4, "quantite": 1200, "unite": "kg", "nombre_sacs": 20, "humidite_pct": 7.5, "prix_unitaire": 1100, "montant": 1320000 } ], "montant": 1320000, "paiement_prevu": "mobile_money", "reference_origine": "L-S12/10/2026", "analytique": { "campagne": "2026-2027", "parcelle": "SURF-2231" } } ``` | Field | Required | Rule | |---|---|---| | `id` | yes | Document identifier in your system (1 to 100 characters, `. _ : -`). Idempotency key. | | `date` | yes | `YYYY-MM-DD`. | | `tiers` | yes | `role` must be `producteur`. `id` required, `nom` or `raison_sociale` required. `entreprise`: `external_id` of the farmer if tracked in dafexpert. | | `lignes` | yes | 1 to 200 lines. `produit` (60), `quantite` strictly positive, `prix_unitaire` and `montant` in whole FCFA. `montant` = `quantite` × `prix_unitaire`, within 1 franc. | | `lignes[].quantite_pesee`, `refaction_pct`, `nombre_sacs`, `humidite_pct`, `qualite`, `unite` | no | Traceability. `quantite` is the **net quantity paid**, after quality deduction. | | `montant` | yes | Exact sum of the line amounts. | | `paiement_prevu` | no | `especes` (cash), `mobile_money`, `credit`, `virement` (transfer) or `cheque`. | | `reference_origine`, `analytique`, `donnees` | no | Readable reference (120), axes `campagne`, `parcelle`, `groupement`, `produit` (60 each), free data. | **2. Issue the slip** Call `POST /v1/partners/companies/{external_id}/bordereaux`, where `{external_id}` is the buyer. The call is synchronous: allow a few seconds for certification. **cURL** ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @bordereau-ACH-6f1c2a9e.json ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux', { method: 'POST', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(bordereau), }, ); const piece = await res.json(); // 201 when issued, 200 on replay ``` `201 Created` response: ```json { "id": "ACH-6f1c2a9e", "type": "bordereau_achat", "piece_id": "0f9d2c1b-7a6e-4d5c-8b3a-2e1f0d9c8b7a", "statut": "certifie", "numero": "BA-ACH-6f1c2a9e", "numero_fiscal": "FNE-2026-000184512", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-6f1c2a9e/pdf", "comptabilisation": "comptabilise", "montant": 1320000, "miroir": { "entreprise": "EXPL-0457", "statut": "comptabilise" } } ``` **3. Read the response** The same structure is used for slips and invoices. | Field | Description | |---|---| | `id` | Your document identifier. | | `type` | `bordereau_achat` (purchase slip) or `facture` (invoice). | | `piece_id` | Document identifier in dafexpert. | | `statut` | `certifie` (FNE-certified), `non_certifie` (Côte d'Ivoire, certification not obtained yet) or `emise` (country without certification). | | `numero` | Document number. Slip: `BA-` followed by your `id`. Invoice: the company's legal numbering. | | `numero_fiscal` | Number assigned by the FNE, `null` until the document is certified. | | `qr` | FNE verification link (QR code content), `null` without certification. | | `lien_pdf` | API route that returns the PDF download link (step 4). | | `comptabilisation` | `comptabilise` (posted) or `non_comptabilise`. In Côte d'Ivoire, the document is posted only once certified. | | `montant` | Document total in FCFA (including tax for an invoice). | | `erreur` | Present if certification or posting failed: reason in French. | | `miroir` | Slip only, if the producer is a tracked company: the sale created in their books (`entreprise`, `statut`, `numero`, `erreur`). | **4. Get the PDF** Call the `lien_pdf` route with your token. It returns a download link valid for **5 minutes** (`expiresIn` in seconds). The certified PDF is served if it exists, otherwise the original. ```bash curl "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-6f1c2a9e/pdf" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "url": "https://…", "expiresIn": 300 } ``` > **tips** Do not store `url`: it expires. Store `lien_pdf` and ask for a fresh link when you display or print the slip. Hand the slip to the producer (printed, or sent by SMS from your app). ## Part 2: the customer invoice **5. Prepare the invoice** The counterparty is the customer. Line amounts are **excluding tax**; `montant` is the **total including tax**. `taux_tva` (VAT rate) defaults to 0. ```json { "id": "VTE-90aa13", "date": "2026-11-03", "echeance": "2026-11-18", "tiers": { "role": "client", "id": "CLI-0021", "raison_sociale": "Ivoire Cacao Export SA", "ncc": "3045678B", "telephone": "2721000000", "email": "achats@ivoirecacao-export.ci" }, "lignes": [ { "produit": "CACAO", "qualite": "grade_1", "quantite": 25000, "unite": "kg", "prix_unitaire": 1450, "taux_tva": 0, "montant": 36250000 }, { "description": "Sacs de conditionnement", "quantite": 100, "unite": "sac", "prix_unitaire": 2500, "taux_tva": 18, "montant": 250000 } ], "montant": 36545000, "mode_paiement": "virement", "reference_origine": "C-K112/11/2026" } ``` Total check: 36,250,000 + 250,000 excluding tax, plus 18% VAT on 250,000 (45,000), i.e. 36,545,000 FCFA including tax. | Field | Required | Rule | |---|---|---| | `id`, `date` | yes | Same as for the slip. | | `echeance` | no | Due date, `YYYY-MM-DD`. | | `tiers` | yes | `role` must be `client`. `id` required, `nom` or `raison_sociale` required. `pays` (two letters) if the customer is abroad. | | `lignes` | yes | 1 to 200 lines. `produit` or `description` (255) required, `quantite` strictly positive, `prix_unitaire` and `montant` (excluding tax) in whole FCFA, `taux_tva` from 0 to 100. | | `montant` | yes | Total including tax: sum of the amounts excluding tax plus each line's VAT rounded to the franc. | | `mode_paiement` | no | `especes`, `mobile_money`, `virement`, `cheque` or `carte` (card). | ### The FNE model is deduced from the customer | Customer | FNE model | Requirements | |---|---|---| | `raison_sociale` or `ncc` provided, in Côte d'Ivoire | B2B | 8-character NCC, email and Ivorian 10-digit phone number required. | | Individual or producer (neither `raison_sociale` nor `ncc`) | B2C | No extra requirement. | | `pays` other than `CI` | B2F | Customer abroad. | **6. Issue the invoice** Call `POST /v1/partners/companies/{external_id}/invoices`. The invoice is numbered, certified, marked as "sent" and posted (4111 customer, sales, output VAT). ```bash curl -X POST "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @invoice-VTE-90aa13.json ``` ```json { "id": "VTE-90aa13", "type": "facture", "piece_id": "6a4b2c1d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "statut": "certifie", "numero": "FAC-2026-0412", "numero_fiscal": "FNE-2026-000193877", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/invoices/VTE-90aa13/pdf", "comptabilisation": "comptabilise", "montant": 36545000 } ``` The PDF is fetched as for the slip, with `GET /v1/partners/companies/{external_id}/invoices/{id}/pdf`. The customer payment is then sent as an `encaissement_client` event that references `VTE-90aa13` in `lie_a`. ## Reading and replaying a document - `GET /v1/partners/companies/{external_id}/bordereaux/{id}` and `GET /v1/partners/companies/{external_id}/invoices/{id}` return the current state of the document, in the same format. - **Replaying** the issuance with the same `id` never creates a second document: the response is `200` with the same document and the same tax number. If the document was not certified or posted yet, the replay retries what is missing. - A concurrent call on the same `id`, during issuance, receives `409`: retry a few seconds later with the same `id`. ## When certification fails The document is still stored, with `statut: non_certifie`, `comptabilisation: non_comptabilise` and the reason in `erreur`. It shows up in the company's discrepancies in dafexpert. | Reason | What to do | |---|---| | FNE not connected for this company | The company connects its FNE account in dafexpert, then you replay the issuance. | | FNE connector: establishment and point of sale missing | To be completed in dafexpert by the company. | | Ivorian 10-digit phone number required | Fix the producer's or customer's phone number, then replay. | | 8-character NCC required (B2B invoice) | Fix the customer's `ncc`, then replay. | | Certification already in progress or outcome unknown | Check on the FNE platform before retrying: dafexpert never automatically resends a certification whose outcome is unknown. | | Refused by the FNE platform | The tax authority's reason is copied into `erreur`. | > **warning** Contact details of a known counterparty are never overwritten, only filled in when missing. If a wrong phone number was stored, it is fixed in dafexpert. ## Errors | Code | Case | |---|---| | `201` | Document issued. | | `200` | Replay: the document already existed for this `id`. | | `404` | Unknown company (create it first), unknown document for this company, or PDF not available yet. | | `409` | Document being issued, or workspace without an active owner or administrator. | | `422` | Invalid data: counterparty role, line totals, total including tax, date format… | ## Common pitfalls **Recomputing the price at send time** Send the price **applied** on the day of the weighing, frozen. A price recomputed at today's rate distorts the tax document, which can no longer be changed once certified. **Using a new id to retry** A new `id` issues a new document, with a new tax number and a new FNE certification. To retry, always keep the same `id`. **Decimals in amounts** Amounts are whole FCFA. A quantity can have decimals (1,200.5 kg); the line amount, however, is rounded to the franc. **Cancelling a certified document** A certified document is never deleted. The partner credit note route is not available yet (coming soon): credit notes are issued in dafexpert. ## Next steps 1. [Send the payment](/en/article/send-events) that settles the slip. 1. [Track event processing](/en/article/track-events). References: [POST bordereaux](/en/article/post-bordereau), [GET bordereau](/en/article/get-bordereau), [Slip PDF](/en/article/get-bordereau-pdf), [POST invoices](/en/article/post-invoice), [GET invoice](/en/article/get-invoice), [Invoice PDF](/en/article/get-invoice-pdf). --- # Connect an AI assistant (MCP) URL: https://developers.stg.dafexpert.com/en/article/connect-ai-assistant > Add dafexpert's MCP server to Claude to query your companies in natural language, read-only. ## Goal The Model Context Protocol (MCP) is an open standard that lets an AI assistant use external tools. dafexpert exposes an MCP server: once connected, the assistant can answer questions such as "What is the cash position of the Soubré cooperative?" or "List overdue invoices" from the real data of your companies. This tutorial is for dafexpert **users** (and for software vendors who want to document it for their customers). It needs no partner credentials: users sign in with their own dafexpert account. ## What the assistant can do | Tool | What it returns | |---|---| | `list_companies` | The companies the user can access, with currency, country, role and balance. | | `list_exercises` | A company's fiscal years: dates, open or closed status, accounting framework. | | `get_balance` | The trial balance of a fiscal year (debit, credit, balance for the current and previous year per account), optionally limited to a period. | | `get_balance_anomalies` | Review checks on the trial balance (abnormal balances, suspense accounts…) with a conclusion. | | `get_income_statement` | The income statement by line items of the accounting framework. | | `list_invoices` | Customer invoices, with an optional status filter. | | `get_invoice` | Invoice details: lines, totals, customer, payments. | | `get_treasury_overview` | Cash position: balance, inflows and outflows over 7 days, 30 days or 1 year, upcoming due dates. | > **info** The server is **read-only**: the assistant cannot create, change or delete anything. It only sees the companies the signed-in user can access, with that user's permissions. ## Prerequisites - A dafexpert account with access to at least one company. - An MCP-compatible assistant with OAuth sign-in: Claude (web, desktop app) or Claude Code. ## Connection details | Item | Value | |---|---| | Server URL (staging) | `https://api.stg.dafexpert.com/mcp` | | Server URL (production) | provided at go-live | | Transport | Streamable HTTP, `POST` requests only | | Authentication | OAuth 2.0, authorization code with PKCE (S256) | | OAuth client ID | `dafexpert-mcp` | | Client secret | none (public client) | The assistant discovers the authorization server by itself: on the first call without a token, the MCP server answers `401` with a `WWW-Authenticate` header pointing to `/.well-known/oauth-protected-resource/mcp`. > **warning** dafexpert does not support dynamic OAuth client registration. Always enter the client ID `dafexpert-mcp` in your assistant's advanced settings, otherwise sign-in fails. ## Steps in Claude (web and desktop) **1. Open connectors** In Claude, open **Settings**, then **Connectors**, and choose **Add custom connector**. **2. Enter the server** Name: `dafexpert`. URL: `https://api.stg.dafexpert.com/mcp`. Open **Advanced settings** and enter the OAuth client ID `dafexpert-mcp`. Leave the secret empty. **3. Sign in** Click **Connect**. The dafexpert sign-in page opens: sign in with your usual account, then accept the consent screen (read access to your DafExpert companies: accounting, invoices, cash). **4. Enable the connector in a conversation** In a new conversation, enable the `dafexpert` connector from the tools menu, then ask your question. ## Steps in Claude Code **1. Add the server** In a terminal: ```bash claude mcp add --transport http --client-id dafexpert-mcp dafexpert https://api.stg.dafexpert.com/mcp ``` **2. Authenticate** In Claude Code, type `/mcp`, select `dafexpert`, then authenticate. Your browser opens the dafexpert sign-in page; the callback goes to a local address (`localhost`). ## Other assistants For now, the `dafexpert-mcp` OAuth client only accepts Claude's callback addresses (claude.ai and claude.com), local addresses (`localhost`, `127.0.0.1`) and the MCP Inspector test tool (`http://localhost:6274`). | Assistant | Status | |---|---| | Claude (web, desktop) | Available | | Claude Code | Available | | MCP Inspector (testing) | Available | | ChatGPT | Coming soon | | Cursor | Coming soon | > **note** Another MCP client whose OAuth callback uses a local address may work, but has not been tested by dafexpert yet. ## Example questions - "List my companies and their currency." - "For Société Coopérative Agricole de Soubré, give me the 2026 income statement." - "Are there any anomalies in the current year's trial balance?" - "Which customer invoices are overdue, and for how much?" - "Which receipts are expected in the next 30 days?" ### Amounts Fields whose name ends with `Cents` are in hundredths of the company currency, including FCFA: `132000000` means 1,320,000 FCFA. The server tells the assistant, but double-check orders of magnitude. See [Amounts and currencies](/en/article/amounts-currencies). ## Troubleshooting **Sign-in fails before the dafexpert page** The client ID was not entered. Remove the connector, then add it again with `dafexpert-mcp` in the advanced settings. **401 error after a while** The session expired. Reconnect the connector (Claude) or authenticate again with `/mcp` (Claude Code). **A company is missing** The assistant only sees the companies of your account. Check in dafexpert that you have access to it, or ask the workspace administrator. **405 error when opening the URL in a browser** Expected: the MCP server only answers `POST` requests sent by an assistant. A `GET` request returns `405 Method not allowed`. ## Next steps 1. Browse the [MCP server reference](/en/article/mcp-overview): parameters and responses of each tool. 1. Explore other [use cases](/en/article/use-cases). --- # Workspaces and companies URL: https://developers.stg.dafexpert.com/en/article/workspaces-companies > How dafexpert organizes your customers: one workspace per organization, one company per set of books, and the link with your external identifiers. Two notions shape the whole API: the **workspace**, which says *who works*, and the **company**, which says *on which books*. Understanding them avoids most `403` and `404` errors. ## The workspace A workspace is an organization that is a dafexpert customer: for example a federation of cooperatives, an interprofessional body or a supply chain programme. It groups members and companies. - Your application is **connected to a workspace** by its administrator, who gives you a `client_id` and `client_secret` specific to that workspace: see [Get your credentials](/en/article/credentials). - The workspace is **deduced from your credentials**. It never appears in URLs or request bodies. - If your application serves several customers, you have one pair of credentials **per workspace**. Always use the right pair for each customer's data. `GET /v1/partners/me` confirms which workspace your credentials give access to: ```json { "partner": "gsmiles-agri", "workspaceId": "9d3f6c1a-2b4e-4f80-a1c7-5e6d8b9a0f12", "apiVersion": 1 } ``` ## The company A company has **its own books**: fiscal years and periods, journals, chart of accounts, counterparties, documents. A cooperative is a company; a farmer tracked in dafexpert is another one. | Type | Example | Possible parent | Default accounting system | |---|---|---|---| | `union` | Regional union of cooperatives | none | `normal` | | `cooperative` | Cocoa producers' cooperative society | a `union` | `normal` | | `acheteur` | Buyer, trader, exporter | none | `normal` | | `exploitant` | Individually tracked farmer | a `cooperative` | `smt` (minimal cash-basis system) | The union → cooperative → farmer hierarchy is used for membership and grouping. Each level keeps its own books: a union does not automatically consolidate its cooperatives' books. ### External identifier and dafexpert identifier You always refer to a company by **your** identifier, the `external_id`: in the URL of company and document routes, in the `entreprise` field (or `exploitation` in version 1) of events, in `acces[].exploitation` for users. dafexpert also returns its own identifier, `company_id`, useful when talking to support. | You send | dafexpert returns | |---|---| | `external_id`: `COOP-SOUBRE` | `company_id`: `ec6a85db-3f1e-4c55-9a0e-2b7d1f0c9a41` | > **warning** An `external_id` belongs to a single workspace. Using it with another workspace's credentials returns `403` for a company or a user, and `refuse` for an event. ## Users People you create with `PUT /v1/partners/users/{external_id}` become **plain members** of the workspace. They only see the companies listed in their access, with one role per company. They never become owners or administrators of the workspace: a cooperative manager therefore does not see the federation's other cooperatives. See [Manage users](/en/article/manage-users). Companies you create belong to the workspace. To create them, the workspace must have at least one active owner or administrator, otherwise the call returns `409`. ## Company or plain counterparty? A producer, a supplier or a customer appears in your events as a `tiers` (counterparty). It becomes: - a **counterparty** in the books of the company performing the operation: supplier for a producer or an input supplier, customer for a buyer. It is created on first send, then found again through its `tiers.id` (or its NCC); - **and**, if `tiers.entreprise` is set, a **company** of the workspace whose books receive the mirror document (for example the cocoa sale in the farmer's books, matching the cooperative's purchase slip). | Operation | In the company's books | In `tiers.entreprise` books | |---|---|---| | Purchase slip (purchase from a producer) | FNE-certified slip, purchase | Sale without FNE, backed by the slip's tax number | Contact details of a known counterparty are never overwritten by what you send: only missing information (phone, email, NCC) is filled in. ## In short - **One workspace** per customer organization, **one pair of credentials** per workspace. - **One company** per set of books, referred to by your `external_id`. - **Users** who are plain members, with per-company access. - **Counterparties** specific to each company, possibly linked to another company of the workspace. Read next: [Accounting frameworks](/en/article/accounting-frameworks) and [Create or update a company](/en/article/create-company). --- # Accounting frameworks URL: https://developers.stg.dafexpert.com/en/article/accounting-frameworks > SYSCOHADA, SYCEBNL and PCG: how a company's country sets its framework, its currency and the accounts used for your events. Each company keeps its books under an **accounting framework**: the chart of accounts, journals, financial statements and checks that apply. You never send account numbers: dafexpert picks the accounts based on the company's framework. ## The three frameworks | Framework | For whom | Zone | |---|---|---| | `SYSCOHADA` | Businesses (companies, cooperatives, farmers…). Normal system or minimal cash-basis system (SMT). | The 17 OHADA member states | | `SYCEBNL` | Non-profit entities: associations, NGOs, foundations, congregations. | OHADA | | `PCG` | French general chart of accounts. | France | ## How the framework is chosen When a company is created through the API, the framework is **deduced from the country** (`pays`): - `FR` → `PCG`; - any other supported country → `SYSCOHADA`. The `PUT /v1/partners/companies/{external_id}` response states it in `referentiel`. It is frozen afterwards, like the country and the currency. > **info** `SYCEBNL` cannot be obtained through the partner API today: it applies when a non-profit entity is created in dafexpert. Cooperatives, unions, buyers and farmers fall under `SYSCOHADA`. ### Supported countries | Zone | Countries (`pays`) | Currency | Framework | |---|---|---|---| | WAEMU (UEMOA) | `CI`, `BJ`, `BF`, `GW`, `ML`, `NE`, `SN`, `TG` | `XOF` (BCEAO CFA franc) | `SYSCOHADA` | | CEMAC | `CM`, `CF`, `CG`, `GA`, `GQ`, `TD` | `XAF` (BEAC CFA franc) | `SYSCOHADA` | | Other OHADA countries | `KM`, `GN`, `CD` | `KMF`, `GNF`, `CDF` | `SYSCOHADA` | | France | `FR` | `EUR` | `PCG` | A country missing from this list returns `422` ("Pays inconnu"). ## Normal system and minimal cash-basis system Under SYSCOHADA, the `systeme_comptable` field selects how the first fiscal year's financial statements are presented: - `normal`: full balance sheet, income statement, cash flow statement and notes. Default for a cooperative, a union or a buyer. - `smt`: minimal cash-basis system, lighter bookkeeping based on receipts and payments. Default for a farmer. The system is frozen at creation, with the first fiscal year. ## The first fiscal year On creation, dafexpert opens the company's first fiscal year, with its monthly periods, journals and chart of accounts: - it starts on `debut_exercice` (the 1st of a month), by default January 1st of the current year; - it lasts 12 months: `2026-10-01` gives a fiscal year from October 2026 to September 2027, handy to follow a crop season. An event dated outside any open fiscal year is rejected with an explicit reason. ## Accounts used for your events Events and documents are posted to default accounts specific to each framework. Each company can adapt them in dafexpert (Settings, posting accounts), for example to separate 601 (trading) from 602 (processing). | Usage | SYSCOHADA | PCG | |---|---|---| | Agricultural produce purchases (slip) | 601 | 607 | | Product sales | 702 | 701 | | Suppliers, producers | 4011 | 401 | | Customers | 4111 | 4111 | | Advances paid to producers | 4091 | 4091 | | Inputs purchased or advanced in kind | 604 | 602 | | Equipment (`immobilisation` line) | 241 | 215 | | Gross salaries | 661 | 641 | | Employer contributions | 664 | 645 | | Staff, wages payable | 422 | 421 | | Social security bodies | 431 | 431 | | State, taxes withheld | 447 | 447 | | Subscribed capital | 1013 | 1013 | | Subscribed capital, called | 4613 | 4562 | | Member fees | 7588 | 758 | | Expenses: transport | 618 | 6241 | | Expenses: fuel | 6042 | 6061 | | Expenses: labour | 637 | 621 | | Expenses: other | 605 | 6068 | > **tips** Cash (cash register, bank, e-money) uses the company's default treasury accounts. The `caisse` code you send is kept but not yet mapped to its own journal. ## What it means for you - Send **natures** and **products**, never accounts: the same event works whatever the framework. - Check `referentiel` and `devise` in the creation response: they drive everything that follows. - A "compte non paramétré" (account not set up) rejection is fixed in dafexpert by the company's accountant, not in your payload. Read next: [Amounts and currencies](/en/article/amounts-currencies) and [Event catalog](/en/article/event-catalog). --- # Event catalog URL: https://developers.stg.dafexpert.com/en/article/event-catalog > Every version 2 event type: what it is for, its key fields, its checks and what it creates in dafexpert. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. An event matches an operation validated in your application that has a financial impact. This catalog describes the types accepted by version 2 of the contract (`"version": 2`). For a complete sending example, see [Send accounting events](/en/article/send-events). > **info** A type missing from this catalog is rejected (`422`, or `invalide` in a batch). Version 1 accepts free-form types: see [v1 and v2 events](/en/article/event-versions). ## Overview | Type | Operation | What dafexpert creates | |---|---|---| | `paiement_producteur` | Payment of one or more purchases to a producer (cash, mobile money…), with optional deductions. | Supplier settlement matched with the slips, offsetting entry for deductions. | | `avance_producteur` | Season advance or credit paid in money. | Entry: 4091 advances on the debit side, cash on the credit side. | | `remboursement_producteur` | Repayment in money of an advance or inputs, outside a purchase payment. | Receipt matched with the referenced input invoices, otherwise an entry on 4091. | | `encaissement_client` | Payment received from a customer. | Customer settlement matched with the referenced invoices. | | `cession_intrant` | Inputs handed to a producer as an advance (to be repaid). | Advance-in-kind entry: 4091 on the debit side, 604 on the credit side. | | `achat_fournisseur` | Supplier invoice (inputs, goods, services, equipment). | Supplier invoice split by nature and posted, or matched with the invoice already present. | | `depense` | Cash expense: transport, labour, fuel… | Paid expense entry. | | `paie` | Closing of a payroll period. | Payroll entry: gross, employer contributions, net, social contributions, taxes. | | `souscription_parts` | Subscription and payment of cooperative shares. | Capital entry, then receipt of the paid-up part. | | `cotisation` | Membership fee or annual contribution. | Receipt and income. | | `inventaire` | Quantities counted at the end of a season or fiscal year. | Nothing for now: stored as `non_comptable`. | | `annulation` | Correction of an event already sent. | Reversal of the referenced event's entries and deletion of its settlements. | | `recolte` | Harvest record (traceability). | No document: status `non_comptable`. | ### Purchases and sales: documents, not events A purchase from a producer and a sale to a customer produce a **tax document** you must hand over right away. They therefore go through synchronous routes, not through events: | Operation | Route | Document | |---|---|---| | Purchase from a producer (e.g. `achat_produit`) | `POST /v1/partners/companies/{external_id}/bordereaux` | FNE-certified purchase slip | | Sale to a customer (e.g. `vente_produit`), inputs sold | `POST /v1/partners/companies/{external_id}/invoices` | FNE-certified customer invoice | Events then reference these documents by their `id`. See [Issue an FNE document](/en/article/fne-documents). ## Common envelope All types share these fields. | Field | Required | Rule | |---|---|---| | `version` | yes | `2` (or inherited from the batch). | | `id` | yes | Stable, unique identifier in your system (1 to 100 characters, `. _ : -`). Deduplication key. | | `type` | yes | A type from the catalog. | | `date` | yes | Operation date, `YYYY-MM-DD`. It sets the fiscal year and period. | | `entreprise` | yes | `external_id` of the company performing the operation: the payer, the buyer, the employer. | | `montant` | yes | Control total in whole FCFA, zero or positive. How it is computed depends on the type. | | `devise` | no | Three-letter ISO code. If sent, it must be the company currency. | | `reference_origine` | no | Readable reference in your system (voucher, lot number), 120 characters. | | `analytique` | no | `campagne` (season), `parcelle` (plot), `groupement` (group), `produit` (60 characters each). | | `donnees` | no | Free object, 32,000 characters at most once serialized. Kept, not used for posting. | ### The counterparty (tiers) | Field | Rule | |---|---| | `role` | `producteur` (producer), `fournisseur` (supplier), `client` (customer), `employe` (employee) or `prestataire` (service provider). | | `id` | Required. Counterparty identifier in your system: used to find it again from one call to the next. | | `nom` or `raison_sociale` | One of the two is required (200 characters). `prenoms` (first names): 120. | | `entreprise` | `external_id` of the counterparty if it is also a company of the workspace. | | `telephone` | 40 characters. Essential for a producer (FNE documents). | | `email` | Valid address. | | `cni`, `id_producteur` | ID card and producer ID, 40 and 60 characters. Recommended to recognize the same producer. | | `ncc` | Taxpayer number, 30 characters. Essential for a business customer. | | `pays` | Two-letter ISO code. | ### The payment Types that carry a money movement (`paiement_producteur`, `avance_producteur`, `remboursement_producteur`, `encaissement_client`, `depense`, `cotisation`) also have: | Field | Required | Rule | |---|---|---| | `mode` | yes | `especes` (cash), `mobile_money`, `virement` (transfer) or `cheque`. | | `caisse` | yes | Code of the cash register or payment account in your system (identifier format). | | `operateur` | no | Mobile money operator (`orange_money`, `mtn`, `wave`…), 40 characters. | | `reference_transaction` | if `mobile_money` | Transaction reference, 120 characters. | ## Fields and checks by type | Type | Specific fields | `montant` check | |---|---|---| | `paiement_producteur` | `tiers`, `lie_a` (1 to 200: `{ id, montant }`, the paid slips and the gross amount allocated), `retenues` (up to 50: `{ nature, lie_a, montant }`, nature `avance`, `intrants` or `cotisation`), payment. | Sum of `lie_a` minus sum of `retenues` (net paid). | | `avance_producteur` | `tiers`, `remboursement` (`retenue_sur_achats` or `especes`), payment. | Advance amount. | | `remboursement_producteur` | `tiers`, `lie_a` (repaid advances or input invoices), payment. | Sum of `lie_a`. | | `encaissement_client` | `tiers`, `lie_a` (paid invoices), payment. | Sum of `lie_a`. | | `cession_intrant` | `tiers`, `lignes` (`produit`, `quantite`, `unite`, `prix_unitaire`, `montant`), `a_credit`, `remboursement`, `financement`. | Sum of the lines. | | `achat_fournisseur` | `tiers`, `facture_fournisseur` (`numero` required, `numero_fiscal`, `date`, `echeance`), `lignes` (`nature`: `intrant`, `marchandise`, `service`, `immobilisation`; `montant` excluding tax; `taux_tva`; `duree_annees`), `piece_jointe` (URL). | Sum of the line amounts excluding tax. dafexpert computes the VAT. | | `depense` | `tiers` (optional), `lignes` (free-text `nature`, `libelle`, `montant`), `piece_jointe`, payment. | Sum of the lines. | | `paie` | `periode` (`YYYY-MM`), `lignes` (1 to 2,000: `employe { id, nom }`, `brut`, `retenues_salariales`, `charges_patronales`, `net`). | Sum of nets; each `net` = `brut` minus employee deductions. | | `souscription_parts` | `tiers`, `nombre_parts`, `valeur_nominale`, `montant_libere`, `mode` and `caisse` when a part is paid up. | `nombre_parts` × `valeur_nominale`; `montant_libere` at most equal. | | `cotisation` | `tiers`, `nature` (`droit_adhesion`, `cotisation_annuelle`, `autre`), `periode`, payment. | Fee amount. | | `inventaire` | `lignes` (`produit`, `entrepot`, `quantite`, `unite`). | Free (often 0). | | `annulation` | `lie_a` (exactly one event `id`), `motif` (reason, 300 characters). | Free (often 0). | | `recolte` | `tiers` (optional). | Free. | ### How expenses are classified For `depense`, each line's `nature` picks the expense account. Matching is done on French keywords: text containing transport, déplacement or fret goes to transport; carburant, essence, gasoil or gazole to fuel; main-d'œuvre, manœuvre, journalier or ouvrier to labour; everything else to other expenses. Without `piece_jointe`, the expense is flagged "Justificatif manquant" (missing receipt), without being blocked. ### How payroll deductions are split For `paie`, an employee deduction whose `nature` refers to social security (cnps, cnss, social, retraite, santé) goes to social security bodies; the others (income tax…) go to the State. ## References between events `lie_a` and `retenues[].lie_a` reference `id` values you already sent: - a slip or invoice issued through the document routes; - an event already posted (an advance, an input transfer). If one of them is missing, the event waits as `en_attente_piece` and the missing ones appear in `en_attente_de`. It is released when the document arrives, or becomes `rejete` after 48 hours. ```json { "version": 2, "id": "ENC-77d1", "type": "encaissement_client", "date": "2026-11-18", "entreprise": "COOP-SOUBRE", "tiers": { "role": "client", "id": "CLI-0021", "raison_sociale": "Ivoire Cacao Export SA" }, "lie_a": [{ "id": "VTE-90aa13", "montant": 36545000 }], "montant": 36545000, "mode": "virement", "reference_transaction": "VIR-SGCI-20261118-0042", "caisse": "BANQUE-SGCI" } ``` ## Current limitations - `inventaire` is stored without an entry: stock valuation at closing is not automated yet. - `cession_intrant` is handled as an advance in kind. **Sold** inputs go through the invoice route. - An `immobilisation` line in `achat_fournisseur` is posted as equipment; its fixed asset record must be completed in dafexpert. - Paying out salaries after a `paie` has no event type yet. - For a producer payment, the mirror receipt in the tracked farmer's books is not created automatically yet. - Cancelling an `achat_fournisseur` does not delete the supplier invoice created: a credit note must be entered in dafexpert. All maximum lengths are in [Field limits](/en/article/field-limits). --- # v1 and v2 events URL: https://developers.stg.dafexpert.com/en/article/event-versions > The two versions of the event contract, successive versions of the same event, per-company effects and discrepancies. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. The word "version" covers two different notions in the events API: - the **contract version** (`"version": 1` or `"version": 2`), which sets the shape of the event; - the **version of an event** (1, 2, 3…), which counts successive sends of the same `id` with different content. ## Contract v1 and contract v2 | | Version 1 | Version 2 | |---|---|---| | Company | `exploitation` | `entreprise` | | Types | Free-form (letters, digits, `_ . -`, 60 characters) | Closed catalog: see [Event catalog](/en/article/event-catalog) | | Amount | Up to two decimals, may be negative | Whole FCFA, zero or positive, checked per type | | Details | Free-form `quantites` and `donnees` | Typed fields: `tiers`, `lignes`, `lie_a`, `retenues`, payment… | | Posting | One journal entry, built from mappings configured in dafexpert for each type | Business documents (settlements, supplier invoices, entries) created in each company involved | | Tracking | `ecriture`: entry number | `effets` per company, `en_attente_de` | | Statuses | `recu`, `en_attente_entreprise`, `en_cours`, `comptabilise`, `rejete`, `ignore` | The same, plus `en_attente_piece`, `partiel`, `annule`, `non_comptable` | Both versions live on the same `POST /v1/partners/events` route. In a batch, the batch `version` (1 by default) applies to events without their own; an event can carry its own version. > **warning** In version 1, version 2 fields (`tiers`, `lignes`, `lie_a`…) are not read: they are lost. Never send v2 events without `"version": 2`. ### v1 example ```json { "version": 1, "id": "9a2e41c7", "type": "achat_cacao", "exploitation": "COOP-SOUBRE", "date": "2026-10-08", "quantites": { "kg": 1200 }, "montant": 1320000, "devise": "XOF", "donnees": { "producteur": "KOUASSI Yao Bernard", "campagne": "2026-2027", "prix": 1100 } } ``` In version 1, each `type` needs an accounting mapping configured in dafexpert (journal, accounts, amount expressions over `montant`, `quantites` and `donnees`). Without a mapping, the event is `rejete`. To move to version 2, see [Migrate to v2 events](/en/article/migrate-events-v2). ## Successive versions of the same event The `id` is the key: sending an `id` already received never creates a second event. What happens depends on the content and on the event's state. | Situation | Response | Effect | |---|---|---| | Same `id`, same content | `doublon: true`, current status | Nothing: safe to resend after a network failure. | | Same `id`, different content, event not posted yet (`recu`, `en_attente_entreprise`, `en_attente_piece`, `rejete`) | `contenu_different: true`, `version_appliquee: true`, `version` incremented | The new version replaces the old one; the event is processed again. | | Same `id`, different content, processing in progress | `version_appliquee: false`, `message` | Version kept; applied if processing fails, otherwise flagged as received after posting. | | Same `id`, different content, event posted (or cancelled, ignored…) | `version_appliquee: false`, `message` | Version kept for reference and flagged in dafexpert, with no effect. | ```json { "results": [ { "id": "PAY-2b77e0", "statut": "comptabilise", "doublon": true, "version": 1, "contenu_different": true, "version_appliquee": false, "message": "Nouvelle version reçue après comptabilisation : elle n'est pas appliquée. Pour corriger, envoyer une annulation puis un nouvel événement." } ] } ``` Reading the event (`GET /v1/partners/events/{id}`) then shows `version_non_appliquee: { version, message }`. Every version received is kept and visible in dafexpert. ### Correcting a posted event A posted event cannot be changed. Send an `annulation` (cancellation) that references it in `lie_a`, with a `motif` (reason), then send the correct operation under a **new `id`**. The cancellation reverses its entries and deletes its settlements; the original event becomes `annule`. > **info** An FNE-certified document (slip, invoice) is not cancelled by an event: it needs a credit note, issued in dafexpert until the partner credit note route is available. ## Effects In version 2, an event can touch several companies, each with its role (`payeur` payer, `preteur` lender, `beneficiaire` beneficiary, `cedant` transferor, `acheteur` buyer, `employeur` employer, `cooperative`…). Each company and role pair is an **effect**, with its own status. | Effect field | Values | |---|---| | `entreprise` | `external_id` of the company involved. | | `role` | Role of the company in the operation. | | `action` | `creee` (document created), `rattachee` (document already in dafexpert, matched), `ecriture` (entry only). | | `statut` | `comptabilise`, `en_cours` or `rejete`. | | `piece` | `type` (`supplier_settlement`, `settlement`, `supplier_invoice`, `entry`), `numero`, `numero_fiscal`. | | `ecriture` | Entry number, if the effect produced one. | | `message` | Note or rejection reason, in French. | The event status sums up its effects: all posted → `comptabilise`; only some → `partiel`; none → `rejete`, with the effects' reasons in `erreur`. ### Matching with a document already present For `achat_fournisseur`, dafexpert first looks for the invoice in the books (same supplier and same number, or same tax number), for example if it was received by email. A single match with the same amount: the event is attached to it (`action: rattachee`). Several matches, or a different amount: the event is rejected as "À rapprocher" (to be matched) and resolved in dafexpert. ## Discrepancies A **discrepancy** (écart) is an event that is not (or not fully) posted: `rejete`, `partiel`, or waiting too long. Discrepancies are shown in dafexpert, at the top of each company's events screen and in the workspace view, for the accountant to handle. | Common cause | Example reason | |---|---| | Referenced document missing | "Pièce citée introuvable après 48 h : ACH-6f1c2a9e" | | Probable duplicate | "Doublon probable de l'événement PAY-2b77e0 (même tiers, date, montant) : confirmer dans dafexpert pour le comptabiliser." | | Inconsistency | "Retenues incohérentes avec les bordereaux payés." | | Accounting | No open fiscal year at that date, closed period, account not set up | | Currency | "Devise EUR différente de celle de l'entreprise (XOF)." | ### Probable duplicates dafexpert computes a business fingerprint for each v2 event, independent of its `id`: type, company, counterparty, date, amount, lines and referenced documents (plus the invoice number for `achat_fournisseur`). If another active event has the same fingerprint, the second one is rejected as a probable duplicate. If they really are two separate operations, the accountant confirms it in dafexpert. `annulation`, `recolte` and `inventaire` are not checked. Read next: [Track event processing](/en/article/track-events) and [Idempotency](/en/article/idempotency). --- # Amounts and currencies URL: https://developers.stg.dafexpert.com/en/article/amounts-currencies > The expected amount format (whole FCFA, no decimals), the company currency, rounding and total checks. Most `422` rejections on events and documents come from a malformed amount or a total that does not add up. This page gathers the rules. ## The currency is the company's Each company has a currency, deduced from its country at creation and frozen afterwards. All amounts you send for it are in that currency. | Country | Currency | |---|---| | WAEMU: `CI`, `BJ`, `BF`, `GW`, `ML`, `NE`, `SN`, `TG` | `XOF` (BCEAO CFA franc) | | CEMAC: `CM`, `CF`, `CG`, `GA`, `GQ`, `TD` | `XAF` (BEAC CFA franc) | | `KM`, `GN`, `CD` | `KMF`, `GNF`, `CDF` | | `FR` | `EUR` | The `devise` field of events is optional. When present, it is a three-letter ISO code (`XOF`) that must be **the company's**: otherwise the event is stored but becomes `rejete` with the reason "Devise … différente de celle de l'entreprise". Document routes have no `devise` field: the company currency applies. > **info** There is no currency conversion: an operation in euros for an Ivorian cooperative must be converted to FCFA by your application before sending. ## Amount format An amount is a **JSON number**, expressed in **units** of the currency (not in cents). | Value | Accepted? | |---|---| | `1320000` | Yes | | `"1320000"` | No: string instead of a number | | `1320000.5` | No in version 2 and for documents; yes in version 1 | | `1 320 000` or `1,320,000` | No: invalid JSON | | `132000000` for 1,320,000 FCFA | No: this is not an amount in cents | ### Version 2 and tax documents: whole FCFA In v2 events, purchase slips and invoices, every amount (`montant`, `prix_unitaire`, `lie_a[].montant`, `retenues[].montant`, `brut`, `net`, `valeur_nominale`…) must be: - an **integer**: the CFA franc has no subdivision (otherwise "Montant en FCFA entiers attendu."); - **zero or positive**, and at most 10,000,000,000,000 (otherwise "Montant hors bornes."). A correction never uses a negative amount: send an `annulation`, then the right operation. ### Version 1 In version 1, `montant` accepts **up to two decimals** (otherwise "Deux décimales au plus.") and can be negative, up to 10,000,000,000,000 in absolute value. A negative amount flips the side of the lines in the accounting mapping. ## Quantities and percentages | Field | Rule | |---|---| | `quantite` (slip, invoice) | Strictly positive number, decimals allowed, at most 1,000,000,000. | | `quantite` (v2 events) | Zero or positive number, decimals allowed, at most 1,000,000,000. | | `taux_tva` | VAT rate from 0 to 100 (%). Defaults to 0 on invoices. 18 for standard-rate Ivorian VAT. | | `refaction_pct`, `humidite_pct` | Quality deduction and moisture, from 0 to 100. | | `superficie_ha` | Zero or positive, at most 1,000,000. | | `capital_social` | Zero or positive, at most 10,000,000,000,000. | A decimal quantity (1,200.5 kg) is accepted: on the invoice, the line becomes "1 × amount" with the exact quantity in the description. ## Rounding and total checks dafexpert recomputes totals and rejects any gap. Compute your amounts the same way: | Check | Rule | Tolerance | |---|---|---| | Slip or invoice line | `montant` = rounded `quantite` × `prix_unitaire` | 1 franc | | Slip total | `montant` = sum of the lines' `montant` | none | | Invoice total (including tax) | `montant` = sum of amounts excluding tax + each line's VAT rounded to the franc | 1 franc per line | | `paiement_producteur` | `montant` = sum of `lie_a` − sum of `retenues` | none | | `remboursement_producteur`, `encaissement_client` | `montant` = sum of `lie_a` | none | | `cession_intrant`, `depense` | `montant` = sum of the lines | none | | `achat_fournisseur` | `montant` = sum of the lines' amounts **excluding tax**; VAT is computed by dafexpert | none | | `paie` | `montant` = sum of `net`; each `net` = `brut` − employee deductions | none | | `souscription_parts` | `montant` = `nombre_parts` × `valeur_nominale` | none | Invoice example with two rates: ```text line 1: 25,000 kg × 1,450 = 36,250,000 excl. tax, VAT 0% → 36,250,000 line 2: 100 × 2,500 = 250,000 excl. tax, VAT 18% → 295,000 (VAT 45,000) expected total including tax = 36,545,000 ``` > **warning** Send amounts **frozen at the time of the operation**: the price applied that day, not a price recomputed at today's rate. A certified document can no longer be changed. ## Amounts in responses - Partner API: amounts in responses (`montant` of an event, a slip, an invoice) are in currency units, as when sent. - MCP server: fields whose name ends with `Cents` are in hundredths of the currency, including FCFA. `132000000` means 1,320,000 FCFA. Read next: [Field limits](/en/article/field-limits) and [Errors](/en/article/errors). --- # Field limits URL: https://developers.stg.dafexpert.com/en/article/field-limits > Maximum lengths, formats and list sizes accepted by the API: identifiers, dates, NCC, phone numbers, emails, batches. This page gathers the limits enforced by the API. A value out of bounds returns `422` with the offending field in `erreurs[].champ` (for example `lignes.0.produit`). > **tips** Leading and trailing spaces are removed before the length check. Lengths are counted in characters. ## Identifiers and common formats | Format | Rule | Fields | |---|---|---| | External identifier | 1 to 100 characters among letters, digits and `. _ : -` | URL `{external_id}`, batch `external_id`, event and document `id`, `tiers.id`, `tiers.entreprise`, `parent_id`, `cooperative_id`, `acces[].exploitation`, `lie_a`, `caisse`, `employe.id` | | v1 event type | 1 to 60 characters, starting with a letter or digit, then letters, digits, `_ . -` | `type` (version 1) | | Date | `YYYY-MM-DD` | `date`, `echeance`, `date_creation`, `date_naissance`, `valide_jusqu_au`, `facture_fournisseur.date` | | Fiscal year start | `YYYY-MM-01` (the 1st of a month) | `debut_exercice` | | Payroll period | `YYYY-MM` | `periode` (`paie`) | | Country | Two letters (`CI`), among supported countries for a company | `pays`, `tiers.pays` | | Currency | Three letters (`XOF`) | `devise` | | Email | Valid email address | `contact.email`, `email` (user), `tiers.email` | | URL | Valid URL | `piece_jointe` | | Batch identifier | UUID | `{lot}` | ## Companies `PUT /v1/partners/companies/{external_id}` and each item of `POST /v1/partners/companies`. | Field | Limit | |---|---| | `nom` | 1 to 200 | | `sigle` | 40 | | `ncc` | 30 | | `forme_juridique`, `numero_immatriculation`, `regime_fiscal` | 60 | | `centre_impots` | 120 | | `agrement_filiere.numero` | 1 to 60 | | `adresse.ligne1` | 200 | | `adresse.ville`, `adresse.commune` | 100 | | `localisation.region`, `departement`, `commune`, `village` | 100 | | `localisation.gps.lat` / `lng` | -90 to 90 / -180 to 180 | | `etiquettes` | 20 keys of 1 to 60 characters; value of 100 characters or a list of 20 values | | `exploitant.prenoms` | 120 | | `exploitant.telephone`, `exploitant.cni` | 40 | | `exploitant.id_producteur` | 60 | | `exploitant.sexe` | `M` or `F` | | `exploitant.superficie_ha` | 0 to 1,000,000 | | `exploitant.cultures` | 20 values of 1 to 60 characters | | `contact.nom` | 120 | | `contact.telephone` | 40 | | `capital_social` | 0 to 10,000,000,000,000 | ## Users | Field | Limit | |---|---| | `prenom`, `nom` | 80 | | `telephone` | 40 | | `mot_de_passe` | 8 to 128, plus dafexpert's security policy | | `acces` | 200 items | | `acces[].role` | 1 to 40 | ## Events ### All versions | Field | Limit | |---|---| | `events` (batch) | 1 to 500 events | | `donnees` | 32,000 characters once serialized as JSON | | `montant` | v1: two decimals, absolute value up to 10^13. v2: integer from 0 to 10^13 | ### Version 1 | Field | Limit | |---|---| | `quantites` | Keys of 30 characters, numeric values | ### Version 2 | Field | Limit | |---|---| | `reference_origine` | 120 | | `analytique.campagne`, `parcelle`, `groupement`, `produit` | 60 | | `tiers.nom`, `tiers.raison_sociale` | 200 | | `tiers.prenoms` | 120 | | `tiers.telephone`, `tiers.cni` | 40 | | `tiers.id_producteur` | 60 | | `tiers.ncc` | 30 | | `operateur` | 40 | | `reference_transaction` | 120 | | `lie_a` (payments, repayments, receipts) | 1 to 200 items | | `lie_a` (`annulation`) | exactly 1 | | `retenues` | 50 items | | `lignes` (`cession_intrant`, `achat_fournisseur`) | 1 to 200 | | `lignes` (`depense`) | 1 to 100 | | `lignes` (`paie`, `inventaire`) | 1 to 2,000 | | `lignes[].produit` | 60 (1 to 60 when required) | | `lignes[].libelle` | 200 | | `lignes[].nature` (`depense`) | 1 to 40 | | `lignes[].unite` | 20 | | `lignes[].entrepot` | 60 | | `lignes[].quantite` | 0 to 1,000,000,000 | | `lignes[].taux_tva` | 0 to 100 | | `lignes[].duree_annees` | integer from 1 to 100 | | `facture_fournisseur.numero` | 1 to 60 | | `facture_fournisseur.numero_fiscal` | 120 | | `financement` | 120 | | `periode` (`cotisation`) | 20 | | `motif` (`annulation`) | 1 to 300 | | `employe.nom` | 1 to 200 | | `retenues_salariales`, `charges_patronales` | 30 items; `nature` from 1 to 40 | | `nombre_parts` | integer, at least 1 | ### Sync | Parameter | Limit | |---|---| | `limite` | 1 to 500, 100 by default | | `apres` | 200 characters | ## Tax documents (slips, invoices) | Field | Limit | |---|---| | `lignes` | 1 to 200 | | `lignes[].produit` | 60 (1 to 60 on a slip, where it is required) | | `lignes[].qualite` | 40 | | `lignes[].description` (invoice) | 255 | | `lignes[].unite` | 20 | | `lignes[].quantite` | strictly positive, at most 1,000,000,000 | | `lignes[].nombre_sacs` | integer, zero or positive | | `lignes[].refaction_pct`, `humidite_pct`, `taux_tva` | 0 to 100 | | `reference_origine` | 120 | | `analytique.*` | 60 | | `tiers.*` | same as for v2 events | ## Company batches | Field | Limit | |---|---| | `entreprises` | 1 to 500 | | `external_id` | unique within the batch | ## FNE requirements (Côte d'Ivoire) These rules are not checked when you send: a field within the limits above is accepted. They are checked at certification time, and a document that does not meet them stays `non_certifie` with the reason in `erreur`. | Data | FNE rule | Applies to | |---|---|---| | Phone | 10 Ivorian digits. Spaces and symbols are ignored, the `+225` prefix is removed (`+225 07 07 12 34 56` becomes `0707123456`). | Producer on a slip, customer on a B2B invoice | | NCC | 8 characters, letters and digits (spaces ignored). | Customer on a B2B invoice | | Email | Required. For a slip, the buyer contact's email is used if the producer has none. | Producer on a slip, customer on a B2B invoice | | Currency | `XOF` only. | Slip | | Quantity | Strictly positive. | All lines | ## Unknown fields - A field that is not part of the contract is ignored, without error. A typo in a field name therefore loses the value silently. - Exceptions: the legacy `utilisateurs` field of a company is rejected (`422`), and so is a field meant for another company type ("Champ non prévu pour le type"). Read next: [Amounts and currencies](/en/article/amounts-currencies) and [Errors](/en/article/errors). --- # Rate limiting URL: https://developers.stg.dafexpert.com/en/article/rate-limiting > The API's volume limits today, fair-use rules and how to get ready for upcoming limits. The partner API does **not enforce rate limits yet**, per partner or per workspace. That doesn't mean volume has no consequences: fair use keeps the API fast for everyone, and keeps your integration ready for the limits to come. > **info** **Coming soon.** Rate limits will be introduced and documented here, with their values. A request over the limit will get a `429 Too Many Requests`. Your code should already know how to handle it. ## Limits that already apply These are not rate limits but maximum sizes per call: | Item | Limit | |---|---| | Events per batch (`POST /v1/partners/events`) | 500 | | Companies per batch (`POST /v1/partners/companies`) | 500 | | Events per sync page (`limite`) | 100 by default, 500 max | | An event's free-form `donnees` | 32 KB | | Token lifetime | 5 minutes | Other field limits are described in [Field limits](/en/article/field-limits). A batch over its maximum size is rejected as a whole with a `422`. ## Fair use - **Reuse your token** for its 5 minutes of validity. Requesting a token on every call doubles your traffic and needlessly loads the identity provider. - **Batch your sends**: one batch of 200 events beats 200 calls. To migrate historical data, use bulk company creation. - **Limit concurrency**: a few parallel requests are enough. There is no need to open dozens of simultaneous connections. - **Sync with the cursor** instead of polling each event one by one. See [Track events](/en/article/track-events). - **Spread out bulk migrations** and tell us in advance about large volumes ([support@dafexpert.com](mailto:support@dafexpert.com)). - **Don't loop on an error**: a `422` resent unchanged will always return the same error. ## Getting ready for `429`s Treat `429` as a temporary error right now: wait, then resend the same request with increasing delays. If the response carries a standard `Retry-After` header, honor it. Thanks to [idempotency](/en/article/idempotency), resending is safe. ```javascript async function sendWithBackoff(doRequest, maxAttempts = 6) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const res = await doRequest(); if (res.status !== 429 && res.status < 500) return res; if (attempt === maxAttempts) return res; const retryAfter = Number(res.headers.get('retry-after')); const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(2 ** attempt * 500, 60_000) * (0.5 + Math.random()); await new Promise((r) => setTimeout(r, delay)); } } ``` ## Next steps - [Error handling & status codes](/en/article/errors) : When to retry, when to fix. - [Create companies in bulk](/en/article/bulk-companies) : Migrate historical data in a few calls. --- # Versioning URL: https://developers.stg.dafexpert.com/en/article/versioning > How the API evolves without breaking your integration: version in the URL, version field in bodies, the v2 event contract and compatibility rules. The dafexpert API is versioned at two levels: **the URL** (`/v1`) and the `version` field of each request body. Regular changes only add fields, so your integration keeps working with no change. ## The two version levels | Level | Where | Current value | Role | |---|---|---|---| | API version | The path: `/v1/partners/...` | `v1` | All routes, authentication and common formats. | | Contract version | The `version` field of the body | `1` (companies, users, events) and `2` (field events) | The schema of a given body type. | `GET /v1/partners/me` tells you which API version you are using: ```json { "partner": "acme", "workspaceId": "3f6b2c1e-...", "apiVersion": 1 } ``` ## The `version` field in bodies Every call carries `"version": 1`. It defaults to `1` when missing, but we recommend always sending it explicitly: that way your code states which contract it speaks. ```json { "version": 1, "type": "cooperative", "nom": "Coopérative Agricole de Daloa", "pays": "CI" } ``` ## What can change without a new version Changes within version 1 only **add**: - new optional request fields; - new response fields; - new status values or new `error_code` values; - new routes. **Removing or renaming** a field, or changing its meaning, will go through a new version, **announced in advance** in the [changelog](/en/changelog). ## Writing a robust integration - **Ignore unknown fields** in responses: new fields can appear at any time. - **Have a default case** for unknown statuses and error codes. - Don't depend on field order within a JSON object. - On the request side, an unknown company field is **ignored without error**. You can therefore send a richer contract than what is accepted today: those fields will be taken into account as soon as they go live. > **warning** Exception: a field **removed** from a contract may be explicitly refused to avoid a silent error. This is the case for the former `utilisateurs` company field, replaced by `PUT /v1/partners/users/{id}`: if it is sent, the call gets a `422`. ## The v2 event contract Events come in two contract versions, both accepted **on the same route** `POST /v1/partners/events`: | | Version 1 | Version 2 | |---|---|---| | `version` field | `1` (or missing) | `2` | | Company concerned | `exploitation` | `entreprise` | | Types | Free-form, booked through configured mappings | Fixed catalog (`paiement_producteur`, `depense`, `paie`…) | | What gets created | An accounting entry | A business document (payment, supplier invoice, expense…) then its entry, for each company concerned | An event's version is read from the event itself, otherwise from the batch. A single batch can therefore mix v1 and v2 events: ```json { "version": 1, "events": [ { "id": "EVT-001", "type": "achat_cacao", "exploitation": "COOP-SOUBRE", "date": "2026-10-08", "montant": 1500000 }, { "version": 2, "id": "DEP-33ab", "type": "depense", "entreprise": "COOP-SOUBRE", "date": "2026-10-12", "lignes": [{ "nature": "transport", "montant": 45000 }], "montant": 45000, "mode": "especes", "caisse": "CAISSE-SOUBRE-1" } ] } ``` To move from one to the other, see [Migrate to v2 events](/en/article/migrate-events-v2). ### Not to be confused with an event's versions The `version` field returned in an event response (`"version": 2`) is not the contract version: it is that event's **revision number**, incremented when you resend it with different content. See [Event versions](/en/article/event-versions). ## MCP server The MCP server is not versioned in its address (`/mcp`). Its tools follow the same rule: tools or fields are added, never removed without notice. ## Next steps - [Migrate to v2 events](/en/article/migrate-events-v2) : The differences and the migration plan. - [Public roadmap](/en/article/roadmap) : What's coming next. - [Changelog](/en/changelog) : Dated changes to the API. --- # Migrate to v2 events URL: https://developers.stg.dafexpert.com/en/article/migrate-events-v2 > Move from the v1 event contract (configured mappings) to the v2 contract (type catalog, business documents, per-company effects, versions). > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. The **v2** event contract (`"version": 2`) replaces v1's mapping-based booking with a **catalog of types** that dafexpert knows. For each event, dafexpert first creates a **business document** (payment, supplier invoice, expense, payslip…), then books it automatically, for each company concerned. > **info** Version 1 is still **accepted as is**, on the same route. Nothing breaks if you don't migrate. But only v2 creates documents and gives you the details of the effects. ## What v2 brings - **Documents, not just entries**: a payment to a farmer becomes a real supplier payment, matched against the purchases it settles. - **Per-company effects**: when the counterparty is also a company of the workspace (a tracked farmer, another cooperative), its mirror document is created on its side. - **Checks on input**: `montant` must equal the sum of the lines, a payroll's net must equal gross minus deductions, otherwise `422`. - **Waiting for a document**: an event that references a document that hasn't arrived yet waits (`en_attente_piece`) instead of being rejected. - **Probable duplicate detection**: the same operation sent under two `id`s is caught before being booked twice. ## The differences | Topic | Version 1 | Version 2 | |---|---|---| | `version` | `1` or missing | `2`, required | | Company performing the operation | `exploitation` | `entreprise` | | `type` | Free-form (letters, digits, `_ . -`), booked through a configured mapping | A type from the catalog. An unknown type is refused (`422`). | | Amount | In the company's currency, two decimals max | **Whole FCFA**, equal to the sum of the lines | | Details | `quantites` and `donnees` | `lignes`, `tiers`, `lie_a`, `analytique`, `reference_origine`, depending on the type | | Counterparty | In `donnees`, no effect | `tiers`, created or updated in dafexpert | | What gets created | An entry | A business document and its entry, per company concerned | | Product purchases and sales | Events | **Synchronous tax document routes** (purchase slips, invoices) | ### The v2 catalog | Type | Operation | |---|---| | `paiement_producteur` | Payment of one or more purchases to a farmer, with its deductions | | `avance_producteur` | Cash advance or season credit | | `remboursement_producteur` | Cash repayment of an advance or of farm inputs | | `cession_intrant` | Farm inputs handed to a farmer | | `encaissement_client` | Payment received from a buyer | | `achat_fournisseur` | Supplier invoice (farm inputs, goods, services, equipment) | | `depense` | Cash expense | | `paie` | Payroll period close | | `souscription_parts` | Subscription or payment of membership shares | | `cotisation` | Membership fee or joining fee | | `inventaire` | Stock quantities at the end of a season or fiscal year | | `annulation` | Cancellation of an event already sent | | `recolte` | Harvest slip, for traceability (no document) | The fields of each type are described in the [Event catalog](/en/article/event-catalog). ### Product purchases and sales go through tax documents In v2, buying produce from a farmer or selling to a buyer is **no longer an event**: it is a tax document, issued synchronously so you get its tax number and PDF right away. | Before | Now | |---|---| | Product purchase event | `POST /v1/partners/companies/{external_id}/bordereaux` | | Product sale event, or farm inputs sold | `POST /v1/partners/companies/{external_id}/invoices` | See [Issue FNE documents](/en/article/fne-documents). Subsequent events (payments, collections) reference these documents by their `id` in `lie_a`. ## Example: a transport expense **Version 1** ```json { "version": 1, "id": "DEP-33ab", "type": "depense_transport", "exploitation": "COOP-SOUBRE", "date": "2026-10-12", "montant": 45000, "devise": "XOF", "donnees": { "prestataire": "Transport Koné", "caisse": "CAISSE-SOUBRE-1" } } ``` **Version 2** ```json { "version": 2, "id": "DEP-33ab", "type": "depense", "entreprise": "COOP-SOUBRE", "date": "2026-10-12", "tiers": { "role": "prestataire", "id": "PRE-0012", "nom": "Transport Koné", "telephone": "0101010101" }, "lignes": [ { "nature": "transport", "libelle": "Transport cacao Dignago vers magasin", "montant": 45000 } ], "montant": 45000, "devise": "XOF", "mode": "especes", "caisse": "CAISSE-SOUBRE-1", "analytique": { "campagne": "2026-2027" } } ``` ## What changes in responses The send route still answers `202` with one result per event. `GET /v1/partners/events/{id}` and sync additionally return, for v2: | Field | Meaning | |---|---| | `effets` | One effect per company concerned: `entreprise`, `role`, `action` (`creee`, `rattachee`, `ecriture`), `statut`, `piece` (`type`, `numero`, `numero_fiscal`), `ecriture`, `message`. | | `en_attente_de` | The `id`s of referenced documents that have not arrived yet. | | `version` | The applied revision number (1 on first send). | | `version_non_appliquee` | A version received but not applied, with its `message`. | > **note** In responses, the event's company stays in the `exploitation` field, in both v1 and v2. ### New statuses | Status | Meaning | |---|---| | `en_attente_piece` | A document referenced in `lie_a` has not arrived yet. The event is released when it arrives. | | `partiel` | Only some of the effects succeeded (for example the buyer's document, but not the mirror document). | | `annule` | Cancelled by an `annulation` event. | | `non_comptable` | Recorded for information (`recolte`), never flagged as a gap. | If your code handles statuses in a `switch`, add these values, and a default case for any that may come later. ## Migration plan **1. List your v1 types** Map each of your free-form types to a v2 catalog type. Product purchases and sales move to the tax document routes. **2. Complete your data** Counterparties (phone required for a farmer), lines, cash register codes, `lie_a` links to the original documents. Amounts become whole FCFA. **3. Test in staging** Send `"version": 2` events for a test company, then check `effets` and `statut` with `GET /v1/partners/events/{id}`. **4. Switch type by type** A batch can mix v1 and v2: each event carries its own `version`, otherwise the batch's. Switch gradually. **5. Update your tracking** Read the new statuses and `effets`, fetch tax document numbers and PDFs. ## Things to watch - **Don't mix versions for the same operation.** A v1 event creates no document and **drops** every v2-specific field: don't send your real operations in v1 if you rely on documents. - **`id`s remain the deduplication key**: an event already sent in v1 and resent in v2 with the same `id` is treated as a new version of the same event, not as a new event. - **Corrections**: a booked event can't be modified. Send an `annulation` that references it in `lie_a`, then the correct operation with a new `id`. - **Mobile money payments**: `reference_transaction` is required. - **Stop producing your own tax documents** (invoices, purchase slips) once v2 is in place: they come from dafexpert. ## Next steps - [Event catalog](/en/article/event-catalog) : The fields of each v2 type. - [Issue FNE documents](/en/article/fne-documents) : Certified purchase slips and invoices. - [Event versions](/en/article/event-versions) : Resending a modified event. --- # Public roadmap URL: https://developers.stg.dafexpert.com/en/article/roadmap > Planned changes to the dafexpert API. Here are the **planned** changes to the API. They are not available yet: don't rely on them until they appear in the [changelog](/en/changelog). > **note** This roadmap gives a direction, not a date commitment. Changes that require work on your side are always announced in advance. ## Planned | Change | What it means for you | |---|---| | **Webhooks** `event.posted` and `event.rejected` | dafexpert notifies you when an event is booked or rejected. No more polling. See [Webhooks](/en/article/webhooks). | | **Certified credit notes through the API** | Cancel an FNE-certified purchase slip or invoice directly through the API, with a certified credit note issued. | | **Grace period on secret rotation** | The old and new `client_secret` stay valid for a while, so you can rotate without downtime. See [Get your credentials](/en/article/credentials). | | **Rate limits** | Documented limits, with a `429` response. See [Rate limiting](/en/article/rate-limiting). | | **Booking of inventories** | `inventaire` events are recorded for information today. Stock valuation and the closing stock variation will be automated. | | **Public company API** | Access for dafexpert's customer companies to their own data, without going through a partner. | | **Production URL** | Provided at go-live. | ## An idea, a need? Does your integration need a route or a field that doesn't exist yet? Email us at [support@dafexpert.com](mailto:support@dafexpert.com) and describe your use case. - [Changelog](/en/changelog) : What's already available, dated. - [Versioning](/en/article/versioning) : How the API evolves without breaking your integration. --- # Glossary URL: https://developers.stg.dafexpert.com/en/article/glossary > dafexpert, OHADA accounting and Ivorian e-invoicing terms used throughout this documentation, with their French API names. API field names are in French. This glossary explains the terms you will come across, with the French term and its API name when there is one. ## Organization | Term | French / API | Definition | |---|---|---| | **Workspace** | espace, `workspaceId` | An organization in dafexpert: a federation, an accounting firm, a group. Your partner credentials are bound to one workspace and only act within it. See [Workspaces & companies](/en/article/workspaces-companies). | | **Company** (or file) | entreprise, dossier, `external_id`, `company_id` | An entity with its own books: cooperative, union, buyer, farmer, SME. It belongs to a workspace. | | **Farm / operation** | exploitation, `exploitation` | Historical name of the company concerned by a v1 event (and in responses). In v2, the field is called `entreprise`. | | **Partner** | partenaire, `partner` | A software vendor connected to one or more workspaces through the partner API. | | **Workspace admin** | administrateur de l'espace | Owner or admin of the workspace: connects partner applications and manages their credentials. | | **Member** | membre | A workspace user. A user created through the API is a plain member, with a role on each listed company (`dirigeant`, `comptable`, `collaborateur`, `lecture-seule`…). | | `external_id` | `external_id`, `id` | Your identifier for a resource, stable, which also serves as the [idempotency](/en/article/idempotency) key. | ## Accounting | Term | French / API | Definition | |---|---|---| | **Accounting framework** | référentiel, `referentiel` | The company's accounting framework, derived from its country. See [Accounting frameworks](/en/article/accounting-frameworks). | | **SYSCOHADA** | `SYSCOHADA` | The OHADA accounting system, used by companies in the 17 OHADA member states (Côte d'Ivoire, Senegal, Cameroon…). | | **SYCEBNL** | | The OHADA accounting system for non-profit entities (associations, foundations, projects). | | **PCG** | | Plan comptable général, the French chart of accounts. | | **Normal system / SMT** | `systeme_comptable` | The normal system is full accounting. The minimal cash-based system (SMT) is a simplified accounting for very small entities, the default for a farmer. | | **Fiscal year** | exercice, `debut_exercice` | The annual accounting period (usually 12 months), split into monthly periods. The first fiscal year is opened when the company is created. | | **Closed period** | période close | A period in which no more entries can be posted. An event dated in a closed period is rejected. | | **Accounting entry** | écriture, `ecriture` | The accounting record of an operation: balanced debit and credit lines on accounts. | | **Draft entries** | brouillard | State of an entry that is recorded but not yet validated by the accountant. Entries coming from the API land there. | | **Journal** | journal | The ledger where entries of the same kind are filed (purchases, sales, bank, cash…). | | **Chart of accounts** | plan comptable | The company's list of accounts, specific to its framework. Your application never sends account numbers. | | **Counterparty** | tiers, `tiers` | A customer, supplier, farmer, employee or service provider. It has a subsidiary account in the company's books. | | **Matching** | lettrage | Matching an invoice with its payment on a counterparty account, to know what remains due. | | **Document** | pièce, `piece` | The document backing an entry: invoice, purchase slip, payment, expense note, payslip… | | **Credit note** | avoir | A document that cancels all or part of an invoice. | | **Opening balances** | AN (à-nouveaux) | Balances carried forward from a closed fiscal year to the next one. | | **Analytics** | analytique, `analytique` | An additional breakdown of operations (season, plot, group, product) to track costs and margins. | | **Season** | campagne, `analytique.campagne` | The marketing season of an agricultural sector, for example `2026-2027`. | | **FCFA** | `XOF` | The West African CFA franc, with no decimals. See [Amounts & currencies](/en/article/amounts-currencies). | ## Tax and identifiers (Côte d'Ivoire) | Term | French / API | Definition | |---|---|---| | **NCC** | `ncc` | Numéro de compte contribuable, the tax ID of an Ivorian company. Unique within a workspace. No NCC, no FNE-certified document. | | **DGI** | | Direction générale des impôts, Côte d'Ivoire's tax authority, which runs FNE. | | **FNE** | | Facture normalisée électronique: the DGI's system that certifies invoices and purchase slips. A certified document gets a tax number and a QR code. See [Issue FNE documents](/en/article/fne-documents). | | **Tax number** | numéro fiscal, `numero_fiscal` | The number assigned by FNE to a certified document. | | **Purchase slip** | bordereau d'achat | The document the buyer issues when buying agricultural produce from a farmer. FNE-certified (type `purchase`). | | **Sticker (FNE)** | | A virtual stamp bought in advance by the company. Each certified document uses one. | | **RCCM** | `numero_immatriculation` | Registre du commerce et du crédit mobilier (or cooperative register): the legal registration number. | | **Legal form** | forme juridique, `forme_juridique` | The legal status: `SCOOPS` or `SCOOP-CA` for a cooperative, `Entreprenant` for a farmer, SARL, SA… | | **SCOOPS / SCOOP-CA** | | Simplified cooperative society / cooperative society with a board of directors, under the OHADA uniform act. | ## API | Term | Definition | |---|---| | **Event** | An operation from your application to be booked (payment, expense, payroll…). See [Event catalog](/en/article/event-catalog). | | **Effect** (effet) | What a v2 event produces for one company: a document and its entry. | | **Gap** (écart) | An event not booked yet (rejected, waiting, partial), flagged in dafexpert to be handled. | | **Batch** (lot) | A grouped send: up to 500 events or 500 companies per call. | | **Cursor** | The opaque `suivant` value that lets you resume event sync where it stopped. | | **Client credentials** | The OAuth 2.0 flow for authenticating a server, with no user. See [Authentication](/en/article/authentication). | | **SSO** | Single sign-on: a user signs in once for dafexpert and your application. See [User sign-in](/en/article/sso). | | **MCP** | Model Context Protocol, the protocol that lets an AI assistant use tools. See [MCP server](/en/article/mcp-overview). | --- # Need support? URL: https://developers.stg.dafexpert.com/en/article/support > How to reach the dafexpert team and what to include to get a fast answer. A question about the API, unexpected behavior, an integration need? Write to the dafexpert team: **[support@dafexpert.com](mailto:support@dafexpert.com)**. ## Before you write - Re-read the response's **message** and `error_code`: they often point to the exact cause. See [Error handling & status codes](/en/article/errors). - For a persistent `401`, first check with the **workspace admin** that your application is still connected and that the secret hasn't been rotated. See [Get your credentials](/en/article/credentials). - For a rejected event, look up its reason with `GET /v1/partners/events/{id}` (the `erreur` field). Many rejections are fixed in dafexpert: account not configured, fiscal year not opened, closed period. ## What to include Every partner API call is logged on our side. With the details below, we can find your exact call: | Item | Example | |---|---| | Environment | Staging or production | | Your `client_id` | `partner-acme-coop` | | Date and time of the call, with time zone | `2026-10-12 14:32 UTC` | | Method and route | `POST /v1/partners/events` | | HTTP status and response body | `422` with its `error_code`, `message` and `erreurs` | | Identifiers involved | company `external_id`, event or document `id`, `lot` | | Request body | With sensitive data removed (see below) | | What you expected | The expected result, and how it differs | > **error** **Never send** your `client_secret`, an access token (`access_token`) or a user password (`mot_de_passe`), by email or in a ticket. We will never ask for them. If a secret has leaked, ask the workspace admin to generate a new one. ## Sample request ```text Subject: [Staging] 422 on POST /v1/partners/events, event DEP-33ab Environment: staging client_id: partner-acme-coop Call: POST /v1/partners/events, on 2026-10-12 at 14:32 UTC Response: 202, item DEP-33ab "invalide" erreurs: [{ "champ": "montant", "message": "montant (45000) différent de la somme des lignes (40000)." }] Body sent (redacted): attached Expected: the expense should be accepted. Is the second line of 5000 FCFA missing? ``` ## Other requests - **Becoming a partner** and getting your application listed: write to us with an overview of your product and your customers. - **Setting up SSO**: send us your redirect URIs. See [User sign-in (SSO)](/en/article/sso). - **Large migrations**: let us know before a bulk company creation or a large event send. - **A mistake in this documentation**: report it to the same address, with the page link. - [Error handling & status codes](/en/article/errors) : Understand an error before writing to us. - [Glossary](/en/article/glossary) : dafexpert and OHADA accounting terms. - [Public roadmap](/en/article/roadmap) : What's coming next. --- # Introduction URL: https://developers.stg.dafexpert.com/en/article/api-introduction > Base URL, authentication, and the format of requests, identifiers, dates, amounts and errors. The dafexpert partner API is a REST API: it accepts and returns JSON, and every route is described in this reference with its parameters, responses and errors. It is called by **your servers**, never by a user's browser. An access token belongs to **one dafexpert workspace** (for example a federation of cooperatives). Every company, user, event and document you create lives in that workspace: it is derived from the token and never read from the request. ## Base URL | Environment | API URL | |---|---| | Staging | `https://api.stg.dafexpert.com/v1` | | Production | provided at go-live | Every route in this reference starts with `/v1/partners`. The version number is part of the path. ## Authentication Each call carries an OAuth 2.0 token obtained with the *client credentials* flow, in the `Authorization` header. Tokens last 5 minutes and there is no refresh token: just request a new one with the same credentials. ```http Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9… ``` | Item | Value | |---|---| | Header | `Authorization: Bearer ` | | Flow | OAuth 2.0 client credentials | | Scopes | None: a valid token grants access to every partner route of its workspace | | Revocation | A disconnection by the workspace administrator takes effect within 30 seconds (`401`) | Details (token URL, caching, handling `401`) are in [Authentication](/en/article/authentication). To test your credentials, call [GET /v1/partners/me](/en/article/get-partner-me). ## Request format - JSON bodies, UTF-8 encoded, with the `Content-Type: application/json` header. - Field names are French and lowercase (`exploitation`, `montant`, `lie_a`). Unknown fields are ignored without error unless stated otherwise. - The `version` field of a body sets the contract version: `1` by default, `2` for the [version 2](/en/article/event-versions) event catalog. **cURL** ```bash curl -X PUT https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "cooperative", "nom": "Coopérative Agricole de Soubré", "pays": "CI" }' ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE', { method: 'PUT', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'cooperative', nom: 'Coopérative Agricole de Soubré', pays: 'CI' }), }); ``` **Python** ```python import os, requests resp = requests.put( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={"type": "cooperative", "nom": "Coopérative Agricole de Soubré", "pays": "CI"}, timeout=30, ) ``` ## Identifiers You refer to every object by **your own identifier** (`external_id`): the one the cooperative, farmer, user, event or document has in your system. dafexpert never asks you to store its internal identifiers. | Rule | Value | |---|---| | Length | 1 to 100 characters | | Characters | letters, digits and `. _ : -` | | Examples | `COOP-SOUBRE`, `EXPL-0457`, `ACH-2026-000412`, `5f1c2a9e-3b7d-4c11-9e0a-8d2f6b1c4e77` | | Stability | An identifier must never change: it is the idempotency key | Responses also include the matching dafexpert identifiers (`company_id`, `user_id`, `piece_id`, as UUIDs) for information. ## Dates and amounts | Data | Format | Example | |---|---|---| | Date | `YYYY-MM-DD` | `2026-10-08` | | Timestamp (responses) | ISO 8601, UTC | `2026-10-08T10:00:03.000Z` | | Country | two-letter ISO code | `CI` | | Currency | three-letter ISO code | `XOF` | | Tax document and v2 event amounts | integer, in FCFA | `2160000` | | v1 event amounts | number, at most two decimals, in the company's currency | `1500000` | Amounts are JSON numbers, never strings, with no thousands separator. See [Amounts and currencies](/en/article/amounts-currencies) and [Field limits](/en/article/field-limits). ## Response codes | Code | Meaning | |---|---| | `200` | Success (read, update, or replay of a creation already done) | | `201` | Object created | | `202` | Received and queued: processing continues in the background | | `401` | Missing, expired or invalid token, or access revoked | | `403` | Object belongs to another workspace | | `404` | Object unknown in your workspace | | `409` | Conflict (taxpayer number already taken, document being issued, workspace without an active administrator…) | | `422` | Invalid data or business refusal, with the reason | | `5xx` | Temporary error or outage: retry later with increasing delays | ## Error format All errors share the same JSON body. Validation errors list each rejected field in `erreurs`, with its path (`lignes.0.montant`). ```json { "statusCode": 422, "error_code": "validation_failed", "message": "Données invalides.", "erreurs": [ { "champ": "pays", "message": "Code pays ISO à deux lettres attendu (ex. CI)." }, { "champ": "debut_exercice", "message": "Date attendue au format AAAA-MM-JJ." } ] } ``` | Field | Type | Description | |---|---|---| | `statusCode` | integer | HTTP status of the response | | `error_code` | string | Stable code: `validation_failed`, `unauthenticated`, `forbidden`, `not_found`, `conflict`… | | `message` | string | Readable reason. In French for business refusals | | `erreurs` | array | Validation only: one `{ champ, message }` entry per rejected field | How to handle each code is covered in [Errors](/en/article/errors). ## Idempotency Replaying a call never creates a duplicate: your identifier is the key. A company, user, event or document sent twice exists only once, and the second call returns the current state. You can safely replay after a network failure or a `5xx`. See [Idempotency](/en/article/idempotency). > **tips** There is no `Idempotency-Key` header: the identifier in the path or body plays that role. ## Routes | Route | Purpose | |---|---| | [GET /v1/partners/me](/en/article/get-partner-me) | Check credentials | | [PUT /v1/partners/companies/{external_id}](/en/article/put-company) | Create or update a company | | [POST /v1/partners/companies](/en/article/post-companies) | Create companies in bulk | | [GET /v1/partners/companies/lots/{lot}](/en/article/get-company-lot) | Track a company batch | | [PUT /v1/partners/users/{external_id}](/en/article/put-user) | Create or update a user | | [DELETE /v1/partners/users/{external_id}](/en/article/delete-user) | Remove a user | | [POST /v1/partners/events](/en/article/post-events) | Send events | | [GET /v1/partners/events/{id}](/en/article/get-event) | Retrieve an event | | [GET /v1/partners/events](/en/article/list-events) | Sync events | | [POST …/companies/{external_id}/bordereaux](/en/article/post-bordereau) | Issue a purchase slip | | [GET …/bordereaux/{id}](/en/article/get-bordereau) | Retrieve a purchase slip | | [GET …/bordereaux/{id}/pdf](/en/article/get-bordereau-pdf) | Download a purchase slip PDF | | [POST …/companies/{external_id}/invoices](/en/article/post-invoice) | Issue an invoice | | [GET …/invoices/{id}](/en/article/get-invoice) | Retrieve an invoice | | [GET …/invoices/{id}/pdf](/en/article/get-invoice-pdf) | Download an invoice PDF | For an end-to-end walkthrough, start with [Create a company](/en/article/create-company), then [Send events](/en/article/send-events) and [FNE tax documents](/en/article/fne-documents). --- # Check credentials URL: https://developers.stg.dafexpert.com/en/article/get-partner-me > Checks end to end that a token is valid and that the partner access is active. Returns the partner and workspace tied to the token. This is the first call to make with new credentials, and a good health check: it only succeeds if the token is valid **and** the access has not been revoked by the workspace administrator. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Parameters This route takes no parameters. ## Request example **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/me \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/me', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests resp = requests.get( "https://api.stg.dafexpert.com/v1/partners/me", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) print(resp.status_code, resp.json()) ``` ## Response `200 OK` - `partner` (string) : Code of the partner the credentials belong to. - `workspaceId` (string (uuid)) : Identifier of the dafexpert workspace the token acts in. - `apiVersion` (integer) : Partner API version. Always `1`. ```json { "partner": "gsmiles-agri", "workspaceId": "3c9f1e52-7a4b-4d0e-9b61-2f8a5c7d1e90", "apiVersion": 1 } ``` ## Errors | Code | Case | |---|---| | `401` | Missing, expired or invalid token, token that does not belong to a partner client, or access disconnected by the workspace administrator. | ```json { "statusCode": 401, "error_code": "unauthenticated", "message": "Partner access is not connected or has been revoked.", "error": "Unauthorized" } ``` On a `401`, request a new token once and replay the call. If the error persists with a fresh token, access has been revoked: contact the workspace administrator. See [Errors](/en/article/errors). --- # Create or update a company URL: https://developers.stg.dafexpert.com/en/article/put-company > Creates a cooperative, union, buyer or farmer with its first fiscal year, or updates its identity. Creates the company identified by your `external_id` in your workspace, or updates it if it already exists. On creation, dafexpert also opens its first fiscal year, with monthly periods, journals and chart of accounts (accounting framework derived from the country). The call is **idempotent**: the first call creates the company (`201`), later calls update it (`200`). Concurrent calls on the same `external_id` create a single company. To migrate hundreds of companies, use [POST /v1/partners/companies](/en/article/post-companies). | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : Your identifier for the company: 1 to 100 characters, letters, digits and `. _ : -`. It is also the value of the `exploitation` (v1) or `entreprise` (v2) field of your events. ## Request body ### Identity - `version` (integer, body) : `1`. Default: `1`. - `type` (string, body, required) : `cooperative`, `union`, `acheteur` (buyer) or `exploitant` (farmer). Sets which fields are accepted (see below) and the default accounting system. - `nom` (string, body, required) : Legal name, 1 to 200 characters. For a farmer: the family name. - `pays` (string, body, required) : Two-letter ISO code (`CI`, `SN`, `FR`…). Sets the currency and accounting framework. Frozen after creation. - `sigle` (string, body) : Short name, at most 40 characters. - `date_creation` (string (date), body) : Date the organization was founded, `YYYY-MM-DD`. - `parent_id` (string | null, body) : `external_id` of the parent organization: a cooperative's union, a farmer's cooperative. The parent may be created after the child. - `cooperative_id` (string | null, body) : Former name of `parent_id`, still accepted. If sent with `parent_id`, both values must match. - `contact.nom` (string, body) : At most 120 characters. - `contact.email` (string (email), body) : Contact email address. - `contact.telephone` (string, body) : At most 40 characters. - `adresse.ligne1` (string, body) : At most 200 characters. - `adresse.ville` (string, body) : City, at most 100 characters. - `adresse.commune` (string, body) : Municipality, at most 100 characters. - `localisation.region` (string, body) : At most 100 characters. Same for `localisation.departement`, `localisation.commune` and `localisation.village`. - `localisation.gps.lat` (number, body) : Latitude, from -90 to 90. Required with `localisation.gps.lng` (from -180 to 180). - `etiquettes` (object, body) : Free groupings (zone, value chain, project, funder): key (at most 60 characters) to a text or a list of at most 20 texts, 100 characters per value. At most 20 keys. Keys are normalized (lowercase, no accents). Each call **replaces** your tags. ### Tax and legal - `ncc` (string, body) : Taxpayer number, at most 30 characters. Unique within the workspace. Required before the first tax document. - `forme_juridique` (string, body) : Code from the country's legal form catalog (`SCOOPS`, `SCOOP-CA`, `Entreprenant`…). Case, accents and punctuation are tolerated. An unknown value is rejected with the list of accepted codes. - `numero_immatriculation` (string, body) : Cooperative registry or RCCM number, at most 60 characters. - `regime_fiscal` (string, body) : Tax regime, at most 60 characters. - `assujetti_tva` (boolean, body) : `true` if the company is subject to VAT. - `centre_impots` (string, body) : Tax office, at most 120 characters. - `agrement_filiere.numero` (string, body) : Value chain buyer license number, 1 to 60 characters. With `agrement_filiere.valide_jusqu_au` (`YYYY-MM-DD`, optional). - `capital_social` (number, body) : Share capital in FCFA, zero or positive. ### Farmer - `exploitant.prenoms` (string, body) : Given names, at most 120 characters. `nom` holds the family name. - `exploitant.telephone` (string, body) : At most 40 characters. Required before the first FNE purchase slip. - `exploitant.cni` (string, body) : ID card number, at most 40 characters. - `exploitant.id_producteur` (string, body) : Value chain identifier (producer card), at most 60 characters. - `exploitant.sexe` (string, body) : `M` or `F`. - `exploitant.date_naissance` (string (date), body) : `YYYY-MM-DD`. - `exploitant.superficie_ha` (number, body) : Farmed area in hectares, zero or positive. - `exploitant.cultures` (array of string, body) : At most 20 crops, 60 characters each (`cacao`, `anacarde`…). ### Accounting - `debut_exercice` (string (date), body) : Start of the first fiscal year, on the 1st of a month (`YYYY-MM-01`). Default: January 1 of the current year. The first fiscal year lasts 12 months. Frozen after creation. - `systeme_comptable` (string, body) : `normal` or `smt` (minimal cash-based system). Default: `smt` for a farmer, `normal` otherwise. Applied on creation. ### Fields by type A field sent for a type that does not allow it is rejected (`422`). | Field | cooperative | union | acheteur | exploitant | |---|---|---|---|---| | `sigle`, `date_creation`, `numero_immatriculation`, `regime_fiscal`, `assujetti_tva`, `centre_impots`, `capital_social` | yes | yes | yes | no | | `agrement_filiere` | yes | no | yes | no | | `parent_id` | a union | no | no | a cooperative | | `exploitant` | no | no | no | yes | > **warning** The former `utilisateurs` field is rejected: create users with [PUT /v1/partners/users/{external_id}](/en/article/put-user) after the company. ### Updates A missing field is left unchanged. The **country**, **currency**, **accounting framework** and **fiscal year** are frozen after creation, since bookkeeping may have started. ## Request example **cURL** ```bash curl -X PUT https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 1, "type": "cooperative", "nom": "Coopérative Agricole de Soubré", "sigle": "CAS", "pays": "CI", "ncc": "2104587K", "forme_juridique": "SCOOP-CA", "numero_immatriculation": "CI-SBR-2015-C-0127", "regime_fiscal": "Réel simplifié", "assujetti_tva": false, "adresse": { "ligne1": "Quartier Commerce", "ville": "Soubré", "commune": "Soubré" }, "localisation": { "region": "Nawa", "departement": "Soubré" }, "etiquettes": { "filiere": "cacao", "zone": "Centre-Ouest" }, "debut_exercice": "2026-01-01", "contact": { "nom": "Awa Koné", "email": "gerance@coop-soubre.ci", "telephone": "+225 07 07 12 34 56" } }' ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE', { method: 'PUT', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ version: 1, type: 'cooperative', nom: 'Coopérative Agricole de Soubré', pays: 'CI', ncc: '2104587K', forme_juridique: 'SCOOP-CA', adresse: { ligne1: 'Quartier Commerce', ville: 'Soubré', commune: 'Soubré' }, debut_exercice: '2026-01-01', }), }); const company = await res.json(); // 201 on creation, 200 on update ``` **Python** ```python import os, requests resp = requests.put( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "version": 1, "type": "cooperative", "nom": "Coopérative Agricole de Soubré", "pays": "CI", "ncc": "2104587K", "forme_juridique": "SCOOP-CA", "adresse": {"ligne1": "Quartier Commerce", "ville": "Soubré", "commune": "Soubré"}, "debut_exercice": "2026-01-01", }, timeout=30, ) company = resp.json() # 201 on creation, 200 on update ``` ## Response `201 Created` on creation, `200 OK` on update. - `external_id` (string) : Your identifier for the company. - `company_id` (string (uuid)) : Company identifier at dafexpert. - `statut` (string) : `cree` (created) or `mis_a_jour` (updated). - `referentiel` (string | null) : Accounting framework of the company (`SYSCOHADA`…). - `devise` (string) : Company currency (`XOF`…). - `evenements_liberes` (integer) : Number of events received before the company (status `en_attente_entreprise`) and released by its creation. Always `0` on update. - `a_completer` (array of string) : What is missing before the first tax document: `ncc`, `numero_immatriculation`, `regime_fiscal`, `assujetti_tva`, `connexion_fne` (Côte d'Ivoire), or `exploitant.telephone` for a farmer. Empty when the company is ready. ```json { "external_id": "COOP-SOUBRE", "company_id": "ec6a85db-2f41-4c8e-b1d3-6a7f9e0c2b58", "statut": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 0, "a_completer": ["connexion_fne"] } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `403` | This `external_id` is already attached to another workspace. | | `409` | Taxpayer number already used by another company in the workspace, or workspace without an active owner or administrator. | | `422` | Invalid data (details in `erreurs`), field not allowed for the type, `utilisateurs` field sent, unknown country, legal form outside the country's catalog, `debut_exercice` not on the 1st of a month, `parent_id` that does not follow the union, cooperative, farmer hierarchy, or fiscal year that cannot be opened. | ```json { "statusCode": 409, "error_code": "conflict", "message": "Le NCC 2104587K est déjà utilisé par une autre entreprise de l'espace." } ``` See also [Create a company](/en/article/create-company) and [Field limits](/en/article/field-limits). --- # Create companies in bulk URL: https://developers.stg.dafexpert.com/en/article/post-companies > Sends up to 500 companies in one call; they are created or updated in the background. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Accepts a batch of up to 500 companies, to migrate the companies you already manage. Each company is validated on its own: an invalid company is reported without blocking the others. Valid companies are queued, then created or updated exactly as by [PUT /v1/partners/companies/{external_id}](/en/article/put-company). The `202` response comes back immediately. Then track progress with [GET /v1/partners/companies/lots/{lot}](/en/article/get-company-lot). For day-to-day changes, prefer the single `PUT`. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Request body - `version` (integer, body) : `1`. Default: `1`. - `entreprises` (array of object, body, required) : 1 to 500 companies. - `entreprises[].external_id` (string, body, required) : Your identifier for the company: 1 to 100 characters, letters, digits and `. _ : -`. If the same `external_id` appears twice in the batch, the second one is `invalide`. - `entreprises[].type` (string, body, required) : `cooperative`, `union`, `acheteur` or `exploitant`. - `entreprises[].nom` (string, body, required) : 1 to 200 characters. - `entreprises[].pays` (string, body, required) : Two-letter ISO code (`CI`…). - `entreprises[].…` (, body) : Every other field of the [PUT /v1/partners/companies/{external_id}](/en/article/put-company) body, with the same rules (`ncc`, `forme_juridique`, `parent_id`, `exploitant`, `debut_exercice`…). ## Request example **cURL** ```bash curl -X POST https://api.stg.dafexpert.com/v1/partners/companies \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 1, "entreprises": [ { "external_id": "COOP-SOUBRE", "type": "cooperative", "nom": "Coopérative Agricole de Soubré", "pays": "CI", "forme_juridique": "SCOOP-CA" }, { "external_id": "EXPL-0457", "type": "exploitant", "nom": "KOUASSI", "pays": "CI", "forme_juridique": "Entreprenant", "parent_id": "COOP-SOUBRE", "exploitant": { "prenoms": "Yao", "telephone": "+225 07 58 21 44 10", "cultures": ["cacao"] } } ] }' ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/companies', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ version: 1, entreprises: [ { external_id: 'COOP-SOUBRE', type: 'cooperative', nom: 'Coopérative Agricole de Soubré', pays: 'CI', forme_juridique: 'SCOOP-CA' }, { external_id: 'EXPL-0457', type: 'exploitant', nom: 'KOUASSI', pays: 'CI', forme_juridique: 'Entreprenant', parent_id: 'COOP-SOUBRE', exploitant: { prenoms: 'Yao', telephone: '+225 07 58 21 44 10' } }, ], }), }); const { lot, results } = await res.json(); // 202 ``` **Python** ```python import os, requests resp = requests.post( "https://api.stg.dafexpert.com/v1/partners/companies", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "version": 1, "entreprises": [ {"external_id": "COOP-SOUBRE", "type": "cooperative", "nom": "Coopérative Agricole de Soubré", "pays": "CI", "forme_juridique": "SCOOP-CA"}, {"external_id": "EXPL-0457", "type": "exploitant", "nom": "KOUASSI", "pays": "CI", "forme_juridique": "Entreprenant", "parent_id": "COOP-SOUBRE", "exploitant": {"prenoms": "Yao", "telephone": "+225 07 58 21 44 10"}}, ], }, timeout=60, ) lot = resp.json()["lot"] # 202 ``` ## Response `202 Accepted`. One result per company, in the order sent. - `lot` (string (uuid) | null) : Batch identifier, to pass to [GET /v1/partners/companies/lots/{lot}](/en/article/get-company-lot). `null` if no company is valid. - `recues` (integer) : Number of valid companies queued. - `invalides` (integer) : Number of companies rejected at validation, not stored. - `results` (array of object) : One result per company sent. - `results[].index` (integer) : Position of the company in `entreprises` (zero-based). - `results[].external_id` (string | null) : The company's `external_id`, or `null` if missing or not a string. - `results[].statut` (string) : `recu` (queued) or `invalide` (not stored). - `results[].id` (string (uuid)) : Identifier of the batch line. Present when `statut` is `recu`. - `results[].erreurs` (array of object) : Present when `statut` is `invalide`: one `{ champ, message }` entry per rejected field. ```json { "lot": "b7e2c4a1-5d3f-4e8b-9a6c-1f0d2e3b4c5a", "recues": 2, "invalides": 0, "results": [ { "index": 0, "external_id": "COOP-SOUBRE", "id": "0c4f9a7e-1b2d-4e3f-8a5b-6c7d8e9f0a1b", "statut": "recu" }, { "index": 1, "external_id": "EXPL-0457", "id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "statut": "recu" } ] } ``` Result for an invalid company (here, a farmer sent with a value chain license): ```json { "index": 1, "external_id": "EXPL-0457", "statut": "invalide", "erreurs": [{ "champ": "agrement_filiere", "message": "Champ non prévu pour le type « exploitant »." }] } ``` > **info** A business refusal of a valid company (unknown country, legal form outside the catalog, taxpayer number already taken, other workspace) does not show up here: the line moves to `rejete` in the batch tracking, with its reason. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `422` | Invalid body: `entreprises` missing, empty or longer than 500 items. No company is stored. | See [Create companies in bulk](/en/article/bulk-companies) for the migration strategy. --- # Track a company batch URL: https://developers.stg.dafexpert.com/en/article/get-company-lot > Progress of a bulk creation batch: counters by status and the result for each company. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Returns the progress of a batch received by [POST /v1/partners/companies](/en/article/post-companies): the status of each company, the company created or the reason for a refusal. Poll this route until `termine` is `true`. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `lot` (string (uuid), path, required) : Batch identifier, returned in the `lot` field of the `POST` response. ## Request example **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/companies/lots/b7e2c4a1-5d3f-4e8b-9a6c-1f0d2e3b4c5a \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const lot = 'b7e2c4a1-5d3f-4e8b-9a6c-1f0d2e3b4c5a'; const res = await fetch('https://api.stg.dafexpert.com/v1/partners/companies/lots/' + lot, { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN }, }); const { termine, compteurs } = await res.json(); ``` **Python** ```python import os, requests lot = "b7e2c4a1-5d3f-4e8b-9a6c-1f0d2e3b4c5a" resp = requests.get( f"https://api.stg.dafexpert.com/v1/partners/companies/lots/{lot}", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) print(resp.json()["compteurs"]) ``` ## Response `200 OK` - `lot` (string (uuid)) : Batch identifier. - `total` (integer) : Number of companies queued in this batch (companies invalid on receipt are not included). - `termine` (boolean) : `true` once no company is `recu` or `en_cours` any more. - `compteurs` (object) : Number of companies per status: `recu`, `en_cours`, `traite`, `rejete`. - `entreprises` (array of object) : One line per company, in the order sent. - `entreprises[].index` (integer) : Position of the company in the batch sent. - `entreprises[].external_id` (string) : Your identifier for the company. - `entreprises[].id` (string (uuid)) : Identifier of the batch line. - `entreprises[].statut` (string) : `recu` (queued), `en_cours` (processing), `traite` (created or updated) or `rejete` (business refusal, not retried: fix and resend). - `entreprises[].essais` (integer) : Number of attempts. Technical errors are retried automatically. - `entreprises[].company_id` (string (uuid) | null) : Company identifier at dafexpert, once processed. - `entreprises[].resultat` (string | null) : `cree` or `mis_a_jour`, once processed. - `entreprises[].referentiel` (string | null) : Accounting framework, once processed. - `entreprises[].devise` (string | null) : Company currency, once processed. - `entreprises[].evenements_liberes` (integer | null) : Pending events released by the creation. - `entreprises[].erreur` (string | null) : Reason for the refusal (`rejete`), or the last technical error of a line still queued. - `entreprises[].traite_le` (string (date-time) | null) : Processing time, in UTC. ```json { "lot": "b7e2c4a1-5d3f-4e8b-9a6c-1f0d2e3b4c5a", "total": 2, "termine": true, "compteurs": { "recu": 0, "en_cours": 0, "traite": 1, "rejete": 1 }, "entreprises": [ { "index": 0, "external_id": "COOP-SOUBRE", "id": "0c4f9a7e-1b2d-4e3f-8a5b-6c7d8e9f0a1b", "statut": "traite", "essais": 1, "company_id": "ec6a85db-2f41-4c8e-b1d3-6a7f9e0c2b58", "resultat": "cree", "referentiel": "SYSCOHADA", "devise": "XOF", "evenements_liberes": 0, "erreur": null, "traite_le": "2026-10-12T09:14:07.512Z" }, { "index": 1, "external_id": "COOP-GAGNOA", "id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d", "statut": "rejete", "essais": 1, "company_id": null, "resultat": null, "referentiel": null, "devise": null, "evenements_liberes": null, "erreur": "Le NCC 2104587K est déjà utilisé par une autre entreprise de l'espace.", "traite_le": "2026-10-12T09:14:08.031Z" } ] } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Unknown batch, or batch from another workspace (`Lot inconnu.`). | | `422` | The batch identifier is not a UUID. | See [Create companies in bulk](/en/article/bulk-companies). --- # Create or update a user URL: https://developers.stg.dafexpert.com/en/article/put-user > Creates or links a user's login account and sets the list of companies they can access. Creates the user identified by your `external_id` (cooperative manager, farmer, accountant…) and grants access to the companies listed in `acces`. Call it **after** creating the companies with [PUT /v1/partners/companies/{external_id}](/en/article/put-company). The call is **idempotent**: `201` on creation, `200` on update. The `acces` list is the source of truth: a new call removes access to companies of your workspace that are no longer listed. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## What the call does - **Login account**: if no account exists for this email, one is created, active, with the email treated as verified. If an account already exists, it is linked; its name and password are then never changed. - **Access**: the user becomes a plain member of the workspace, never an owner or administrator. They only see the listed companies, with the role given for each. - **First login**: the user lands directly in their company, from dafexpert or from your application (SSO). ## Path parameters - `external_id` (string, path, required) : Your identifier for the user: 1 to 100 characters, letters, digits and `. _ : -`. ## Request body - `version` (integer, body) : `1`. Default: `1`. - `email` (string (email), body, required) : Email address, used as the login. - `prenom` (string, body) : First name, at most 80 characters. - `nom` (string, body) : Last name, at most 80 characters. - `telephone` (string, body) : At most 40 characters. - `mot_de_passe` (string, body) : 8 to 128 characters, subject to dafexpert's security policy. Only applied to accounts you created; never stored by dafexpert. - `mot_de_passe_temporaire` (boolean, body) : `true`: the user must change the password at first login. Default: `false`. - `acces` (array of object, body) : Accessible companies, at most 200. Default: empty list (no access). - `acces[].exploitation` (string, body, required) : `external_id` of a company already created in your workspace. - `acces[].role` (string, body) : Role on that company, at most 40 characters: `collaborateur` (default), `dirigeant`, `comptable`, `lecture-seule`… `dirigeant` makes the user the manager of that company only. ## Request example **cURL** ```bash curl -X PUT https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 1, "email": "awa.kone@coop-soubre.ci", "prenom": "Awa", "nom": "Koné", "telephone": "+225 07 07 12 34 56", "mot_de_passe": "Cacao-Soubre-2026!", "mot_de_passe_temporaire": true, "acces": [ { "exploitation": "COOP-SOUBRE", "role": "dirigeant" } ] }' ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE', { method: 'PUT', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ version: 1, email: 'awa.kone@coop-soubre.ci', prenom: 'Awa', nom: 'Koné', mot_de_passe: process.env.INITIAL_PASSWORD, mot_de_passe_temporaire: true, acces: [{ exploitation: 'COOP-SOUBRE', role: 'dirigeant' }], }), }); const user = await res.json(); // 201 or 200 ``` **Python** ```python import os, requests resp = requests.put( "https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "version": 1, "email": "awa.kone@coop-soubre.ci", "prenom": "Awa", "nom": "Koné", "mot_de_passe": os.environ["INITIAL_PASSWORD"], "mot_de_passe_temporaire": True, "acces": [{"exploitation": "COOP-SOUBRE", "role": "dirigeant"}], }, timeout=30, ) user = resp.json() # 201 or 200 ``` ## Response `201 Created` on creation, `200 OK` on update. - `external_id` (string) : Your identifier for the user. - `user_id` (string (uuid)) : User identifier at dafexpert. - `statut` (string) : `cree` (created) or `mis_a_jour` (updated). - `compte` (string) : `cree`: login account created by this or an earlier call from you. `existant`: account already present for this email, linked. - `mot_de_passe` (string | null) : `defini`: password applied. `ignore`: existing account you did not create, password unchanged. `null`: no password sent. - `acces` (array of object) : Access granted, one per company in the request. - `acces[].exploitation` (string) : The company's `external_id`. - `acces[].company_id` (string (uuid)) : Company identifier at dafexpert. - `acces[].role` (string) : Role granted. - `acces_retires` (integer) : Number of accesses removed from companies of your workspace missing from `acces`. ```json { "external_id": "USR-GERANT-SOUBRE", "user_id": "8f2d6c1a-4b3e-4a5d-9c7f-0e1b2a3c4d5e", "statut": "cree", "compte": "cree", "mot_de_passe": "defini", "acces": [ { "exploitation": "COOP-SOUBRE", "company_id": "ec6a85db-2f41-4c8e-b1d3-6a7f9e0c2b58", "role": "dirigeant" } ], "acces_retires": 0 } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `403` | This user is attached to another workspace. | | `409` | Email already used by another account (email change), or account already linked to another of your users. | | `422` | Invalid data, company unknown in the workspace (create it first), unknown role, or password rejected by the security policy. If the account had just been created, it is deleted: the call can safely be resent. | | `503` | Account creation temporarily unavailable: retry later. | ```json { "statusCode": 422, "error_code": "error", "message": "Exploitation(s) inconnue(s) dans l'espace : COOP-DALOA. Créez-les d'abord (PUT /v1/partners/companies)." } ``` > **tips** To remove all of a user's access, use [DELETE /v1/partners/users/{external_id}](/en/article/delete-user). See also [Manage users](/en/article/manage-users). --- # Remove a user URL: https://developers.stg.dafexpert.com/en/article/delete-user > Removes all of a user's access to your workspace's companies, without deleting their account. Removes all of the user's access to the companies of your workspace and deletes the link with your `external_id`. The login account is **kept**, since it may have other access in dafexpert. A new call to [PUT /v1/partners/users/{external_id}](/en/article/put-user) links the user again. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : Your identifier for the user, the one used in the `PUT`. ## Request example **cURL** ```bash curl -X DELETE https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE', { method: 'DELETE', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN }, }); console.log(await res.json()); ``` **Python** ```python import os, requests resp = requests.delete( "https://api.stg.dafexpert.com/v1/partners/users/USR-GERANT-SOUBRE", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) print(resp.json()) ``` ## Response `200 OK` - `external_id` (string) : Your identifier for the user. - `acces_retires` (integer) : Number of accesses removed. ```json { "external_id": "USR-GERANT-SOUBRE", "acces_retires": 1 } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | User unknown in your workspace, or already removed (`Utilisateur inconnu.`). | | `422` | Invalid `external_id` (characters or length). | > **info** A second `DELETE` on the same user returns `404`: you can treat it as a success. See [Manage users](/en/article/manage-users). --- # Send events URL: https://developers.stg.dafexpert.com/en/article/post-events > Sends one event or a batch of up to 500 events to post; they are processed in the background. Records the operations from your system that need to be posted: payments to farmers, advances, expenses, payroll… The `202` response comes back immediately; posting happens in the background. Then track each event with [GET /v1/partners/events/{id}](/en/article/get-event) or [GET /v1/partners/events](/en/article/list-events). The body is **either a single event** or **a batch** `{ "events": [...] }` of up to 500 events. In a batch, each event is validated on its own: an invalid event is reported without blocking the others. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Request body ### Batch - `version` (integer, body) : `1` or `2`: contract version applied to the events of the batch that do not set their own. Default: `1`. - `events` (array of object, body, required) : 1 to 500 events, in version 1 or 2 format. An event may carry its own `version` field. ### Event, version 2 Version 2 follows the [event catalog](/en/article/event-catalog): each `type` has its own fields (counterparty, documents referenced in `lie_a`, lines, payment method…). A single event sent on its own must carry `"version": 2`. Fields shared by all types: - `version` (integer, body, required) : `2` (optional inside a version 2 batch). - `id` (string, body, required) : Your identifier for the event, 1 to 100 characters, letters, digits and `. _ : -`. Deduplication key. - `type` (string, body, required) : `paiement_producteur`, `avance_producteur`, `remboursement_producteur`, `encaissement_client`, `cession_intrant`, `achat_fournisseur`, `depense`, `paie`, `souscription_parts`, `cotisation`, `inventaire`, `annulation` or `recolte`. - `entreprise` (string, body, required) : `external_id` of the company concerned. - `date` (string (date), body, required) : Operation date, `YYYY-MM-DD`. - `montant` (integer, body, required) : Total amount in whole FCFA, from 0 to 10 trillion. It must match the type details (sum of lines, of `lie_a`…). - `devise` (string, body) : Three-letter ISO code (`XOF`). If sent, it must be the company's currency. - `reference_origine` (string, body) : Reference in your system (receipt number…), at most 120 characters. - `analytique.campagne` (string, body) : At most 60 characters. Same for `analytique.parcelle`, `analytique.groupement` and `analytique.produit`. - `donnees` (object, body) : Free data from your system, at most 32,000 bytes as JSON. ### Event, version 1 Version 1 is still accepted: how each `type` is posted is configured at dafexpert. - `version` (integer, body) : `1`. Default: `1`. - `id` (string, body, required) : Your identifier for the event, 1 to 100 characters, letters, digits and `. _ : -`. Deduplication key. - `type` (string, body, required) : Event type, 1 to 60 characters: letters, digits and `_ . -`, starting with a letter or digit. - `exploitation` (string, body, required) : `external_id` of the company concerned. - `date` (string (date), body, required) : Operation date, `YYYY-MM-DD`. - `montant` (number, body, required) : Amount in the company's currency, at most two decimals. - `devise` (string, body) : Three-letter ISO code. If it differs from the company's currency, the event is `rejete`. - `quantites` (object, body) : Quantities per unit, for example `{ "kg": 1200 }` (key of at most 30 characters). - `donnees` (object, body) : Operation data used by the posting rules, at most 32,000 bytes as JSON. ## Request example A version 2 batch with a payment to a farmer, net of an advance deduction, by mobile money: **cURL** ```bash curl -X POST https://api.stg.dafexpert.com/v1/partners/events \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": 2, "events": [{ "id": "PAI-2026-000318", "type": "paiement_producteur", "entreprise": "COOP-SOUBRE", "date": "2026-10-09", "montant": 2010000, "tiers": { "role": "producteur", "id": "PROD-0457", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao", "telephone": "+225 07 58 21 44 10" }, "lie_a": [{ "id": "ACH-2026-000412", "montant": 2160000 }], "retenues": [{ "nature": "avance", "lie_a": "AV-2026-000051", "montant": 150000 }], "mode": "mobile_money", "operateur": "Orange Money", "reference_transaction": "OM-CI-8812734501", "caisse": "CAISSE-OM-SOUBRE", "analytique": { "campagne": "2026-2027", "produit": "cacao" } }] }' ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/events', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ version: 2, events: [{ id: 'PAI-2026-000318', type: 'paiement_producteur', entreprise: 'COOP-SOUBRE', date: '2026-10-09', montant: 2010000, tiers: { role: 'producteur', id: 'PROD-0457', entreprise: 'EXPL-0457', nom: 'KOUASSI', prenoms: 'Yao' }, lie_a: [{ id: 'ACH-2026-000412', montant: 2160000 }], retenues: [{ nature: 'avance', lie_a: 'AV-2026-000051', montant: 150000 }], mode: 'mobile_money', operateur: 'Orange Money', reference_transaction: 'OM-CI-8812734501', caisse: 'CAISSE-OM-SOUBRE', }], }), }); const { results } = await res.json(); // 202 ``` **Python** ```python import os, requests resp = requests.post( "https://api.stg.dafexpert.com/v1/partners/events", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "version": 2, "events": [{ "id": "PAI-2026-000318", "type": "paiement_producteur", "entreprise": "COOP-SOUBRE", "date": "2026-10-09", "montant": 2010000, "tiers": {"role": "producteur", "id": "PROD-0457", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao"}, "lie_a": [{"id": "ACH-2026-000412", "montant": 2160000}], "retenues": [{"nature": "avance", "lie_a": "AV-2026-000051", "montant": 150000}], "mode": "mobile_money", "operateur": "Orange Money", "reference_transaction": "OM-CI-8812734501", "caisse": "CAISSE-OM-SOUBRE", }], }, timeout=60, ) results = resp.json()["results"] # 202 ``` A single version 1 event (the `type` is the one from your configuration): ```json { "version": 1, "id": "9a2e41c7-0b5d-4f3e-8a6c-2d1e0f9b8a7c", "type": "achat_cacao", "exploitation": "COOP-SOUBRE", "date": "2026-10-08", "quantites": { "kg": 1200 }, "montant": 2160000, "devise": "XOF", "donnees": { "producteur": "EXPL-0457", "campagne": "2026-2027" } } ``` ## Response `202 Accepted`. One result per event, in the order sent. - `results` (array of object) : One result per event sent. - `results[].id` (string | null) : Your identifier for the event (`null` for an invalid event without a readable `id`). - `results[].statut` (string) : Current status: `recu`, `en_attente_entreprise`, `en_attente_piece`, `en_cours`, `comptabilise`, `partiel`, `rejete`, `ignore`, `annule`, `non_comptable`. Or, on receipt: `invalide` (not stored, batch only) or `refuse` (company or `id` from another workspace). - `results[].doublon` (boolean) : `true` if this `id` was already stored: the status returned is the current one. - `results[].version` (integer) : Number of the applied version of the event (`1` on first send). - `results[].contenu_different` (boolean) : Present (`true`) when the `id` was resent with different content. - `results[].version_appliquee` (boolean) : With `contenu_different`: `true` if the new version replaced the old one, `false` if it was kept without effect (event already posted or being processed). - `results[].message` (string) : Explanation when a new version is not applied. - `results[].erreur` (string) : Reason for a rejection or refusal. - `results[].index` (integer) : Invalid event in a batch: its position in `events` (zero-based). - `results[].erreurs` (array of object) : Invalid event in a batch: one `{ champ, message }` entry per rejected field. ```json { "results": [ { "id": "PAI-2026-000318", "statut": "recu", "doublon": false, "version": 1 }, { "id": "PAI-2026-000319", "statut": "comptabilise", "doublon": true, "version": 1 }, { "id": "PAI-2026-000320", "statut": "comptabilise", "doublon": true, "version": 1, "contenu_different": true, "version_appliquee": false, "message": "Nouvelle version reçue après comptabilisation : elle n'est pas appliquée. Pour corriger, envoyer une annulation puis un nouvel événement." }, { "index": 3, "id": "PAI-2026-000321", "statut": "invalide", "erreurs": [{ "champ": "reference_transaction", "message": "reference_transaction obligatoire pour un paiement mobile money." }] }, { "id": "PAI-2026-000322", "statut": "refuse", "erreur": "L'exploitation de cet événement est rattachée à un autre espace." } ] } ``` ### Behaviors to know - **Duplicate**: resending an event already received changes nothing and returns its current status. You can replay a whole batch after an outage. - **New version**: the same `id` with different content replaces the event as long as it is not posted. Once posted, send an `annulation` then a new event. See [Event versions](/en/article/event-versions). - **Company not created yet**: the event is kept as `en_attente_entreprise` and released when the company is created. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `422` | Invalid single event (details in `erreurs`), or malformed batch (`events` empty or longer than 500 items, `version` other than 1 or 2). Nothing is stored. | > **info** In a batch, an invalid event does not cause a `422`: it comes back with `"statut": "invalide"` and the others are stored. See [Send events](/en/article/send-events) and [Track events](/en/article/track-events). --- # Retrieve an event URL: https://developers.stg.dafexpert.com/en/article/get-event > Posting status of an event, with its documents and journal entries per company. Returns the current state of an event sent with [POST /v1/partners/events](/en/article/post-events): its status, the number of the journal entry posted, the reason for a rejection and, in version 2, the documents and entries produced for each company concerned. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `id` (string, path, required) : Your identifier for the event, the one sent in the `id` field. ## Request example **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/events/PAI-2026-000318 \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const res = await fetch('https://api.stg.dafexpert.com/v1/partners/events/PAI-2026-000318', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN }, }); const event = await res.json(); console.log(event.statut, event.erreur); ``` **Python** ```python import os, requests resp = requests.get( "https://api.stg.dafexpert.com/v1/partners/events/PAI-2026-000318", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) event = resp.json() print(event["statut"], event["erreur"]) ``` ## Response `200 OK` - `id` (string) : Your identifier for the event. - `type` (string) : Event type. - `exploitation` (string) : The company's `external_id` (`exploitation` field in v1, `entreprise` in v2). - `date` (string (date)) : Operation date. - `montant` (number) : Event amount. - `devise` (string | null) : Currency sent, or `null`. - `statut` (string) : `recu`, `en_attente_entreprise`, `en_attente_piece`, `en_cours`, `comptabilise`, `partiel`, `rejete`, `ignore`, `annule` or `non_comptable`. See [Track events](/en/article/track-events). - `erreur` (string | null) : Reason for the rejection or discrepancy, otherwise `null`. - `ecriture` (string | null) : Document number of the journal entry posted, otherwise `null`. - `recu_le` (string (date-time)) : Time received, in UTC. - `mis_a_jour_le` (string (date-time)) : Time of the last change, in UTC. - `version` (integer) : Number of the applied version (`1` = first send). - `version_non_appliquee` (object) : Version received but not applied: `version` (integer) and `message` (string). - `en_attente_de` (array of string) : Version 2: identifiers of referenced documents that have not arrived yet. - `effets` (array of object) : Version 2: one effect per company concerned. - `effets[].entreprise` (string) : The company's `external_id`. - `effets[].role` (string) : The company's role in the operation (`payeur`, `preteur`, `beneficiaire`, `cedant`, `acheteur`, `employeur`, `cooperative`…). - `effets[].action` (string) : `creee` (document created), `rattachee` (existing document linked) or `ecriture` (journal entry only). - `effets[].statut` (string) : `comptabilise`, `en_cours` or `rejete`. - `effets[].piece` (object | null) : Document produced: `type` (`supplier_invoice`, `supplier_settlement`, `settlement` or `entry`), `numero` and `numero_fiscal` (the last two may be `null`). - `effets[].ecriture` (string | null) : Document number of the journal entry. - `effets[].message` (string | null) : Reason for a rejection, or a summary of what was done. ```json { "id": "PAI-2026-000318", "type": "paiement_producteur", "exploitation": "COOP-SOUBRE", "date": "2026-10-09", "montant": 2010000, "devise": null, "statut": "comptabilise", "erreur": null, "ecriture": null, "recu_le": "2026-10-09T16:02:11.304Z", "mis_a_jour_le": "2026-10-09T16:02:14.877Z", "version": 1, "effets": [ { "entreprise": "COOP-SOUBRE", "role": "payeur", "action": "creee", "statut": "comptabilise", "piece": { "type": "supplier_settlement", "numero": "OM-CI-8812734501", "numero_fiscal": null }, "ecriture": null, "message": "Payé 2010000 FCFA (mobile money), retenues 150000 FCFA." } ] } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Event unknown in your workspace (`Événement inconnu.`). | | `422` | Invalid identifier (characters or length). | To track many events, prefer cursor-based sync: [GET /v1/partners/events](/en/article/list-events). --- # Sync events URL: https://developers.stg.dafexpert.com/en/article/list-events > Lists events changed since a cursor, oldest first, so you catch every status change. Returns the events of your workspace **changed** after the `apres` cursor, oldest first. Keep the last `suivant` you received and pass it on the next call: you get every status change, without missing any and without duplicates. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Query parameters - `apres` (string, query) : Opaque cursor returned in `suivant` by the previous call, at most 200 characters. Omitted: from the beginning. - `limite` (integer, query) : Events per page, from 1 to 500. Default: `100`. - `statut` (string, query) : Only returns events with this status: `recu`, `en_attente_entreprise`, `en_attente_piece`, `en_cours`, `comptabilise`, `partiel`, `rejete`, `ignore`, `annule` or `non_comptable`. ## Request example **cURL** ```bash curl "https://api.stg.dafexpert.com/v1/partners/events?limite=100&apres=MjAyNi0xMC0wOVQxNjowMjoxNC44NzdafDNm..." \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript // Sync loop: the cursor is persisted between runs. let cursor = await loadCursor(); // null on the first run for (;;) { const url = new URL('https://api.stg.dafexpert.com/v1/partners/events'); url.searchParams.set('limite', '500'); if (cursor) url.searchParams.set('apres', cursor); const res = await fetch(url, { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN }, }); const { evenements, suivant } = await res.json(); for (const e of evenements) await updateLocalStatus(e.id, e.statut, e.erreur); if (!suivant) break; cursor = suivant; await saveCursor(cursor); } ``` **Python** ```python import os, requests cursor = load_cursor() # None on the first run while True: params = {"limite": 500} if cursor: params["apres"] = cursor resp = requests.get( "https://api.stg.dafexpert.com/v1/partners/events", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, params=params, timeout=30, ) page = resp.json() for e in page["evenements"]: update_local_status(e["id"], e["statut"], e["erreur"]) if not page["suivant"]: break cursor = page["suivant"] save_cursor(cursor) ``` ## Response `200 OK` - `evenements` (array of object) : Changed events, oldest first. Each item has the same shape as the [GET /v1/partners/events/{id}](/en/article/get-event) response (`id`, `type`, `exploitation`, `statut`, `erreur`, `ecriture`, `effets`…). - `suivant` (string | null) : Cursor to pass as `apres` for the next page. `null` when the page is not full: everything has been read for now. ```json { "evenements": [ { "id": "PAI-2026-000318", "type": "paiement_producteur", "exploitation": "COOP-SOUBRE", "date": "2026-10-09", "montant": 2010000, "devise": null, "statut": "comptabilise", "erreur": null, "ecriture": null, "recu_le": "2026-10-09T16:02:11.304Z", "mis_a_jour_le": "2026-10-09T16:02:14.877Z", "version": 1 }, { "id": "DEP-2026-001207", "type": "depense", "exploitation": "COOP-SOUBRE", "date": "2026-10-10", "montant": 45000, "devise": "EUR", "statut": "rejete", "erreur": "Devise EUR différente de celle de l'entreprise (XOF).", "ecriture": null, "recu_le": "2026-10-10T08:41:52.019Z", "mis_a_jour_le": "2026-10-10T08:41:53.660Z", "version": 1 } ], "suivant": null } ``` > **tips** An event shows up again in the sync each time its status changes. Simply update your local copy with the latest value received. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `422` | Invalid parameter: unknown `statut`, `limite` outside 1 to 500, or unreadable `apres` cursor (`Curseur invalide.`). | See [Track events](/en/article/track-events). --- # Issue a purchase slip URL: https://developers.stg.dafexpert.com/en/article/post-bordereau > Issues a purchase slip for farm produce, certifies it with the FNE, renders its PDF and posts it, in a single call. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Issues the purchase slip (*bordereau d'achat*) for farm produce that the buying company (`external_id`) hands to a farmer. The call is **synchronous**: the response directly gives the number, the tax number, the verification QR code link, the PDF link and the posting status. - The slip is recorded as a supplier invoice of the buyer, numbered `BA-`. - In Côte d'Ivoire, it is certified with the FNE (standardized electronic invoice, *purchase* model). It is only posted once certified. - Outside Côte d'Ivoire, it is issued and posted without certification (`statut`: `emise`). - If the farmer is also a company of the workspace (`tiers.entreprise`), the matching sale is also recorded in their books (`miroir`). The call is **idempotent** on your `id`: `201` on first issue, `200` on a replay, which returns the same slip and the same tax number. A replay also retries a missing certification or posting. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the buying company that issues the slip, created with [PUT /v1/partners/companies/{external_id}](/en/article/put-company). ## Request body - `id` (string, body, required) : Your identifier for the slip, 1 to 100 characters, letters, digits and `. _ : -`. Idempotency key. - `date` (string (date), body, required) : Slip date, `YYYY-MM-DD`. - `montant` (integer, body, required) : Slip total in whole FCFA. Must equal the sum of `lignes[].montant`. - `paiement_prevu` (string, body) : Planned payment: `especes` (cash), `mobile_money`, `credit`, `virement` (transfer) or `cheque`. - `reference_origine` (string, body) : Reference in your system, at most 120 characters. - `analytique.campagne` (string, body) : Crop season, at most 60 characters. Same for `analytique.parcelle`, `analytique.groupement` and `analytique.produit`. - `donnees` (object, body) : Free data from your system. ### Counterparty (the farmer) - `tiers.role` (string, body, required) : `producteur` (the only value accepted for a slip). - `tiers.id` (string, body, required) : Your identifier for the farmer, 1 to 100 characters. - `tiers.nom` (string, body) : Family name, at most 200 characters. `nom` or `raison_sociale` is required. - `tiers.prenoms` (string, body) : Given names, at most 120 characters. - `tiers.raison_sociale` (string, body) : For a farmer operating as a company, at most 200 characters. - `tiers.entreprise` (string, body) : The farmer's `external_id` if they are also a company of the workspace (tracked farmer). - `tiers.telephone` (string, body) : At most 40 characters. Required by the FNE to deliver the slip. - `tiers.email` (string (email), body) : Farmer's email address. - `tiers.cni` (string, body) : ID card number, at most 40 characters. - `tiers.id_producteur` (string, body) : Value chain identifier, at most 60 characters. - `tiers.ncc` (string, body) : Taxpayer number, at most 30 characters. - `tiers.pays` (string, body) : Two-letter ISO code. ### Lines - `lignes` (array of object, body, required) : 1 to 200 lines. - `lignes[].produit` (string, body, required) : Product bought (`Cacao fèves`…), 1 to 60 characters. - `lignes[].qualite` (string, body) : Grade or quality, at most 40 characters. - `lignes[].quantite` (number, body, required) : Quantity paid, strictly positive (decimals accepted). - `lignes[].unite` (string, body) : `kg`, `t`, `sac`…, at most 20 characters. - `lignes[].prix_unitaire` (integer, body, required) : Unit price in whole FCFA. - `lignes[].montant` (integer, body, required) : Line amount in whole FCFA: `quantite × prix_unitaire`, to the nearest franc. - `lignes[].quantite_pesee` (number, body) : Gross weighed quantity, strictly positive. - `lignes[].refaction_pct` (number, body) : Quality deduction applied, in %, from 0 to 100. - `lignes[].humidite_pct` (number, body) : Moisture rate, in %, from 0 to 100. - `lignes[].nombre_sacs` (integer, body) : Number of bags, zero or positive. ## Request example **cURL** ```bash curl -X POST https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "ACH-2026-000412", "date": "2026-10-08", "tiers": { "role": "producteur", "id": "PROD-0457", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao", "telephone": "+225 07 58 21 44 10" }, "lignes": [{ "produit": "Cacao fèves", "qualite": "Grade 1", "quantite_pesee": 1236, "humidite_pct": 7.5, "nombre_sacs": 19, "quantite": 1200, "unite": "kg", "prix_unitaire": 1800, "montant": 2160000 }], "montant": 2160000, "paiement_prevu": "mobile_money", "analytique": { "campagne": "2026-2027", "produit": "cacao" } }' ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ id: 'ACH-2026-000412', date: '2026-10-08', tiers: { role: 'producteur', id: 'PROD-0457', entreprise: 'EXPL-0457', nom: 'KOUASSI', prenoms: 'Yao', telephone: '+225 07 58 21 44 10' }, lignes: [{ produit: 'Cacao fèves', qualite: 'Grade 1', quantite: 1200, unite: 'kg', prix_unitaire: 1800, montant: 2160000 }], montant: 2160000, paiement_prevu: 'mobile_money', }), }, ); const slip = await res.json(); // 201, or 200 on a replay console.log(slip.numero_fiscal, slip.statut); ``` **Python** ```python import os, requests resp = requests.post( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "id": "ACH-2026-000412", "date": "2026-10-08", "tiers": {"role": "producteur", "id": "PROD-0457", "entreprise": "EXPL-0457", "nom": "KOUASSI", "prenoms": "Yao", "telephone": "+225 07 58 21 44 10"}, "lignes": [{"produit": "Cacao fèves", "qualite": "Grade 1", "quantite": 1200, "unite": "kg", "prix_unitaire": 1800, "montant": 2160000}], "montant": 2160000, "paiement_prevu": "mobile_money", }, timeout=60, ) slip = resp.json() # 201, or 200 on a replay ``` ## Response `201 Created` on first issue, `200 OK` on a replay of the same `id`. - `id` (string) : Your identifier for the slip. - `type` (string) : `bordereau_achat`. - `piece_id` (string (uuid)) : Document identifier at dafexpert. - `statut` (string) : `certifie` (FNE-certified), `non_certifie` (Côte d'Ivoire, certification failed: see `erreur`) or `emise` (country without certification). - `numero` (string) : Slip number: `BA-` followed by your `id`. - `numero_fiscal` (string | null) : Number assigned by the FNE, `null` until the slip is certified. - `qr` (string | null) : FNE verification link, to print as a QR code on the slip handed to the farmer. - `lien_pdf` (string) : Path of the route that returns the PDF download link: [GET …/bordereaux/{id}/pdf](/en/article/get-bordereau-pdf). - `comptabilisation` (string) : `comptabilise` (posted) or `non_comptabilise` (not posted). - `montant` (integer) : Slip total in FCFA. - `erreur` (string) : Reason for a certification or posting failure. Absent when everything succeeded. - `miroir` (object) : Present when the farmer is a company of the workspace: the sale recorded in their books. `entreprise` (their `external_id`), `statut` (`comptabilise`, `non_comptabilise` or `rejete`), `numero` and `erreur` (optional). ```json { "id": "ACH-2026-000412", "type": "bordereau_achat", "piece_id": "6e0b3c2a-8f1d-4a7e-b5c9-2d4f6a8b0c1e", "statut": "certifie", "numero": "BA-ACH-2026-000412", "numero_fiscal": "…", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412/pdf", "comptabilisation": "comptabilise", "montant": 2160000, "miroir": { "entreprise": "EXPL-0457", "statut": "comptabilise", "numero": "…" } } ``` > **warning** A certification failure does not return an HTTP error: the slip is stored with `"statut": "non_certifie"` and the reason in `erreur` (FNE not connected, missing phone number…). Fix the cause, then resend the same call: it retries certification without creating a second slip. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Company unknown in your workspace (create it first with `PUT /v1/partners/companies/{external_id}`). | | `409` | Slip being issued by another call (retry in a few seconds with the same `id`), or workspace without an active owner or administrator. | | `422` | Invalid data: counterparty that is not a `producteur`, both `nom` and `raison_sociale` missing, line amount different from `quantite × prix_unitaire`, `montant` different from the sum of lines, non-integer amount… Or issue refused, with the reason. | ```json { "statusCode": 422, "error_code": "validation_failed", "message": "Données invalides.", "erreurs": [ { "champ": "montant", "message": "montant (2100000) différent de la somme des lignes (2160000)." } ] } ``` See [FNE tax documents](/en/article/fne-documents) and [Amounts and currencies](/en/article/amounts-currencies). --- # Retrieve a purchase slip URL: https://developers.stg.dafexpert.com/en/article/get-bordereau > Current state of a purchase slip: FNE certification, tax number, posting and mirror sale. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Returns the current state of a slip issued with [POST …/bordereaux](/en/article/post-bordereau). Useful to check a certification or posting still pending. This route does not retry anything: to retry, resend the `POST` with the same `id`. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the company that issued the slip. - `id` (string, path, required) : Your identifier for the slip, the one sent in the `id` field. ## Request example **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412 \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN } }, ); const slip = await res.json(); ``` **Python** ```python import os, requests resp = requests.get( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) slip = resp.json() ``` ## Response `200 OK`. Same shape as the issue response. - `id` (string) : Your identifier for the slip. - `type` (string) : `bordereau_achat`. - `piece_id` (string (uuid)) : Document identifier at dafexpert. - `statut` (string) : `certifie`, `non_certifie` or `emise`. - `numero` (string) : Slip number (`BA-` followed by your `id`). - `numero_fiscal` (string | null) : Number assigned by the FNE. - `qr` (string | null) : FNE verification link. - `lien_pdf` (string) : Path of the [GET …/bordereaux/{id}/pdf](/en/article/get-bordereau-pdf) route. - `comptabilisation` (string) : `comptabilise` or `non_comptabilise`. - `montant` (integer) : Slip total in FCFA. - `erreur` (string) : Last recorded certification failure reason. - `miroir` (object) : Sale recorded in the farmer's books when they are a company of the workspace: `entreprise`, `statut` (`comptabilise` or `non_comptabilise`), `numero`. ```json { "id": "ACH-2026-000412", "type": "bordereau_achat", "piece_id": "6e0b3c2a-8f1d-4a7e-b5c9-2d4f6a8b0c1e", "statut": "non_certifie", "numero": "BA-ACH-2026-000412", "numero_fiscal": null, "qr": null, "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412/pdf", "comptabilisation": "non_comptabilise", "montant": 2160000, "erreur": "FNE non connectée pour cette entreprise : la pièce est enregistrée mais pas certifiée." } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Company unknown in your workspace, or slip unknown for this company (`Pièce inconnue pour cette entreprise.`). | | `409` | Workspace without an active owner or administrator. | | `422` | Invalid identifier (characters or length). | See [FNE tax documents](/en/article/fne-documents). --- # Download a purchase slip PDF URL: https://developers.stg.dafexpert.com/en/article/get-bordereau-pdf > Returns a temporary download link (5 minutes) for a purchase slip PDF. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Returns a download link for a purchase slip PDF, valid for **5 minutes**. The certified PDF is returned if it exists, otherwise the original PDF. This is the route given in the `lien_pdf` field of slip responses. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the company that issued the slip. - `id` (string, path, required) : Your identifier for the slip. ## Request example **cURL** ```bash # 1. Get the link URL=$(curl -s https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412/pdf \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" | jq -r .url) # 2. Download the PDF (no Authorization header) curl -o BA-ACH-2026-000412.pdf "$URL" ``` **Node.js** ```javascript import { writeFile } from 'node:fs/promises'; const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412/pdf', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN } }, ); const { url } = await res.json(); const pdf = await fetch(url); // signed link: no Authorization header await writeFile('BA-ACH-2026-000412.pdf', Buffer.from(await pdf.arrayBuffer())); ``` **Python** ```python import os, requests link = requests.get( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/bordereaux/ACH-2026-000412/pdf", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ).json() pdf = requests.get(link["url"], timeout=60) # signed link: no Authorization header with open("BA-ACH-2026-000412.pdf", "wb") as f: f.write(pdf.content) ``` ## Response `200 OK` - `url` (string) : Signed download link for the PDF. Call it without an `Authorization` header. - `expiresIn` (integer) : Link lifetime, in seconds (`300`). ```json { "url": "https://…", "expiresIn": 300 } ``` > **tips** Do not store the link: it expires after 5 minutes. Store the PDF itself instead, or call this route again when you need to download it. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Unknown company or slip, or PDF not available yet (`PDF pas encore disponible pour cette pièce.`). | | `409` | Workspace without an active owner or administrator. | | `422` | Invalid identifier (characters or length). | See [FNE tax documents](/en/article/fne-documents). --- # Issue an invoice URL: https://developers.stg.dafexpert.com/en/article/post-invoice > Issues a customer invoice numbered by dafexpert, certifies it with the FNE and posts it, in a single call. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Issues an invoice from the `external_id` company to one of its customers (sale of produce, inputs…). The call is **synchronous**: the response gives the number assigned by dafexpert, the tax number, the verification QR code link, the PDF link and the posting status. - The invoice is numbered in the company's sequence and goes straight to the « sent » status: you are the one handing it to the customer. - In Côte d'Ivoire, it is certified with the FNE, then posted once certified. - Outside Côte d'Ivoire, it is issued and posted without certification (`statut`: `emise`). The call is **idempotent** on your `id`: `201` on first issue, `200` on a replay, which returns the same invoice, number and tax number. A replay also retries a missing certification or posting. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the company issuing the invoice. ## Request body - `id` (string, body, required) : Your identifier for the invoice, 1 to 100 characters, letters, digits and `. _ : -`. Idempotency key. - `date` (string (date), body, required) : Issue date, `YYYY-MM-DD`. - `echeance` (string (date), body) : Due date, `YYYY-MM-DD`. - `montant` (integer, body, required) : Total **including tax** in whole FCFA: sum of the lines' net amounts plus each line's VAT (rounded to the franc). A difference of one franc per line is tolerated. - `mode_paiement` (string, body) : Payment method: `especes` (cash), `mobile_money`, `virement` (transfer), `cheque` or `carte` (card). - `reference_origine` (string, body) : Reference in your system (delivery note, contract…), at most 120 characters. Printed on the invoice. - `analytique.campagne` (string, body) : At most 60 characters. Same for `analytique.parcelle`, `analytique.groupement` and `analytique.produit`. - `donnees` (object, body) : Free data from your system. ### Counterparty (the customer) - `tiers.role` (string, body, required) : `client` (the only value accepted for an invoice). - `tiers.id` (string, body, required) : Your identifier for the customer, 1 to 100 characters. - `tiers.raison_sociale` (string, body) : Legal name, at most 200 characters. `raison_sociale` or `nom` is required. - `tiers.nom` (string, body) : Name of an individual customer, at most 200 characters. - `tiers.prenoms` (string, body) : Given names, at most 120 characters. - `tiers.ncc` (string, body) : Customer's taxpayer number, at most 30 characters. - `tiers.pays` (string, body) : Two-letter ISO code. Default: the company's country. - `tiers.entreprise` (string, body) : The customer's `external_id` if they are also a company of the workspace. - `tiers.telephone` (string, body) : At most 40 characters. - `tiers.email` (string (email), body) : Customer's email address. - `tiers.cni` (string, body) : ID card number, at most 40 characters. Same for `tiers.id_producteur` (60 characters). ### Lines - `lignes` (array of object, body, required) : 1 to 200 lines. - `lignes[].produit` (string, body) : At most 60 characters. `produit` or `description` is required. - `lignes[].description` (string, body) : Free description, at most 255 characters. - `lignes[].qualite` (string, body) : Grade or quality, at most 40 characters. Added in parentheses to the description. - `lignes[].quantite` (number, body, required) : Quantity, strictly positive. A decimal quantity is printed in the description and the line becomes `1 × montant`. - `lignes[].unite` (string, body) : At most 20 characters. - `lignes[].prix_unitaire` (integer, body, required) : Net unit price in whole FCFA. - `lignes[].taux_tva` (number, body) : VAT rate in %, from 0 to 100. Default: `0`. - `lignes[].montant` (integer, body, required) : Line **net** amount in whole FCFA: `quantite × prix_unitaire`, to the nearest franc. ## Request example **cURL** ```bash curl -X POST https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "VTE-2026-000087", "date": "2026-10-10", "echeance": "2026-11-09", "tiers": { "role": "client", "id": "CLI-IVOIRE-CACAO-EXPORT", "raison_sociale": "Ivoire Cacao Export SA", "ncc": "1856432B", "pays": "CI", "email": "achats@ivoirecacaoexport.ci" }, "lignes": [ { "produit": "Cacao fèves", "qualite": "Grade 1", "quantite": 25000, "unite": "kg", "prix_unitaire": 1850, "taux_tva": 0, "montant": 46250000 }, { "description": "Transport Soubré - San-Pedro", "quantite": 1, "prix_unitaire": 500000, "taux_tva": 18, "montant": 500000 } ], "montant": 46840000, "mode_paiement": "virement", "reference_origine": "BL-SBR-2026-0193" }' ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ id: 'VTE-2026-000087', date: '2026-10-10', echeance: '2026-11-09', tiers: { role: 'client', id: 'CLI-IVOIRE-CACAO-EXPORT', raison_sociale: 'Ivoire Cacao Export SA', ncc: '1856432B', pays: 'CI' }, lignes: [ { produit: 'Cacao fèves', qualite: 'Grade 1', quantite: 25000, unite: 'kg', prix_unitaire: 1850, taux_tva: 0, montant: 46250000 }, { description: 'Transport Soubré - San-Pedro', quantite: 1, prix_unitaire: 500000, taux_tva: 18, montant: 500000 }, ], montant: 46840000, // 46,250,000 + 500,000 + 90,000 VAT mode_paiement: 'virement', }), }, ); const invoice = await res.json(); // 201, or 200 on a replay ``` **Python** ```python import os, requests resp = requests.post( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, json={ "id": "VTE-2026-000087", "date": "2026-10-10", "echeance": "2026-11-09", "tiers": {"role": "client", "id": "CLI-IVOIRE-CACAO-EXPORT", "raison_sociale": "Ivoire Cacao Export SA", "ncc": "1856432B", "pays": "CI"}, "lignes": [ {"produit": "Cacao fèves", "qualite": "Grade 1", "quantite": 25000, "unite": "kg", "prix_unitaire": 1850, "taux_tva": 0, "montant": 46250000}, {"description": "Transport Soubré - San-Pedro", "quantite": 1, "prix_unitaire": 500000, "taux_tva": 18, "montant": 500000}, ], "montant": 46840000, # 46,250,000 + 500,000 + 90,000 VAT "mode_paiement": "virement", }, timeout=60, ) invoice = resp.json() # 201, or 200 on a replay ``` ## Response `201 Created` on first issue, `200 OK` on a replay of the same `id`. - `id` (string) : Your identifier for the invoice. - `type` (string) : `facture`. - `piece_id` (string (uuid)) : Invoice identifier at dafexpert. - `statut` (string) : `certifie` (FNE-certified), `non_certifie` (Côte d'Ivoire, certification failed: see `erreur`) or `emise` (country without certification). - `numero` (string) : Number assigned by dafexpert in the company's invoice sequence. - `numero_fiscal` (string | null) : Number assigned by the FNE, `null` until the invoice is certified. - `qr` (string | null) : FNE verification link, to print as a QR code. - `lien_pdf` (string) : Path of the route that returns the PDF download link: [GET …/invoices/{id}/pdf](/en/article/get-invoice-pdf). - `comptabilisation` (string) : `comptabilise` (posted) or `non_comptabilise` (not posted). - `montant` (integer) : Invoice total including tax in FCFA, recomputed from the lines. - `erreur` (string) : Reason for a certification or posting failure. Absent when everything succeeded. ```json { "id": "VTE-2026-000087", "type": "facture", "piece_id": "2f7a9c1e-0b3d-4e5f-a6b7-c8d9e0f1a2b3", "statut": "certifie", "numero": "FAC-2026-000087", "numero_fiscal": "…", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087/pdf", "comptabilisation": "comptabilise", "montant": 46840000 } ``` > **warning** A certification failure does not return an HTTP error: the invoice is stored with `"statut": "non_certifie"` and the reason in `erreur`. Fix the cause (the company's FNE connection, customer data…), then resend the same call: it retries certification without creating a second invoice. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Company unknown in your workspace (create it first with `PUT /v1/partners/companies/{external_id}`). | | `409` | Invoice being issued by another call (retry in a few seconds with the same `id`), or workspace without an active owner or administrator. | | `422` | Invalid data: counterparty that is not a `client`, line without `produit` or `description`, line amount different from `quantite × prix_unitaire`, `montant` different from the total computed from the lines, non-integer amount… Or issue refused, with the reason. | ```json { "statusCode": 422, "error_code": "validation_failed", "message": "Données invalides.", "erreurs": [ { "champ": "montant", "message": "montant TTC (46750000) différent du calcul des lignes (46840000)." } ] } ``` See [FNE tax documents](/en/article/fne-documents) and [Amounts and currencies](/en/article/amounts-currencies). --- # Retrieve an invoice URL: https://developers.stg.dafexpert.com/en/article/get-invoice > Current state of an invoice: number, FNE certification and posting. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Returns the current state of an invoice issued with [POST …/invoices](/en/article/post-invoice). This route does not retry anything: to retry a certification or posting, resend the `POST` with the same `id`. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the company that issued the invoice. - `id` (string, path, required) : Your identifier for the invoice, the one sent in the `id` field. ## Request example **cURL** ```bash curl https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087 \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" ``` **Node.js** ```javascript const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN } }, ); const invoice = await res.json(); ``` **Python** ```python import os, requests resp = requests.get( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ) invoice = resp.json() ``` ## Response `200 OK`. Same shape as the issue response. - `id` (string) : Your identifier for the invoice. - `type` (string) : `facture`. - `piece_id` (string (uuid)) : Invoice identifier at dafexpert. - `statut` (string) : `certifie`, `non_certifie` or `emise`. - `numero` (string) : Invoice number. - `numero_fiscal` (string | null) : Number assigned by the FNE. - `qr` (string | null) : FNE verification link. - `lien_pdf` (string) : Path of the [GET …/invoices/{id}/pdf](/en/article/get-invoice-pdf) route. - `comptabilisation` (string) : `comptabilise` or `non_comptabilise`. - `montant` (integer) : Total including tax in FCFA. - `erreur` (string) : Last recorded certification failure reason. ```json { "id": "VTE-2026-000087", "type": "facture", "piece_id": "2f7a9c1e-0b3d-4e5f-a6b7-c8d9e0f1a2b3", "statut": "certifie", "numero": "FAC-2026-000087", "numero_fiscal": "…", "qr": "https://…", "lien_pdf": "/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087/pdf", "comptabilisation": "comptabilise", "montant": 46840000 } ``` ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Company unknown in your workspace, or invoice unknown for this company (`Pièce inconnue pour cette entreprise.`). | | `409` | Workspace without an active owner or administrator. | | `422` | Invalid identifier (characters or length). | See [FNE tax documents](/en/article/fne-documents). --- # Download an invoice PDF URL: https://developers.stg.dafexpert.com/en/article/get-invoice-pdf > Returns a temporary download link (5 minutes) for an invoice PDF. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. Returns a download link for an invoice PDF, valid for **5 minutes**. The certified PDF is returned if it exists, otherwise the original PDF. This is the route given in the `lien_pdf` field of invoice responses. | Authentication | Value | |---|---| | Header | `Authorization: Bearer ` | | Token | OAuth 2.0 client credentials, see [Authentication](/en/article/authentication) | | Scopes | None | ## Path parameters - `external_id` (string, path, required) : `external_id` of the company that issued the invoice. - `id` (string, path, required) : Your identifier for the invoice. ## Request example **cURL** ```bash # 1. Get the link URL=$(curl -s https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087/pdf \ -H "Authorization: Bearer $DAFEXPERT_TOKEN" | jq -r .url) # 2. Download the PDF (no Authorization header) curl -o FAC-2026-000087.pdf "$URL" ``` **Node.js** ```javascript import { writeFile } from 'node:fs/promises'; const res = await fetch( 'https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087/pdf', { headers: { Authorization: 'Bearer ' + process.env.DAFEXPERT_TOKEN } }, ); const { url } = await res.json(); const pdf = await fetch(url); // signed link: no Authorization header await writeFile('FAC-2026-000087.pdf', Buffer.from(await pdf.arrayBuffer())); ``` **Python** ```python import os, requests link = requests.get( "https://api.stg.dafexpert.com/v1/partners/companies/COOP-SOUBRE/invoices/VTE-2026-000087/pdf", headers={"Authorization": f"Bearer {os.environ['DAFEXPERT_TOKEN']}"}, timeout=30, ).json() pdf = requests.get(link["url"], timeout=60) # signed link: no Authorization header with open("FAC-2026-000087.pdf", "wb") as f: f.write(pdf.content) ``` ## Response `200 OK` - `url` (string) : Signed download link for the PDF. Call it without an `Authorization` header. - `expiresIn` (integer) : Link lifetime, in seconds (`300`). ```json { "url": "https://…", "expiresIn": 300 } ``` > **tips** Do not store the link: it expires after 5 minutes. Store the PDF itself instead, or call this route again when you need to download it. ## Errors | Code | Case | |---|---| | `401` | Missing or expired token, or access revoked. | | `404` | Unknown company or invoice, or PDF not available yet (`PDF pas encore disponible pour cette pièce.`). | | `409` | Workspace without an active owner or administrator. | | `422` | Invalid identifier (characters or length). | See [FNE tax documents](/en/article/fne-documents). --- # MCP server URL: https://developers.stg.dafexpert.com/en/article/mcp-overview > Connect an AI assistant (Claude and others) to a user's dafexpert companies, read-only. The dafexpert MCP (Model Context Protocol) server lets a compatible AI assistant, such as Claude, read a user's data: companies, fiscal years, trial balance, income statement, customer invoices and cash position. The assistant acts **on behalf of the signed-in user** and only sees what that user sees in dafexpert. For a step-by-step setup in Claude, see [Connect an AI assistant](/en/article/connect-ai-assistant). ## URL | Environment | MCP server URL | |---|---| | Staging | `https://api.stg.dafexpert.com/mcp` | | Production | shared at go-live | ## Transport - **Streamable HTTP**, stateless: every JSON-RPC request is sent as a `POST` to the server URL, and no session is kept between calls. - `GET` and `DELETE` return `405` (no standalone SSE stream and no session to close). - The URL is not versioned (no `/v1` prefix). ## Authentication The server is protected by OAuth 2.0 using the **Authorization Code + PKCE (S256)** flow. The user signs in with their usual dafexpert account, then approves access on a consent screen. | Item | Value | |---|---| | `client_id` | `dafexpert-mcp` (public client, no secret) | | Authorization server (staging) | `https://auth.stg.dafexpert.com/realms/dafexpert` | | Scopes | `openid`, `profile`, `email` | | Token | `Authorization: Bearer ` header | > **warning** Dynamic client registration (DCR) is not supported. In your assistant's connector advanced settings, enter the Client ID `dafexpert-mcp` and leave the secret empty. **Automatic discovery.** A call without a valid token gets a `401` with the header below, which lets the MCP client find the authorization server (RFC 9728) and start the sign-in on its own: ```http WWW-Authenticate: Bearer resource_metadata="https://api.stg.dafexpert.com/.well-known/oauth-protected-resource/mcp" ``` ```json { "resource": "https://api.stg.dafexpert.com/mcp", "authorization_servers": ["https://auth.stg.dafexpert.com/realms/dafexpert"], "scopes_supported": ["openid", "profile", "email"], "bearer_methods_supported": ["header"], "resource_name": "DafExpert" } ``` ## Read-only All tools are **read-only** (annotated `readOnlyHint: true`): the assistant cannot create, update or delete anything. Write tools are coming later. ## Visible data - The assistant only sees the companies the user is a **member** of, exactly as in the app. - Each tool takes the company's `companyId` and checks the user's access on every call. A company outside their permissions returns a tool error (`isError: true`) with a message. - Removing a user from a company in dafexpert immediately cuts their assistant's access to it. ## Amounts and dates - Fields ending in `Cents` are in **hundredths** of the company's currency: `150000000` = 1,500,000 FCFA. - The trial balance (`get_balance`) and income statement (`get_income_statement`) return amounts **in currency units** (`1500000` = 1,500,000 FCFA). - Input dates use `YYYY-MM-DD`; output dates are ISO 8601 timestamps. ## Tools The assistant usually starts with `list_companies` to get the `companyId`, then `list_exercises` for the accounting tools. | Tool | Purpose | Parameters | |---|---|---| | [list_companies](/en/article/mcp-list-companies) | List accessible companies | none | | [list_exercises](/en/article/mcp-list-exercises) | List fiscal years | `companyId` | | [get_balance](/en/article/mcp-get-balance) | Trial balance, current and prior year | `companyId`, `exerciseId`, `from`, `to` | | [get_balance_anomalies](/en/article/mcp-get-balance-anomalies) | Review checks on the trial balance | `companyId`, `exerciseId` | | [get_income_statement](/en/article/mcp-get-income-statement) | Income statement by line item | `companyId`, `exerciseId`, `from`, `to` | | [list_invoices](/en/article/mcp-list-invoices) | List customer invoices | `companyId`, `status` | | [get_invoice](/en/article/mcp-get-invoice) | Customer invoice details | `companyId`, `invoiceId` | | [get_treasury_overview](/en/article/mcp-get-treasury-overview) | Cash position overview | `companyId`, `range` | ## Try it with Claude Code 1. Add the server: `claude mcp add --transport http dafexpert https://api.stg.dafexpert.com/mcp`. 1. Run `/mcp` in Claude Code and pick **dafexpert** to sign in. 1. Ask, for example: "Which dafexpert companies do I have access to?" --- # List my companies URL: https://developers.stg.dafexpert.com/en/article/mcp-list-companies > The companies the user can access, with currency, country, access level and balance. ## Description Lists the companies the signed-in user can access, with their currency, country, the user's access level and cash balance. It is usually the first tool called: it provides the `companyId` every other tool needs. See the [MCP server overview](/en/article/mcp-overview). ## Input parameters None. ## Output An array of companies. - `id` (uuid) : Company ID, to pass as `companyId` to the other tools. - `name` (string) : Legal name. - `legalForm` (string) : Legal form (`SCOOP-CA`, `SARL`…). - `country` (string) : Country (ISO code). - `currency` (string) : Company currency (`XOF`, `EUR`…). - `role` (string) : User's access level on the company: `owner`, `admin` or `member`. - `city` (string) : City. - `address` (string) : Address. - `registrationNumber` (string) : Trade register number (RCCM), empty if unknown. - `taxpayerNumber` (string) : Taxpayer number (NCC), empty if unknown. - `taxRegime` (string) : Tax regime. - `manager` (string) : Company manager. - `accountant` (string) : Company accountant, empty if none. - `balanceCents` (integer) : Balance of cash movements, in hundredths of the currency. Other display fields (`tint`, `commune`, `region`…) may be present. ```json [ { "id": "6f1d2c3a-8b4e-4f0a-9c1d-2e3f4a5b6c7d", "name": "Coopérative Agricole de Soubré", "legalForm": "SCOOP-CA", "country": "CI", "currency": "XOF", "role": "owner", "city": "Soubré", "address": "Quartier Commerce, BP 120", "registrationNumber": "CI-SOU-2019-B-0457", "taxpayerNumber": "1904578K", "taxRegime": "RSI", "manager": "Awa Koné", "accountant": "Cabinet Yao & Associés", "balanceCents": 1845000000 } ] ``` ## Example prompt > Which dafexpert companies do I have, and what is the balance of each? --- # List fiscal years URL: https://developers.stg.dafexpert.com/en/article/mcp-list-exercises > A company's fiscal years: dates, status and accounting framework. ## Description Lists a company's fiscal years, oldest first, with their dates, status (open or closed) and accounting framework. Provides the `exerciseId` the accounting tools need. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). ## Output An array of fiscal years. - `id` (uuid) : Fiscal year ID, to pass as `exerciseId`. - `label` (string) : Fiscal year label. - `referentialCode` (string) : Accounting framework: `SYSCOHADA`, `SYCEBNL` or `PCG`. - `startDate` (datetime) : Start of the fiscal year. - `endDate` (datetime) : End of the fiscal year. - `status` (string) : `open` or `closed`. - `reopenedAt` (datetime) : Reopening date after a closing, otherwise `null`. - `validatedEntries` (integer) : Number of validated entries. - `isFirst` (boolean) : The company's first fiscal year. - `hasOpening` (boolean) : The fiscal year already holds opening balances. - `chartVersionLabel` (string) : Chart of accounts version used by the fiscal year. ```json [ { "id": "3c9e7a10-5d2b-4b8f-a1e6-7f0c2d4e9b31", "label": "Exercice 2026", "referentialCode": "SYSCOHADA", "startDate": "2026-01-01T00:00:00.000Z", "endDate": "2026-12-31T00:00:00.000Z", "status": "open", "reopenedAt": null, "validatedEntries": 412, "isFirst": false, "hasOpening": true, "chartVersionLabel": "SYSCOHADA révisé" } ] ``` ## Example prompt > Which fiscal years are open for Coopérative Agricole de Soubré? --- # Trial balance URL: https://developers.stg.dafexpert.com/en/article/mcp-get-balance > The trial balance of a fiscal year, by account, with prior-year comparison. ## Description Returns the trial balance of a fiscal year: debit, credit and balance of each account, with the prior-year balance. It can be limited to a period with `from` and `to`. > **info** This tool returns amounts **in currency units**, not hundredths. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `exerciseId` (uuid, body, required) : Fiscal year ID, from [list_exercises](/en/article/mcp-list-exercises). - `from` (string, body) : Period start, `YYYY-MM-DD`. Defaults to the start of the fiscal year. - `to` (string, body) : Period end, `YYYY-MM-DD`. Defaults to the end of the fiscal year. ## Output - `exercise` (object) : Requested fiscal year: `id`, `startDate`, `endDate`, `status`. - `previous` (object) : Prior fiscal year (`id`, `endDate`, `status`), or `null`. - `rows` (array) : One row per account: `account`, `label`, `debit`, `credit`, `solde` (balance), `soldeN1` (prior-year balance), `lines` (number of entry lines), `lettrage` (matching status: `lettre`, `partiel`, `non-lettre` or `non-lettrable`), `known` (account exists in the company chart). - `totals` (object) : Totals: `debit`, `credit`, `solde`, `soldeN1`. - `result` (number) : Profit or loss for the period (income minus expenses). ```json { "exercise": { "id": "3c9e7a10-…", "startDate": "2026-01-01", "endDate": "2026-12-31", "status": "open" }, "previous": { "id": "a71b2c04-…", "endDate": "2025-12-31", "status": "closed" }, "rows": [ { "account": "401100", "label": "Fournisseurs", "debit": 8200000, "credit": 12500000, "solde": -4300000, "soldeN1": -3150000, "lines": 37, "lettrage": "partiel", "known": true }, { "account": "521100", "label": "Banque SGCI", "debit": 46000000, "credit": 27550000, "solde": 18450000, "soldeN1": 9800000, "lines": 112, "lettrage": "non-lettrable", "known": true } ], "totals": { "debit": 254300000, "credit": 254300000, "solde": 0, "soldeN1": 0 }, "result": 6250000 } ``` ## Example prompt > Show me the cooperative's trial balance at September 30, 2026, and compare the bank accounts with last year. --- # Trial balance anomalies URL: https://developers.stg.dafexpert.com/en/article/mcp-get-balance-anomalies > Review checks on the trial balance of a fiscal year, with a conclusion. ## Description Runs the review checks on the trial balance of a fiscal year (abnormal balances, uncleared suspense accounts…) and returns the anomalies found, with an overall conclusion. Each anomaly comes with a severity, a likely explanation and its impact. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `exerciseId` (uuid, body, required) : Fiscal year ID, from [list_exercises](/en/article/mcp-list-exercises). ## Output - `conclusion` (string) : One-sentence summary. - `anomalies` (array) : Anomalies found (see below). - `generatedAt` (datetime) : When the check ran. Each anomaly contains: - `id` (string) : Check identifier. - `severity` (string) : `high`, `medium` or `low`. - `account` (string) : Account or account prefix concerned, for example `471`. - `accountLabel` (string) : Account label. - `title` (string) : Anomaly title. - `hypothesis` (string) : Likely explanation. - `impact` (string) : Effect on the accounts. - `accounts` (string[]) : Trial balance accounts concerned. - `status` (string) : Review point status: `open`, `justified` or `resolved`. - `comment` (string) : Accountant's justification. ```json { "conclusion": "1 anomalie détectée sur la balance, dont 1 prioritaire (comptes d'attente et sous-comptes). Il est recommandé de traiter d'abord les anomalies prioritaires, de lettrer les comptes de tiers et de justifier chaque solde atypique avant la clôture.", "anomalies": [ { "id": "comptes-attente", "severity": "high", "account": "47", "accountLabel": "Comptes d'attente et sous-comptes", "title": "Soldes non apurés sur les comptes d'attente (2 350 000,00 net, 1 compte) : ces comptes doivent être soldés à la clôture.", "hypothesis": "Opérations bancaires ou diverses non affectées à leur nature réelle, lettrage manquant ou justification incomplète des mouvements.", "impact": "Ces montants faussent le résultat et le bilan (trésorerie, dettes et créances) et nuisent à la fiabilité des états financiers.", "accounts": ["471000"], "status": "open", "comment": "" } ], "generatedAt": "2026-10-12T09:14:02.000Z" } ``` Texts are returned in French. ## Example prompt > Are there any anomalies in the cooperative's 2026 trial balance before closing? --- # Income statement URL: https://developers.stg.dafexpert.com/en/article/mcp-get-income-statement > The income statement of a fiscal year, by line item of the company's accounting framework. ## Description Returns the income statement of a fiscal year, laid out by the line items of the company's accounting framework (SYSCOHADA, SYCEBNL or PCG): expenses, income, subtotals and net result. Each line item comes with its breakdown by account. It can be limited to a period with `from` and `to`. > **info** This tool returns amounts **in currency units**, not hundredths. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `exerciseId` (uuid, body, required) : Fiscal year ID, from [list_exercises](/en/article/mcp-list-exercises). - `from` (string, body) : Period start, `YYYY-MM-DD`. Defaults to the start of the fiscal year. - `to` (string, body) : Period end, `YYYY-MM-DD`. Defaults to the end of the fiscal year. ## Output - `referential` (string) : `SYSCOHADA`, `SYCEBNL` or `PCG`. - `system` (string) : Presentation system of the fiscal year: `normal`, `smt` (minimal cash-basis system) or `pd` (development projects). - `columns` (array) : Table columns (a single one here): `exerciseId`, `label`, `startDate`, `endDate`, `status`, `kind`. - `rows` (array) : Line items in presentation order: `id`, `label`, `kind` (`section`, `line`, `total` or `result`), `values` (one per column), `accounts` (breakdown by account: `account`, `label`, `values`). - `unmapped` (array) : Expense or income accounts in use but not mapped to any line item. ```json { "referential": "SYSCOHADA", "system": "normal", "columns": [ { "exerciseId": "3c9e7a10-…", "label": "Exercice 2026", "startDate": "2026-01-01", "endDate": "2026-12-31", "status": "open", "kind": "exercise" } ], "rows": [ { "id": "TA", "label": "Ventes de marchandises", "kind": "line", "values": [98500000], "accounts": [{ "account": "701100", "label": "Ventes de cacao", "values": [98500000] }] }, { "id": "RA", "label": "Achats de marchandises", "kind": "line", "values": [-81200000], "accounts": [{ "account": "601100", "label": "Achats de cacao", "values": [-81200000] }] }, { "id": "XI", "label": "Résultat net", "kind": "result", "values": [6250000], "accounts": [] } ], "unmapped": [], "analytic": null } ``` ## Example prompt > What is the cooperative's result for the first half of 2026, and what are its three largest expense items? --- # List customer invoices URL: https://developers.stg.dafexpert.com/en/article/mcp-list-invoices > A company's customer invoices, with an optional status filter. ## Description Lists a company's customer invoices, newest first: number, customer, status, dates, totals before tax, VAT and including tax, amounts settled. An optional filter keeps a single status. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `status` (string, body) : Keep only this status (see the table). | Status | Meaning | |---|---| | `draft` | Draft | | `created` | Finalized, not sent yet | | `sent` | Sent to the customer | | `late` | Overdue | | `paid` | Paid | | `cancelled` | Cancelled | ## Output An array of invoices. - `id` (uuid) : Invoice ID, to pass to [get_invoice](/en/article/mcp-get-invoice). - `number` (string) : Invoice number (empty for a draft). - `clientId` (uuid) : Customer ID. - `clientName` (string) : Customer name. - `status` (string) : Invoice status. - `invoiceKind` (string) : Document type: `standard`, `deposit`, `intermediate`, `balance` or `credit_note`. - `issueDate` (datetime) : Issue date. - `dueDate` (datetime) : Due date. - `paidAt` (datetime) : Date fully paid. - `currency` (string) : Invoice currency. - `subtotalCents` (integer) : Total before tax, in hundredths. - `taxCents` (integer) : VAT, in hundredths. - `totalCents` (integer) : Total including tax, in hundredths. - `collectedCents` (integer) : Amount settled, in hundredths. - `cashedCents` (integer) : Amount already received, in hundredths. ```json [ { "id": "b2f4c6d8-1a3e-4c5b-9d7f-0e2a4c6b8d1f", "number": "F-2026-0142", "clientId": "7e8f9a0b-…", "clientName": "SACO Abidjan", "status": "late", "invoiceKind": "standard", "issueDate": "2026-09-02T00:00:00.000Z", "dueDate": "2026-10-02T00:00:00.000Z", "paidAt": null, "currency": "XOF", "subtotalCents": 1250000000, "taxCents": 225000000, "totalCents": 1475000000, "collectedCents": 500000000, "cashedCents": 500000000 } ] ``` ## Example prompt > Which of the cooperative's invoices are overdue, and how much is still to be collected? --- # Invoice details URL: https://developers.stg.dafexpert.com/en/article/mcp-get-invoice > The details of a customer invoice: lines, totals, customer, payments and linked transactions. ## Description Returns the full details of a customer invoice: customer contact details, lines, totals, discount, settlements, matched bank transactions, credit notes and, in Côte d'Ivoire, the FNE certification status. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `invoiceId` (uuid, body, required) : Invoice ID, from [list_invoices](/en/article/mcp-list-invoices). ## Output - `id` (uuid) : Invoice ID. - `number` (string) : Invoice number. - `status` (string) : Status (`draft`, `created`, `sent`, `late`, `paid`, `cancelled`). - `issueDate` (datetime) : Issue date. - `dueDate` (datetime) : Due date. - `currency` (string) : Currency. - `clientName` (string) : Customer name, along with `clientEmail`, `clientAddress`, `clientCity`, `clientCountry`, `clientVatNumber`. - `lineItems` (array) : Lines: `description`, `quantity`, `unit`, `unitPriceCents`, `taxRateBp` (VAT rate in basis points, `1800` = 18%), `lineTotalCents`, `accountCode`. - `subtotalCents` (integer) : Total before tax after discount, in hundredths. - `taxCents` (integer) : VAT, in hundredths. - `totalCents` (integer) : Total including tax, in hundredths. - `settledCents` (integer) : Amount settled, in hundredths. - `cashedCents` (integer) : Amount received, in hundredths. - `creditedCents` (integer) : Share of the total cancelled by credit notes, in hundredths. - `linkedTransactions` (array) : Matched transactions: `id`, `name`, `amountCents`, `occurredAt`. - `creditNotes` (array) : Credit notes issued against this invoice: `id`, `number`, `issueDate`, `totalCents`. - `fiscalStatus` (string) : FNE certification status (`none`, `pending`, `certified`, `failed`), with `fiscalNumber` and `fiscalVerificationUrl` once certified. ```json { "id": "b2f4c6d8-1a3e-4c5b-9d7f-0e2a4c6b8d1f", "number": "F-2026-0142", "status": "late", "issueDate": "2026-09-02T00:00:00.000Z", "dueDate": "2026-10-02T00:00:00.000Z", "currency": "XOF", "clientName": "SACO Abidjan", "clientEmail": "compta@saco.ci", "clientCity": "Abidjan", "clientCountry": "CI", "lineItems": [ { "description": "Fèves de cacao grade 1", "quantity": 5000, "unit": "kg", "unitPriceCents": 250000, "taxRateBp": 1800, "lineTotalCents": 1250000000, "accountCode": "701100" } ], "subtotalCents": 1250000000, "taxCents": 225000000, "totalCents": 1475000000, "settledCents": 500000000, "cashedCents": 500000000, "creditedCents": 0, "linkedTransactions": [ { "id": "c4d5e6f7-…", "name": "VIR SACO ABIDJAN", "subtitle": null, "amountCents": 500000000, "occurredAt": "2026-09-20T00:00:00.000Z" } ], "creditNotes": [], "fiscalStatus": "certified", "fiscalNumber": "FNE-2026-000123", "fiscalVerificationUrl": "https://…" } ``` ## Example prompt > Break down invoice F-2026-0142: what have we already received from SACO and what is still due? --- # Cash position URL: https://developers.stg.dafexpert.com/en/article/mcp-get-treasury-overview > A company's cash overview: balance, flows, upcoming due dates and latest transactions. ## Description Returns a company's cash overview: available balance (real bank balances when a bank is connected), inflows and outflows over the chosen window, customer receivables and supplier payables, upcoming due dates and latest transactions. ## Input parameters - `companyId` (uuid, body, required) : Company ID, from [list_companies](/en/article/mcp-list-companies). - `range` (string, body) : Analysis window: `7d`, `30d` (default) or `1y`. ## Output All amounts are in hundredths of the company's currency. - `treasuryCents` (integer) : Available cash balance. - `cash` (object) : Balance details: `source` (`bank` when bank accounts are synced, otherwise `transactions`), `accounts` (`name`, `type`, `bankName`, `balanceCents`, `lastSyncAt`), `asOf`. - `inflowCents` (integer) : Inflows over the window. - `outflowCents` (integer) : Outflows over the window (positive value). - `currentNetCents` (integer) : Net flow over the window. - `prevNetCents` (integer) : Net flow over the previous window of the same length. - `receivableCents` (integer) : Amount still to collect on issued customer invoices, with `receivableCount`. - `payableCents` (integer) : Amount still owed to suppliers, with `payableCount`. - `upcoming` (array) : Next five due items: `name`, `subtitle`, `amountCents`, `dueDate`, `kind` (`invoice` to collect or `transaction` to pay). - `latest` (array) : Last three transactions: `name`, `amountCents`, `kind`, `status`, `missingReceipt`, `occurredAt`. - `dailyBalances` (integer[]) : Day-by-day balance over the window, oldest first. ```json { "treasuryCents": 1845000000, "cash": { "cents": 1845000000, "source": "bank", "accounts": [ { "id": "…", "type": "bank", "bankName": "SGCI", "name": "Compte courant", "currency": "XOF", "balanceCents": 1845000000, "lastSyncAt": "2026-10-12T06:00:00.000Z" } ], "asOf": "2026-10-12T06:00:00.000Z" }, "inflowCents": 920000000, "outflowCents": 640000000, "currentNetCents": 280000000, "prevNetCents": -115000000, "receivableCents": 975000000, "receivableCount": 3, "payableCents": 412000000, "payableCount": 5, "upcoming": [ { "id": "…", "name": "SACO Abidjan", "subtitle": "Facture F-2026-0142 à encaisser", "amountCents": 975000000, "dueDate": "2026-10-02T00:00:00.000Z", "kind": "invoice" } ], "latest": [ { "id": "…", "name": "Achat intrants", "subtitle": null, "amountCents": -85000000, "kind": "out", "status": "done", "missingReceipt": true, "occurredAt": "2026-10-11T00:00:00.000Z" } ], "dailyBalances": [1565000000, 1570000000, 1845000000] } ``` ## Example prompt > How has the cooperative's cash moved over the last 30 days, and what is coming up next? --- # FNE tax documents via the API URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-fne-documents > Two synchronous routes issue FNE-certified purchase slips and invoices, with tax number, QR code and PDF. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. **October 12, 2026** Two synchronous routes issue FNE-certified purchase slips and invoices, with tax number, QR code and PDF. ## What changed - New route `POST /v1/partners/companies/{external_id}/bordereaux`: FNE-certified agricultural purchase slip, issued by the buying company. - New route `POST /v1/partners/companies/{external_id}/invoices`: FNE-certified sales invoice. The FNE template (B2B, B2C, B2F, B2G) is derived from the customer. - The response is immediate: document number, tax number, QR code, PDF link and accounting status. - Both routes are idempotent on your identifier: a replay returns the same document and tax number, with no double certification. - A failed certification (FNE not connected, stickers used up, missing phone number) saves the document as not certified and flags it as a discrepancy. ## Why it matters Tax documents no longer go through events: you get the tax number and the PDF in the response and can hand them to the producer or customer right away. ## Documentation - [FNE tax documents](/en/article/fne-documents) - [Create a purchase slip](/en/article/post-bordereau) - [Create an invoice](/en/article/post-invoice) --- # Events version 2 URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-events-v2 > Events sent with `"version": 2` get a type catalog, versions, per-company effects and likely-duplicate detection. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. **October 12, 2026** Events sent with `"version": 2` get a type catalog, versions, per-company effects and likely-duplicate detection. ## What changed - Events sent with `"version": 2` follow a type catalog (producer payment, advance, expense, payroll…) and reference tax documents by their identifier. - Versions: sending the same `id` with different content replaces the event as long as it is not posted yet (`version_appliquee: true`). Otherwise, the version is kept without effect and flagged. - Each event reports its effects per company (document created or matched, accounting entry). - An event that references a document not received yet goes to `en_attente_piece` and resumes when the document arrives. - Likely duplicates (the same operation under two `id`s) are detected and held before posting. - Version 1 is still accepted. ## Why it matters You can fix an event by sending it again, and see exactly what it produced in each company. ## Documentation - [Migrate to v2 events](/en/article/migrate-events-v2) - [Event versions](/en/article/event-versions) - [Send events](/en/article/post-events) --- # Complete company profile URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-company-fields > `PUT /v1/partners/companies/{external_id}` accepts new company types and tax, location and farmer fields. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. **October 12, 2026** `PUT /v1/partners/companies/{external_id}` accepts new company types and tax, location and farmer fields. ## What changed - New types `union` and `acheteur` (buyer), alongside `cooperative` and `exploitant` (farmer). - New fields: `parent_id` (parent organization), `localisation`, `etiquettes` (tags), tax and legal details, `exploitant` block, `systeme_comptable` (`smt` by default for a farmer). - A field sent for a type that does not support it is rejected (`422`). - The response lists in `a_completer` what is missing before the first tax document. ## Why it matters Companies arrive ready to issue tax documents, and you know right away what is left to fill in. ## Documentation - [Create or update a company](/en/article/put-company) --- # Bulk company creation URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-12-bulk-companies > `POST /v1/partners/companies` loads up to 500 companies per call, processed in the background with batch tracking. > **warning** **Coming soon.** This feature is ready but not yet open on staging. The contract described here is the one that will ship. **October 12, 2026** `POST /v1/partners/companies` loads up to 500 companies per call, processed in the background with batch tracking. ## What changed - New route `POST /v1/partners/companies`: up to 500 companies per call, `202` response with a `lot` (batch) ID. - Each company is validated on its own: an invalid company does not block the others. - Track with `GET /v1/partners/companies/lots/{lot}`: status per company (`recu`, `en_cours`, `traite`, `rejete`), counters and rejection reason. - Technical errors are retried automatically. ## Why it matters Migrating an existing base of cooperatives and farmers takes only a few calls. Day to day, `PUT` remains the normal path. ## Documentation - [Bulk create companies](/en/article/post-companies) --- # User management URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-09-users > `PUT /v1/partners/users/{id}` creates or links a dafexpert account and gives it access to companies. **October 9, 2026** `PUT /v1/partners/users/{id}` creates or links a dafexpert account and gives it access to companies. ## What changed - New idempotent route `PUT /v1/partners/users/{id_utilisateur}`: creates the sign-in account (or links an existing one by email) and grants access to the companies listed in `acces`, with a role. - The partner can set the password for accounts it creates; it is never stored by dafexpert. - The `acces` list is authoritative: sending it again removes access that is no longer listed. - New route `DELETE /v1/partners/users/{id_utilisateur}`: removes all access granted by the partner. - The `utilisateurs` field of `PUT /v1/partners/companies/{external_id}` is removed (`422` if sent). ## Why it matters Your users sign in to dafexpert with no invitation or onboarding, and only see their own companies. ## Documentation - [Create or update a user](/en/article/put-user) --- # Single sign-on (SSO) URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-sso > Your users sign in once to access both dafexpert and your application. **October 8, 2026** Your users sign in once to access both dafexpert and your application. ## What changed - Each partner gets an OpenID Connect client (Authorization Code flow, PKCE supported) on the dafexpert identity provider. - The session is shared: a user signed in to one is signed in to the other. Single logout is available with a front-channel logout URL. - After sign-in, the user picks which application to open (dafexpert or yours) and can switch from the header. ## Why it matters One set of credentials for your users, and a smooth path between your field tool and their accounting. ## Documentation - [Authentication](/en/article/authentication) --- # Automatic event posting URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-event-accounting > Received events are processed in the background and turned into accounting entries using configurable mappings. **October 8, 2026** Received events are processed in the background and turned into accounting entries using configurable mappings. ## What changed - Each received event is processed in the background, with automatic retries on technical errors. - It produces an accounting entry from a mapping (journal, accounts, label, amounts) configured for the partner, the workspace or the company. - The entry goes through the same checks as manual entry and stays as a draft until the accountant validates it. - The status moves to `comptabilise` (with the entry) or `rejete` (with the reason). A rejected event can be retried from dafexpert once fixed. ## Why it matters Your field operations reach the books with no re-keying, and you can follow what happened to them through the API. ## Documentation - [List events](/en/article/list-events) --- # Sending and tracking events URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-events > `POST /v1/partners/events` receives business events, one at a time or in batches of 500, and `GET /v1/partners/events` syncs them. **October 8, 2026** `POST /v1/partners/events` receives business events, one at a time or in batches of 500, and `GET /v1/partners/events` syncs them. ## What changed - New route `POST /v1/partners/events`: one event or a batch of up to 500, `202` response with one result per event. - The event `id` is the deduplication key: resending after an outage is safe. - An event whose company does not exist yet is kept (`en_attente_entreprise`) and released when the company is created. - New routes `GET /v1/partners/events/{id}` and `GET /v1/partners/events?apres=` to follow status changes without missing any. ## Why it matters You send operations as they happen and reliably get their posting status back. ## Documentation - [Send events](/en/article/post-events) - [List events](/en/article/list-events) --- # Company creation URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-companies > `PUT /v1/partners/companies/{external_id}` creates or updates a cooperative or farmer, with its first fiscal year. **October 8, 2026** `PUT /v1/partners/companies/{external_id}` creates or updates a cooperative or farmer, with its first fiscal year. ## What changed - New idempotent route `PUT /v1/partners/companies/{external_id}`: the first call creates the company, later calls update it. - On creation, the first fiscal year is opened with its periods, journals and the country's chart of accounts (SYSCOHADA in the OHADA zone). - Country, currency, accounting framework and fiscal year are locked after creation. - Validation errors return a `422` with per-field details. ## Why it matters Every company you manage has its books ready from the first call, with no manual setup. ## Documentation - [Create or update a company](/en/article/put-company) --- # Partner API launch URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-08-partner-api > OAuth 2.0 client credentials per workspace, and `GET /v1/partners/me` to check your credentials. **October 8, 2026** OAuth 2.0 client credentials per workspace, and `GET /v1/partners/me` to check your credentials. ## What changed - Partners authenticate with OAuth 2.0 *client credentials*, using one `client_id` and `client_secret` per workspace, obtained by the workspace admin under *Settings › Connected apps*. - The token lasts 5 minutes and is requested again with the same credentials (no refresh token). - The admin can generate a new secret or disconnect the partner at any time. - New route `GET /v1/partners/me`: returns the partner, the workspace and the API version. ## Why it matters A generic API, the same for every partner, isolated per workspace: a set of credentials can only act within its own workspace. ## Documentation - [Authentication](/en/article/authentication) - [Check your credentials](/en/article/get-partner-me) --- # MCP server launch URL: https://developers.stg.dafexpert.com/en/article/changelog-2026-10-06-mcp-server > dafexpert customers can connect Claude or another AI assistant to their companies, read-only. **October 6, 2026** dafexpert customers can connect Claude or another AI assistant to their companies, read-only. ## What changed - New MCP server at `https://api.stg.dafexpert.com/mcp` (staging), over Streamable HTTP. - Sign-in with the user's dafexpert account: OAuth 2.0 + PKCE, Client ID `dafexpert-mcp`, no secret. - Eight read-only tools: companies, fiscal years, trial balance, trial balance anomalies, income statement, customer invoices, invoice details, cash position. - The assistant only sees the companies the user is a member of. ## Why it matters Your users query their accounting in plain language from their AI assistant, with no export. ## Documentation - [MCP server](/en/article/mcp-overview) - [Connect an AI assistant](/en/article/connect-ai-assistant)