exsited - Dart

Dart client for the Exsited v4 REST API. Handles OAuth2 with a cross-process token cache, retry with backoff, corporate TLS and proxies, content-type decoding, a typed error hierarchy, secret redaction and terminal diagnostics. The core (config, errors, response, DTO base, transport, retry, URL and query building, multipart, logging, console, redaction, registry and client) is shared by every family; what is specific to the Exsited v4 API is listed under Architecture.

The wire is untouched. No key is renamed or case-folded in either direction: the names you send are the names on the wire, and the names the API answers with are the names on response.map. A Map payload, a form and a query are encoded, as JSON, as application/x-www-form-urlencoded and as a query string. A raw JSON String is not re-encoded: it goes out byte for byte, which is what makes a body pasted from an API client arrive as the document it was. Optional typed views sit over that: send a map, a raw JSON string or a DTO, and read the same response as .map, .json or .dto. See Bodies, both ways.

Async is native here. I/O returns a Future; pagination is a Stream consumed with await for.

import 'package:exsited/exsited.dart';

Future<void> main() async {
  final client = ExsitedClient(SdkConfig(
    AuthConfig(
      clientName: 'default', // required: which instance this is
      baseUrl: 'https://your-instance.exsited.com',
      clientId: '...',
      clientSecret: '...',
      redirectUri: 'https://your-app.example/callback',
    ),
    raiseOnError: true,
  ));

  try {
    final response = await client.accounts.getAccount('W13ZGY');
    print(response.map);       // the body as it arrived
    print(response.dto?.name); // the typed view
  } finally {
    client.close(); // release the HTTP client when you are done
  }
}

close() is a one-way latch: it is idempotent, and any request issued afterwards is refused with an SdkUsageError naming the client, before any I/O. A closed client is not reopened; build a new one.

A failure comes back as a response rather than throwing, which is right for application code and wrong for a script that must stop. raiseOnError: true above flips it. "Failure" is a non-2xx or a 2xx whose body names itself "object": "error"; see Responses.

Security note. clientSecret is a server credential. Never embed it in a shipped mobile or web app, where it can be extracted. The SDK's safe home is a server, a backend-for-frontend, or a CLI, even though the code runs everywhere.

Package exsited on pub.dev
Import package:exsited/exsited.dart, plus one library per family
Dart 3.5+ (developed on 3.12.2)
Dependencies http ^1.6.0, http_parser ^4.1.0
Endpoints 864 in 91 families, documented at exsited.mintlify.app
Author Ashiq Rahman ashiq@webalive.com.au
License MIT, see LICENSE

Contents

Install

dart pub add exsited

Prerequisites. Dart 3.5 or newer; pubspec.yaml declares environment: sdk: ^3.5.0. Get it from dart.dev/get-dart; the Flutter SDK bundles Dart, so a Flutter install works too, and flutter pub add exsited is the same command there.

The command adds the dependency to your pubspec.yaml and fetches it:

dependencies:
  exsited: ^4.0.0

Then import the core library, plus a family library wherever you call one of the other 89 families:

import 'package:exsited/exsited.dart';
import 'package:exsited/payments.dart' as payments;

Credentials

The SDK never reads a credential from the environment: not from .env, not at any point. Build an AuthConfig from wherever your credentials live and hand it in. (One non-secret presentation setting is read from the environment, EXSITED_RESPONSE_FORMAT, and it is the only one: it changes how a body is presented to you, never what is sent. See Bodies, both ways.)

import 'dart:io';
import 'package:exsited/exsited.dart';

AuthConfig loadAuth() => AuthConfig(
      clientName: 'default',
      baseUrl: Platform.environment['EXSITED_BASE_URL']!,
      clientId: Platform.environment['EXSITED_CLIENT_ID']!,
      clientSecret: Platform.environment['EXSITED_CLIENT_SECRET']!,
      redirectUri: Platform.environment['EXSITED_REDIRECT_URI']!,
    );

Validation runs at construction and reports every problem at once:

SdkConfigError: instance default: clientId: must not be blank; clientSecret: must not be blank
Field Required Default Notes
clientName yes Label in logs and output; selects the instance by name. Use 'default' for a single-instance setup
baseUrl yes http:// or https://; trailing / stripped; a sub-path is allowed; a query, fragment, embedded user:pass@ or a port outside 1 to 65535 is refused
clientId yes
clientSecret yes Redacted in toString()
redirectUri yes Sent in the token request body
grantType 'client_credentials'
tokenCachePath null null with inMemoryToken: false is the platform's per-user cache file; a blank string is refused
inMemoryToken false Keep the token in memory for the life of the process

The credential is clientId, clientSecret, redirectUri and grantType, and nothing else: those four are what the Exsited token request carries.

Configuration

AuthConfig is who you are; SdkConfig is how the SDK behaves, passed as named arguments. Both are immutable, validated at construction with every problem reported in one exception, and redact their secrets in toString(). Every setting has a default, and each default is also a SdkConfig.default* constant:

