# ContactManager API
ContactManager exposes one underlying implementation (Epona.ContactManager.Web.Api.ContactManager) over two protocols, hosted side by side on the same web application:
| Protocol | Base URL | Reference |
|---|---|---|
| SOAP | {BaseUrl}Api/ContactManager.asmx (WSDL at ?WSDL) | SOAP.md |
| REST / JSON | {BaseUrl}Api/ContactManager.svc | REST.md |
{BaseUrl} is the site's configured base URL (e.g. https://<customer>.eponalegal.com/).
Every method that exists is reachable over SOAP. Only a subset is additionally exposed over REST — a method needs an explicit [OperationContract] plus [WebGet]/[WebInvoke] attribute in the source to get a REST route; the rest are SOAP-only. See each page for the exact method list.
# Authentication
Both protocols share the same underlying session/login mechanism — the web application runs with AspNetCompatibilityRequirementsMode.Required, so both the classic ASMX (SOAP) and the WCF webHttpBinding (REST) endpoints execute inside the normal ASP.NET pipeline and get the same ASP.NET Session and Forms Authentication cookies.
SOAP: call Login(userName, password). On success, the response carries two cookies: the Forms Authentication cookie (named CMAuth, per the site's Web.config) and the ASP.NET session cookie (the ASP.NET default name ASP.NET_SessionId, unless the environment overrides it). Your SOAP client must keep a cookie container and resend both cookies on every subsequent call, or the server won't recognize you as logged in. IsAuthenticated() and Logout() are also available. Windows/NTLM authentication is supported by the ASP.NET pipeline in principle, but the site's current Web.config has that authentication mode commented out in favor of Forms Authentication — check with whoever manages the target environment before assuming Windows auth is active there.
REST: call LoginSecure (POST, JSON body). Same story — it delegates straight to the same Login logic, so a successful call sets the same session/Forms-auth cookies, and your HTTP client needs to preserve cookies across requests (e.g. a CookieContainer in .NET, -SessionVariable/-WebSession in PowerShell's Invoke-RestMethod). There's no bearer token returned in the response body — the cookie is the session.
For end-to-end REST login, the practical sequence is:
POST .../LoginSecurewith a JSON body (see REST.md for the exact shape).- Keep using the same cookie-aware session for all further calls.
- Optionally confirm with
GET .../IsAuthenticatedandGET .../GetCurrentUserName.
# SameSite cookie attribute and CSRF protection
As of version 26.2, ContactManager's session/authentication cookies (including CMAuth) carry the SameSite=Lax attribute, and state-changing requests to the browser UI require an anti-CSRF token. The anti-CSRF token requirement applies only to the interactive browser UI, not to the SOAP or REST API described here.
The SOAP and REST endpoints share the same CMAuth and ASP.NET session cookies as the browser UI, so for the API surface, SameSite=Lax is currently the only cross-site protection in place — there is no anti-CSRF token check. This fully covers SOAP calls, since the SOAP endpoint requires a text/xml (or application/soap+xml) request body that a plain cross-site <form> cannot produce. It does not fully cover every REST operation: a handful of operations take all their parameters in the URL with no JSON body — for example Logout, ChangeDefaultCulture, SaveActivity, SaveActivityDescription, and DeleteAttendee (see REST.md) — and a plain cross-site <form method="POST"> can still reach them, since that request type needs no preflight. SameSite=Lax blocks it in current browsers, but does nothing on clients that don't enforce SameSite (older Safari, or a caller embedding ContactManager under legacy IE compatibility mode). If your integration is exposed to that residual risk, check with Epona on the current status of API-level CSRF protection before relying on SameSite alone.
Separately: if you embed the ContactManager UI itself (e.g. in an iframe on another site), SameSite=Lax means the session cookie will not be sent on cross-site requests that aren't top-level navigations — check with whoever manages the target environment if an embedded scenario stops working after an upgrade.
# Microsoft 365 / Entra ID login
If the target environment has EnableOffice365Login turned on, and the userName you send contains @, the server treats password not as a literal password but as a Microsoft Graph bearer access token for that user. It calls Microsoft Graph's /v1.0/me with that token to identify the signed-in Microsoft account, then matches it to an existing CM user by email/username. If EnableOffice365Login is off (or the username has no @), password is treated as a literal ContactManager password instead — sending a Graph token in that case will simply fail to match, not error.
# Common conventions across both protocols
- Method versioning: a released method is never changed. A behavioral or field-level change ships as a new method with an incrementing suffix instead — hence
GetContact/GetContact2,GetMatter/GetMatter2/GetMatter3, and thePerson2/Company2/Employee2/Address2entity shapes. Existing integrations keep working against the unsuffixed method; new integrations should use the highest-numbered variant. - ID parameters generally accept either the ContactManager internal numeric ID, or an external-identifier format
Application@ExternalID(and sometimesApplication@ExternalID@ExternalCode) for records synchronized from another system. For lookup-item types, a lookup code also works directly. - Almost every read/search method checks
IsAuthenticated()first and — if not authenticated — returns an empty/default value (empty string, empty object, or a placeholder array) rather than an HTTP error. Don't rely on an exception to detect "not logged in"; check the result shape, or better, confirmIsAuthenticated()up front. - Several array-returning methods have a
Configuration.Current.General.MSWordSoapReturnEmptyArrayWhenNoResultstoggle that changes "no results" behavior: whentrue, an empty array is returned; whenfalse(the historical default, kept for an older Word add-in client), a single-element array containing one blank/default record is returned instead. Don't assume "length 1" means "found something" without also checking whether that element is actually populated.