final config = SdkConfig(auth, printApiInfo: true, maxRetries: 3, logLevel: 'DEBUG');
final quieter = config.withOverrides(printApiInfo: false);
Setting Default Notes
apiVersion ApiVersion.v4 Fills {api_version}. An enum that also carries the token path, /api/v4/oauth2/token
timeoutSeconds 30.0 A hard ceiling on one attempt; a retry starts a fresh one. See Timeouts
connectTimeoutSeconds 10.0 Connection setup alone, inside timeoutSeconds (native only)
maxRetries 1 Total attempts including the first, so 1 disables retry
retryableHttpCodes {429, 500, 502, 503, 504} Inert while maxRetries is 1
unsafeRetryableHttpCodes {} The second gate every method outside GET/HEAD/OPTIONS/TRACE must also pass; empty, so no status alone replays a write
retryBackoffSeconds 0.5 Base for exponential backoff with full jitter
retryBackoffMaxSeconds 20.0 Ceiling for one sleep
verifySsl true Native only; the browser decides on web
caBundle null PEM path (native). Replaces the trust store. Contradicts verifySsl: false; a blank value reads as unset
proxyUrl null Applied to http and https (native), stored normalised so a password redacts; an https:// proxy is refused
poolMaxsize 10 Connections the client aims to keep for reuse (native); a hint, not a ceiling on concurrency
maxResponseBytes 64 << 20 Ceiling on how much of one response body is buffered. Over it the call fails with an SdkError naming the cap, and is not retried
maxRequestBytes 64 << 20 Ceiling on the final wire body of one request, framing included
maxRequestSourceBytes 64 << 20 Ceiling on the source material one request is built from
maxMultipartOverheadBytes 1 << 20 Ceiling on a multipart body's metadata and framing alone
maxTokenCacheBytes 1 << 20 Ceiling on the token cache file
maxCaBundleBytes 4 << 20 Ceiling on the CA bundle file
defaultHeaders {} Sent on every request, stored as an unmodifiable copy
raiseOnError false true throws SdkError instead of answering the failed response
userAgent exsited-dart-sdk/<version>
logLevel 'WARNING' DEBUG/INFO/WARNING/ERROR/CRITICAL, or null for none
logBodies false Payload and response in a DEBUG record, redacted
logFile null Also append log records to this path (native); shared between clients naming the same file, released on close(); blank reads as unset
printApiInfo false Client, api version, method, endpoint, params, status, attempt, elapsed, request id
printRequestPayload false Redacted
printResponseBody false Bytes summarised, never dumped
printHeaders false Redacted
printCurl false Copy-pasteable, token redacted
printOnlyOnError false Suppress the block when the call succeeded
printFormat 'pretty' Or 'compact', one line per call
printColor false ANSI colour, forced off when stdout is not a terminal
printMaxChars 0 0 is unlimited
responseFormat 'map' Which form response.data takes: 'map', 'json' or 'dto'. Falls back to EXSITED_RESPONSE_FORMAT; an unrecognised value is refused naming all three
httpClient null An http.Client of your own; never closed by the SDK

SdkConfig.fromCredentials(list, instance: ..., <any setting>: ...) builds one from a list of credential blocks, each an SdkConfig, an AuthConfig or a mapping of snake_case field names, chosen by index or clientName.

Two hooks live on the ExsitedClient constructor rather than the config, so the config stays a printable value object: onLog (every log record, one 'LEVEL message' line, gated by logLevel) and onWarning (each InsecureTransportWarning). Both are under Diagnostics.

Bodies, both ways

Three ways to send one, and the same endpoint function takes all three:

// a map
await cancelAccount(client, id, {'account': {'effective_date': '2026-08-31'}});

// a JSON string, sent byte for byte, so a pasted body keeps its spacing and key
// order
await cancelAccount(client, id, '{"account": {"effective_date": "2026-08-31"}}');

// a DTO
await cancelAccount(client, id, const AccountCancelBody(effectiveDate: '2026-08-31'));

A payload is a non-null Object, so a missing one does not compile; {} is a legal body. An invalid JSON string, a nested map in the query string and a blank path parameter are all refused locally, before a socket opens.

Three ways to read one, and all three are always populated:

response.map    // the decoded body: Map, List, String, List<int>, or null on 204
response.json   // the text that arrived, spacing and key order intact
response.dto    // the typed view, or null when the endpoint names no type
response.data   // whichever of the three responseFormat selects

map is the only one that can tell a key the API omitted from one it sent as null, which is why it is never traded away for the typed view.

responseFormat resolves through four tiers, highest first: the call, the client's config, the EXSITED_RESPONSE_FORMAT environment variable, then 'map'. The environment sits below code on purpose: a project-wide variable that could override a format written in source would silently change behaviour the source spells out. A blank variable reads as unset. On the web there is no environment to read, so that tier is empty.

await getAccount(client, 'W13ZGY', responseFormat: 'json');
await client.accounts.getAccount('W13ZGY', responseFormat: SdkConfig.responseFormatDto);

A DTO converts nothing. A field declared String? reads null when a number arrives; only an int widening to a double is allowed. Each view has the same shape: a static X? fromMap(Object? body) that answers null for a body that is not a map, an envelopeKey where the API wraps the record, and one fields map from which the base builds toMap() and a redacting toString(). A nested view is written without its own envelope, as the API sends it. Every field type was measured against live answers from every endpoint: a nested object is a view of its own, a field that was always null is left untyped, and so is one whose records disagreed on its type.

Multiple instances

Declare each instance as its own AuthConfig (or SdkConfig) and select between them by name or index. Built the ordinary way, each is isolated, with its own HTTP client, token provider and token store. An instance you never ask for is never built, so it never authenticates.

final instances = <Object>[staging, SdkConfig(production, raiseOnError: true)];

// Hold several open at once; the first is the default.
final registry = ClientRegistry(instances);
registry.getClient();              // the first, staging
registry.getClient(1);             // by index (negatives count from the end)
registry.getClient('production');  // by clientName
registry.listClientNames();        // ['staging', 'production']
await registry.closeAllClients();  // closes every client it built

// Or register once and build anywhere, per isolate.
registerInstances(instances);
buildClient();                               // the default
buildClient(instance: 'production');         // by clientName
buildClient(instance: 'staging', printCurl: true);
listInstanceNames();                         // ['staging', 'production']

Two entries cannot share a clientName; that is refused when the registry is built, with the colliding positions reported. A client handed in through addClient is left open by closeAllClients(). The earlier Exsited names (ClientRegistry.fromAuth, client, indexOf, clientNames, length, close) still work.

Authentication

Automatic, and Exsited's own. OAuth2 client_credentials against POST /api/v4/oauth2/token on first use, cached, and reused while valid. A token is renewed 60 seconds before it expires (expirySkew), a server-stated lifetime is believed for at most 24 hours (maxTokenTtl), and a renewal tries the refresh_token grant first, falling back to a full client_credentials request.

A 401 that means a stale token is handled compare-and-clear: the dead token is invalidated only if the store still holds that same token, then the client refreshes and replays the call once. If another worker refreshed first, its token is used rather than minting a third. A 401 whose body names no token failure (unauthorized, invalid_access_token, invalid_token, expired_token, invalid_request, matched as whole words) is returned as-is, because replaying a rejected write submits it twice; a non-JSON 401 is replayed only on a safe method.

Token cache. On native platforms the default is a file keyed by a SHA-256 fingerprint of the credentials, safe to share across processes:

Platform Path
Windows %LOCALAPPDATA%\exsited\exsited_dart_token.json
macOS ~/Library/Caches/exsited/exsited_dart_token.json
Linux $XDG_CACHE_HOME/exsited/exsited_dart_token.json, else ~/.cache/exsited/exsited_dart_token.json

When the variable a row names is unset, the system temp directory takes its place. On web there is no file system, so the token is kept in memory, and a tokenCachePath spelled out explicitly is refused rather than ignored.

AuthConfig(..., tokenCachePath: '/path/of/your/choosing/token.json'); // an explicit file
AuthConfig(..., inMemoryToken: true);                                 // memory only

// force a fresh authentication: delete the file, or inject a memory store
ExsitedClient(SdkConfig(auth), tokenStore: MemoryTokenStore());

Several processes sharing one cache file are serialised by a lock on a sidecar .lock file, one per cache file, because a write rebuilds the whole file. A lock that cannot be taken fails the call with SdkTokenLockError rather than running the work with no exclusion. A cache file that is present but unusable is not treated as empty: that is SdkTokenCacheError, since answering "empty" and rewriting it would discard every other namespace's entry, refresh tokens included. The read is bounded at 1 MiB.

Requests

Two ways to call, and they are the same call. Every endpoint is a top-level function taking the client first, and every one is also a method on the family bound to that client. The bound method is a delegate to the function:

await getAccount(client, 'W13ZGY');           // the function
await client.accounts.getAccount('W13ZGY');   // the same call, bound

client.<family> works for all 91 families and needs only package:exsited/exsited.dart; each getter builds its family once and caches it. To choose a family by a name that is not a literal in your source, look it up:

client.listResourceNames();                    // ['abaFiles', 'accountEmails', ...]
final accounts = client.resourceByName('accounts') as AccountsResource;

An unknown name raises SdkAttributeError naming what is available. registerResourceModule adds a family of your own.

The resource libraries. The main library carries the core plus the accounts and items families. Each other family is its own library, imported with a prefix, because families declare some of the same names (Reference, User, CustomForm, Note, Pagination) and export can neither rename nor prefix:

import 'package:exsited/payments.dart' as payments;

await payments.getPayments(client);

The bound style needs none of that; the prefixed import is only needed to name a view or a path constant.

Per call an endpoint function takes raiseOnError and responseFormat, and nothing else. Everything else a request is built from belongs to the client, reached through client.sendGetRequest, sendPostRequest, sendPutRequest, sendPatchRequest, sendDeleteRequest and sendApiRequest, which take flat named parameters: pathParams, queryParams, jsonBody, formData, files, headers, authenticated, raiseOnError, responseFormat and dtoDecoder. The earlier options: CallOptions(...) still compiles, and a flat argument wins over it.

A body is a map, a JSON string or a DTO, whichever you have (see Bodies, both ways). files sends multipart: each entry is bytes, or a list of the bytes, a filename and a content type.

X-Idempotency-Key on every POST. v4 answers a POST without one 400 x.idempotency.key.header.required, so the SDK generates a fresh version 4 uuid per call, JSON, form and multipart alike, and a retry or the 401 replay sends the same key again. A caller's own key, in any spelling, wins; PUT, PATCH and DELETE carry none.

Query helpers. v4 filters and sorts are spelled by helpers, so the wire names are never typed by hand:

await getAccounts(client, queryParams: {
  filterParam('status'): 'ACTIVE',
  filterParam('created_at', 'gte'): '2026-01-01',
  'sort': sortParam('created_at', SortDirection.desc),   // sort=-created_at
});

filterParam refuses an operator the API does not accept, and filterValues renders the value list for the in and nin operators.

Pagination

await for (final account in iterRecords(client, AccountsUrls.accounts,
    queryParams: {filterParam('status'): 'ACTIVE'})) {
  print((account! as Map)['id']);
}

iterPages yields each page's response and iterRecords each record. queryParams is re-sent with every page: a cursor is a position within one filtered listing, so the filters travel with it.

The account and item families page by cursor (pagination.next_cursor); most other v4 listings page by offset. Both walkers detect which one each page uses, so nothing is configured. Some listings key their records by resource name rather than data, so pass recordKey: when they do; each family's <Family>Urls.dataKey names the key its listing views read. At the end of an offset listing next_page is the literal string "NULL", which the walkers read as the end.

Responses

ApiResponse carries the request's outcome and never throws on its own:

Member What it is
statusCode, headers as received
map, json, dto, data the body in each form (see Bodies, both ways)
succeeded a 2xx whose body does not name itself "object": "error"
errorType an ErrorType: none, redirect, validation, authentication, authorization, notFound, methodNotAllowed, conflict, rateLimit, server, network, timeout, unknown
getErrorMessages() the problems an error body reports, most specific first: errors[], then message, then code
errorCode, requestId, docUrl, fieldErrors the v4 error envelope, Exsited's own
requestInfo a RequestInfo: client name, method, url, endpointTemplate, query params, attempt count and elapsed seconds
raiseForStatus() the response, or an SdkError if it did not succeed
if (!response.succeeded) {
  print(response.statusCode);          // 400
  print(response.errorCode);           // "validation_error"
  print(response.requestId);           // "req_..."
  print(response.fieldErrors);         // [FieldError(field: "effective_date", ...)]
  print(response.getErrorMessages());  // ["effective_date: ..."]
}

A 2xx can deny its own success. Every v4 body names what it is under object, and an error names itself error, so a 200 carrying that is not a success.

Errors

raiseOnError: true, on the config or per call, throws instead:

SdkError
  SdkConfigError       the configuration is wrong; costs no request
  SdkUsageError        the SDK refused before sending; costs no request
    SdkAttributeError  an unknown resource or attribute name
  SdkAuthError         the token grant failed
  SdkTokenCacheError   a token cache that is there and unusable (Exsited)
  SdkTokenLockError    a token cache that could not be locked (Exsited)

A raised error's message is HTTP <status> alone: the server's words are on getErrorMessages(), and explain(includeSensitive: true) renders the whole exchange, while explain() renders only its shape. statusCode, errorType, requestInfo, rawResponse, cause and getHint() are on every one, and a raised response error also carries the v4 envelope (errorCode, requestId, docUrl, fieldErrors). A network or timeout failure withholds the platform's own text and keeps it on cause. The earlier ...Exception names (SdkException and the rest) are type aliases of these.

Diagnostics

The print* flags render each exchange to the console, secrets redacted:

SdkConfig(auth,
  printApiInfo: true,          // client, method, endpoint, params, status, timing
  printRequestPayload: true,   // the body sent
  printResponseBody: true,     // the body received (bytes summarised, never dumped)
  printHeaders: true,          // request and response headers
  printCurl: true,             // a copy-pasteable POSIX-shell curl, token redacted
  printOnlyOnError: true,      // only when a call fails
  printFormat: 'pretty');      // or 'compact' for one line per call

No box and no | gutter, so a JSON body lifts out of a terminal as it is and the curl block pastes and runs:

Exsited  GET  200  0.231s
client      : default
api version : v4
method      : GET
endpoint    : https://your-instance.exsited.com/api/v4/accounts
params      : {"limit":1}
status      : 200
attempt     : 1 / 1
elapsed     : 0.231s
-- response --------------------------------------------------
{ ... }

compact collapses the whole exchange onto one exsited | ... line. printOnlyOnError is what makes these usable across a full run: leave everything on, see nothing until something breaks.

Glyphs are probed, not assumed: resolveBoxGlyphs() tests whether the console's encoding can carry each Unicode glyph and falls back to ASCII for the whole set when it cannot. What the terminal obeys is a separate guard: control characters in any value that came from outside the SDK (the method, the url, the query, a body, an error message, a request id) are escaped before the block is assembled, so a response body cannot move the cursor or erase the rows already printed.

The client takes an optional onLog hook, structured single-line records each opening with its level, filtered by logLevel, and an onWarning hook that receives each InsecureTransportWarning (an http:// base URL to a host that is not loopback, once per origin) instead of stderr:

final client = ExsitedClient(config,
    onLog: (line) => myLogger.info(line), onWarning: (w) => report(w.message));

For a file, name a logFile: records are appended, filtered by the same logLevel, shared between clients naming the same path, and the handle is released when the last of them calls close() (native only; on web a logFile is refused at construction).

Console and log lines share one redactor: a key whose name carries one of the eighteen markers (secret, password, token, bearer, jwt, authorization, apikey, cookie, session, signature, privatekey and the rest in sensitiveKeyMarkers) prints as ***, matched as substrings ignoring case and separators, and URLs and query strings inside free text have their secret parameters and userinfo stripped.

Timeouts

timeoutSeconds is a hard ceiling on one attempt, not an idle read timeout and not a ceiling on the call:

Budget Setting Bounds
connect connectTimeoutSeconds DNS, TCP connect and TLS handshake (native; inert on web)
read timeoutSeconds the response headers, then the body, both drawing on the same budget
overall timeoutSeconds one attempt, start to finish

A request that runs out of budget is aborted, not abandoned, so it does not keep running behind the error. A retry starts a fresh ceiling, so maxRetries: 3 is bounded by three ceilings plus the backoff between them.

Retries

Off by default: maxRetries counts the first attempt.

SdkConfig(auth, maxRetries: 3);

Exponential backoff with full jitter, capped by retryBackoffMaxSeconds. A Retry-After beats the jitter but is clamped by the same ceiling; delay-seconds and all three HTTP-date forms are read, and a date already past reads as zero. 400 and 401 are not in the retryable set: repeating a malformed request cannot succeed, and a 401 is refresh-and-replayed one layer up.

A status that came back:

Method Retried on
GET HEAD OPTIONS TRACE (safe) retryableHttpCodes
anything else, an unknown verb included retryableHttpCodes and unsafeRetryableHttpCodes, the second empty by default

The safe row is an allow-list, so a method it does not name is treated as a write. unsafeRetryableHttpCodes: {429, 503} is the explicit opt-in for a deployment you know.

A failure with no status:

Failure Retried on
zero request bytes transmitted, measured rather than inferred any method
the request may already have been delivered GET HEAD OPTIONS TRACE PUT DELETE only

A POST that timed out awaiting its response is not retried; a POST that never reached a socket is. On web nothing is classified as never-sent, so a POST is never replayed after a transport failure there. The X-Idempotency-Key a retried POST carries is the first attempt's.

Transport

SdkConfig(auth,
  caBundle: '/path/to/corporate-ca.pem',  // native
  proxyUrl: 'http://proxy.internal:8080', // native
  poolMaxsize: 10);                        // native

One http.Client per client, so two with different TLS or proxy settings never contend; an httpClient you pass in is used as it is and never closed. caBundle replaces the trust store rather than extending it. Setting both caBundle and verifySsl: false is refused at construction.

Redirects are not followed. A 3xx comes back like any other non-2xx, with its Location header intact.

Absolute URLs stay on the configured origin. A path given as a full URL to another origin is refused, so the bearer token never leaves for a host it was not minted for.

Endpoints

91 families and 864 endpoints; the largest are accounts (65), items (41) and invoices (30). Every endpoint, with its request and response bodies, is documented at exsited.mintlify.app. Each endpoint has one function, in the family that owns the first segment of its path.

Library Families
exsited.dart accounts, items
invoices.dart invoices
<family>.dart aba_files, account_emails, account_groups, accounting_codes, action_scripts, additional_codes, alert_responsibilities, alert_responsibility, alerts, applications, approval_profiles, approval_user_groups, approvals, attribute_attachments, billing_periods, bookings, charging_periods, communication_profiles, communications, countries, credit_note_applications, credit_notes, custom_attributes, custom_components, custom_events, custom_features, custom_fields, custom_forms, day_of_month, departments, designations, document_templates, email_templates, employees, events, express, external_databases, fulfillment_profiles, fulfillment_terms, gift_certificates, integration_connections, inventory_adjustments, invoice_terms, item_fulfillments, item_receipts, labours, milestones, note_attachments, note_groups, orders, organisation, organisations, payments, portals, pricing_periods, proforma_invoices, purchase_credit_note_applications, purchase_credit_notes, purchase_express, purchase_invoices, purchase_orders, purchase_payments, purchase_refunds, quote_orders, receive_terms, reconciliations, refunds, rental_assets, reports, return_merchandise_authorisations, return_vendor_authorizations, roles, sales_reps, salutations, settings, settings_workflows, shipping_packages, shipping_rules, system_events, tasks, teams, time_periods, timesheets, uom, usages, webhooks, workflows, zones

Each family is four files: <family>.dart (the endpoint functions), <family>_urls.dart (a <Family>Urls class of path templates and body keys), <family>_dto.dart (the views) and <family>_bound.dart (the class client.<family> answers). Eight families have no typed views, because none of their endpoints answered a 2xx that could be measured: aba_files, alert_responsibilities, attribute_attachments, communications, organisations, proforma_invoices, reconciliations and webhooks.

Architecture

Two layers. The core (lib/src/client, config, http, errors.dart, redaction.dart) holds the files, public names, options, defaults and behaviour every family shares. The resource layer (lib/src/resources) is plain functions over the client, four files per family: the functions, the Urls class, the views and the bound class.

The wiring. An endpoint function hands ExsitedClient.send*Request a path template and its parts. The client validates the headers, builds the URL through UrlBuilder and the query through QueryParams, attaches the token from TokenProvider, adds the X-Idempotency-Key on a POST, and passes a TransportRequest to Transport.sendHttpRequest, which runs the attempts under RetryPolicy and answers an ApiResponse. The console and the log sink observe the exchange; Redaction is applied to everything they print.

The Exsited-specific parts:

  • Auth: lib/src/auth/, AuthConfig, the token stores, the token path, and the compare-and-clear 401 replay. TokenProvider(config, store, client) is built by the client; CallOptions, buildDto and the ApiResponse<T> type argument are what the token layer names.
  • The v4 conventions: the cursor envelope and shared Pagination view, the error envelope accessors, the "object": "error" denial, X-Idempotency-Key, the filter and sort helpers, and the ApiVersion enum.
  • Names: ExsitedClient, the EXSITED_ environment prefix, the exsited-dart-sdk/<version> user agent, and the exsited console prefix.
  • The endpoint set, the per-family prefixed libraries and the <Family>Resource bound classes.
  • Safeguards: media_type.dart, the once-per-origin insecure transport warning and the console's control-character guard.

Platform notes

  • Conditional imports are the platform seam. Anything needing dart:io (the file token cache, the native HTTP client factory, the logFile sink, the file probe, the terminal check, the environment read) ships as an _io/_web pair selected by if (dart.library.io), so one package compiles for the VM, Flutter and web.
  • Web caveats. No file system: the token is kept in memory and a configured logFile or tokenCachePath is refused. TLS, proxies and pooling are the browser's. int is a JS double, so very large ids lose precision on web.
  • Async throughout. No Async name suffix; pagination is a lazy Stream.
  • Logging is a hook, not a framework: onLog, onWarning and logFile.
  • The version the SDK reports is Sdk.version (also sdkVersion), and the User-Agent sends it as exsited-dart-sdk/<version>.

Behaviour worth knowing

  • Overrides are typed, not maps. SdkConfig.withOverrides(...) is a copyWith whose result is re-validated. buildClient and SdkConfig.fromCredentials take every override as its own named parameter.
  • A blank path parameter is refused before any I/O, with the template and the slot named, and a null payload does not compile.
  • A header no HTTP layer could send is refused before any I/O, naming the header and never its value; a query parameter given both in the path and in queryParams is refused rather than sent twice.
  • An account is created with createAccount. POST /api/v4/accounts answers 201 for an account body carrying name and type. Items are created directly too, with createItem.
  • id, not uuid. A response carries both; only id addresses the record in a path. The uuid answers 404; filter on filter_uuid instead.
  • Reads are nested, writes are flat. currency reads back as an object and is written as "AUD". Posting a response body back unchanged is a 400.
  • A locked account's delete is a 409 with code internal_error, a business rule wearing a server fault's name. Not retryable.
  • external_databases/{db_name} answers 200 with the whole collection for a name that cannot exist, so nothing in the response says anything went wrong.

Troubleshooting

Symptom Fix
dart: command not found Put the Dart SDK's bin on PATH
SdkConfigError: instance default: clientId: must not be blank A credential arrived blank: the variable is unset, or its name differs
SdkConfigError: ... baseUrl: needs an http:// or https:// scheme Scheme missing, or a query, fragment or user:pass@ is present
A listing stops after the first page You passed the wrong recordKey; <Family>Urls.dataKey names it
SdkTokenLockError The cache directory cannot be locked; set inMemoryToken: true or point tokenCachePath at a filesystem where locking works
A redirect is not followed By design; read Location off response.headers and re-issue deliberately
Want a fresh token Delete the token file, or inject MemoryTokenStore()

Public API

lib/exsited.dart is an allow-list: every export in it carries a show, so the importable surface is exactly what that file names.

Group Names
Entry points ExsitedClient (listResourceNames, resourceByName, close, closeClient, isClosed) ClientRegistry buildClient registerInstances getRegisteredInstances listInstanceNames
Configuration AuthConfig SdkConfig Sdk ApiVersion ResponseFormat readClientName selectCredentials sdkVersion defaultUserAgent defaultMaxResponseBytes responseFormatEnvVar
Requests CallOptions (kept for the token layer), idempotencyHeader idempotencyKeyMethods idempotentMethods; the senders take named parameters
Responses ApiResponse RequestInfo ErrorType deriveEndpointTemplate Dto DtoDecoder
Errors SdkError SdkConfigError SdkUsageError SdkAttributeError SdkAuthError SdkTokenCacheError SdkTokenLockError InsecureTransportWarning FieldError RequestDetail ErrorCategory, and the ...Exception aliases
Resources the accounts and items functions, views, AccountsUrls, ItemsUrls, AccountsResource, ItemsResource, itemFilterParam; BoundResource ResourceFactory registerResourceModule registeredResourceNames resolveResourceModule registerGeneratedResources; the other 89 families in their own libraries
Pagination iterPages iterRecords readNextCursor cursorParam limitParam offsetParam defaultRecordKey noNextPage
Tokens TokenProvider TokenRecord TokenStore MemoryTokenStore FileTokenStore buildCredentialsNamespace defaultTokenCachePath defaultTokenCacheFilename defaultLockWait staleLockDuration maxTokenCacheBytes expirySkew maxTokenTtl and the token grammar checks
HTTP Transport TransportRequest RetryPolicy TransportFailureKind UrlBuilder QueryParams SortDirection buildUrl substitutePathParams buildQueryParams filterParam filterValues filterOperators listOperators presenceOperators sortParam safeMethods defaultRetryableHttpCodes defaultUnsafeRetryableHttpCodes isJsonContentType isJsonMediaType jsonMediaType mediaTypeOf
Diagnostics Console renderExchange buildCurlCommand resolveBoxGlyphs
Redaction Redaction maxRedactionDepth redacted tooDeep circular sensitiveKeyMarkers shouldRedact redactValue redactMapping redactText

License

MIT. See LICENSE.

Libraries

aba_files
The v4 aba files family: its endpoints, bound class, path templates and views.
account_emails
The v4 account emails family: its endpoints, bound class, path templates and views.
account_groups
The v4 account groups family: its endpoints, bound class, path templates and views.
accounting_codes
The v4 accounting codes family: its endpoints, bound class, path templates and views.
action_scripts
The v4 action scripts family: its endpoints, bound class, path templates and views.
additional_codes
The v4 additional codes family: its endpoints, bound class, path templates and views.
alert_responsibilities
The v4 alert responsibilities family: its endpoints, bound class, path templates and views.
alert_responsibility
The v4 alert responsibility family: its endpoints, bound class, path templates and views.
alerts
The v4 alerts family: its endpoints, bound class, path templates and views.
applications
The v4 applications family: its endpoints, bound class, path templates and views.
approval_profiles
The v4 approval profiles family: its endpoints, bound class, path templates and views.
approval_user_groups
The v4 approval user groups family: its endpoints, bound class, path templates and views.
approvals
The v4 approvals family: its endpoints, bound class, path templates and views.
attribute_attachments
The v4 attribute attachments family: its endpoints, bound class, path templates and views.
billing_periods
The v4 billing periods family: its endpoints, bound class, path templates and views.
bookings
The v4 bookings family: its endpoints, bound class, path templates and views.
charging_periods
The v4 charging periods family: its endpoints, bound class, path templates and views.
communication_profiles
The v4 communication profiles family: its endpoints, bound class, path templates and views.
communications
The v4 communications family: its endpoints, bound class, path templates and views.
countries
The v4 countries family: its endpoints, bound class, path templates and views.
credit_note_applications
The v4 credit note applications family: its endpoints, bound class, path templates and views.
credit_notes
The v4 credit notes family: its endpoints, bound class, path templates and views.
custom_attributes
The v4 custom attributes family: its endpoints, bound class, path templates and views.
custom_components
The v4 custom components family: its endpoints, bound class, path templates and views.
custom_events
The v4 custom events family: its endpoints, bound class, path templates and views.
custom_features
The v4 custom features family: its endpoints, bound class, path templates and views.
custom_fields
The v4 custom fields family: its endpoints, bound class, path templates and views.
custom_forms
The v4 custom forms family: its endpoints, bound class, path templates and views.
day_of_month
The v4 day of month family: its endpoints, bound class, path templates and views.
departments
The v4 departments family: its endpoints, bound class, path templates and views.
designations
The v4 designations family: its endpoints, bound class, path templates and views.
document_templates
The v4 document templates family: its endpoints, bound class, path templates and views.
email_templates
The v4 email templates family: its endpoints, bound class, path templates and views.
employees
The v4 employees family: its endpoints, bound class, path templates and views.
events
The v4 events family: its endpoints, bound class, path templates and views.
express
The v4 express family: its endpoints, bound class, path templates and views.
exsited
Dart client library for the Exsited v4 REST API: payloads go out as written, and the API's response comes back unchanged. This library carries the core and the accounts and items families; each other family is its own library, imported with a prefix (import 'package:exsited/payments.dart' as payments;), because 15 top-level names are declared by more than one family and export can neither rename nor prefix them.
external_databases
The v4 external databases family: its endpoints, bound class, path templates and views.
fulfillment_profiles
The v4 fulfillment profiles family: its endpoints, bound class, path templates and views.
fulfillment_terms
The v4 fulfillment terms family: its endpoints, bound class, path templates and views.
gift_certificates
The v4 gift certificates family: its endpoints, bound class, path templates and views.
integration_connections
The v4 integration connections family: its endpoints, bound class, path templates and views.
inventory_adjustments
The v4 inventory adjustments family: its endpoints, bound class, path templates and views.
invoice_terms
The v4 invoice terms family: its endpoints, bound class, path templates and views.
invoices
The v4 invoices family: its endpoints, bound class, path templates and views.
item_fulfillments
The v4 item fulfillments family: its endpoints, bound class, path templates and views.
item_receipts
The v4 item receipts family: its endpoints, bound class, path templates and views.
labours
The v4 labours family: its endpoints, bound class, path templates and views.
milestones
The v4 milestones family: its endpoints, bound class, path templates and views.
note_attachments
The v4 note attachments family: its endpoints, bound class, path templates and views.
note_groups
The v4 note groups family: its endpoints, bound class, path templates and views.
orders
The v4 orders family: its endpoints, bound class, path templates and views.
organisation
The v4 organisation family: its endpoints, bound class, path templates and views.
organisations
The v4 organisations family: its endpoints, bound class, path templates and views.
payments
The v4 payments family: its endpoints, bound class, path templates and views.
portals
The v4 portals family: its endpoints, bound class, path templates and views.
pricing_periods
The v4 pricing periods family: its endpoints, bound class, path templates and views.
proforma_invoices
The v4 proforma invoices family: its endpoints, bound class, path templates and views.
purchase_credit_note_applications
The v4 purchase credit note applications family: its endpoints, bound class, path templates and views.
purchase_credit_notes
The v4 purchase credit notes family: its endpoints, bound class, path templates and views.
purchase_express
The v4 purchase express family: its endpoints, bound class, path templates and views.
purchase_invoices
The v4 purchase invoices family: its endpoints, bound class, path templates and views.
purchase_orders
The v4 purchase orders family: its endpoints, bound class, path templates and views.
purchase_payments
The v4 purchase payments family: its endpoints, bound class, path templates and views.
purchase_refunds
The v4 purchase refunds family: its endpoints, bound class, path templates and views.
quote_orders
The v4 quote orders family: its endpoints, bound class, path templates and views.
receive_terms
The v4 receive terms family: its endpoints, bound class, path templates and views.
reconciliations
The v4 reconciliations family: its endpoints, bound class, path templates and views.
refunds
The v4 refunds family: its endpoints, bound class, path templates and views.
rental_assets
The v4 rental assets family: its endpoints, bound class, path templates and views.
reports
The v4 reports family: its endpoints, bound class, path templates and views.
return_merchandise_authorisations
The v4 return merchandise authorisations family: its endpoints, bound class, path templates and views.
return_vendor_authorizations
The v4 return vendor authorizations family: its endpoints, bound class, path templates and views.
roles
The v4 roles family: its endpoints, bound class, path templates and views.
sales_reps
The v4 sales reps family: its endpoints, bound class, path templates and views.
salutations
The v4 salutations family: its endpoints, bound class, path templates and views.
settings
The v4 settings family: its endpoints, bound class, path templates and views.
settings_workflows
The v4 settings workflows family: its endpoints, bound class, path templates and views.
shipping_packages
The v4 shipping packages family: its endpoints, bound class, path templates and views.
shipping_rules
The v4 shipping rules family: its endpoints, bound class, path templates and views.
system_events
The v4 system events family: its endpoints, bound class, path templates and views.
tasks
The v4 tasks family: its endpoints, bound class, path templates and views.
teams
The v4 teams family: its endpoints, bound class, path templates and views.
time_periods
The v4 time periods family: its endpoints, bound class, path templates and views.
timesheets
The v4 timesheets family: its endpoints, bound class, path templates and views.
uom
The v4 uom family: its endpoints, bound class, path templates and views.
usages
The v4 usages family: its endpoints, bound class, path templates and views.
webhooks
The v4 webhooks family: its endpoints, bound class, path templates and views.
workflows
The v4 workflows family: its endpoints, bound class, path templates and views.
zones
The v4 zones family: its endpoints, bound class, path templates and views.