# eVault > eNote creation, inventory, transfers, and MERS eRegistry actions. ## OpenAPI specs - [Authentication](https://developers.snapdocs.com/content/evault/specs/authentication.json) - [eNote Creation](https://developers.snapdocs.com/content/evault/specs/enote-creation.json) - [eHELOC Creation](https://developers.snapdocs.com/content/evault/specs/heloc-creation.json) - [eNote Inventory](https://developers.snapdocs.com/content/evault/specs/enote-inventory.json) - [eNote Custom Data](https://developers.snapdocs.com/content/evault/specs/enote-custom-data.json) - [MERS Actions](https://developers.snapdocs.com/content/evault/specs/mers-actions.json) - [Auto Validation](https://developers.snapdocs.com/content/evault/specs/auto-val.json) - [Lender Preferences](https://developers.snapdocs.com/content/evault/specs/lender-preferences.json) - [Users](https://developers.snapdocs.com/content/evault/specs/users.json) (Specs are schemas, not prose — fetch the JSON directly rather than pasting it here.) ## Shared platform guides Authentication, environments, webhooks, and testing are shared across every Snapdocs product — see [Getting Started](https://developers.snapdocs.com/platform/llms.txt). ## Authentication & Security ### Authentication Overview # Authentication Overview ## Authentication Snapdocs eVault uses OAuth 2.0, an industry-standard protocol that allows us to grant access to our API without sharing unique credentials with a third party. Tokens assigned to authenticated clients are required to access protected resources. ![](../images/auth_client_flow_1.png) ## OAuth 2.0 Grant Type The type of access called "OAuth 2.0 grant type" used for Snapdocs eVault is client credentials. Here, the username and password are not required, instead, you obtain the Access Token by providing the client id, client secret, and the audience. ## Client ID, Client Secret, Grant, Scope and Audience Your Customer Success Manager will reach out to you and provide your Client ID, Client Secret, as well as a list of scopes you have access to. These API keys carry many privileges, so be sure to keep them secure! * `client_id` the unique Client ID provided by Snapdocs * `client_secret` the unique Client Secret provided by Snapdocs * `grant_type` always set to `client_credentials` * `audience` the API you intend to call using the token generated by this request, example `https://evault-user-login.snapdocs.com/api/federated_apps/` * `scope` the authorization codes used to control the access to API endpoints, examples: * `documents:basic` used for eNote creation * `auto_validation:basic` used for auto validation ## Access Token The access token is obtained by doing a POST call to the Authorization Server's token endpoint > 📘 A token expires 2 hours from issue time. We do not support refresh tokens at this time > 🚧 To optimize system performance and ensure API stability, clients should cache and reuse authentication tokens for the entire duration of their validity. See the [Auth Bearer Token Caching](https://developers.snapdocs.com/evault/guides/authentication-security/auth_bearer_token_caching) section for more information. Access tokens are mapped to your credentials and determine your authorization to call the approved APIs you connected to your App. ## Access protected resources All requests you make to Snapdocs eVault must contain a valid access token. Requests with invalid tokens will be denied access to the resource with the API, returning an HTTP 401 status code. --- ### Auth Bearer Token Caching # Auth Bearer Token Caching Reusing client_credential bearer tokens for the token's full lifetime. # OAuth 2.0 Bearer Token Caching Best Practices This guide covers best practices for caching and reusing OAuth 2.0 bearer tokens when integrating with Snapdocs APIs. Implementing token caching reduces latency, minimizes load on the authorization server, and improves the overall reliability of your integration. ## Overview Snapdocs APIs use OAuth 2.0 with the `client_credentials` grant type. When you request a token, the response includes an `expires_in` field indicating how long the token remains valid (typically **2 hours / 7200 seconds**). By caching the token and reusing it for its full lifetime, your application avoids the overhead of fetching a new token on every API call. ## Benefits of Token Caching | Concern | Without Caching | With Caching | | -------------------- | ------------------------------------- | --------------------------------- | | Latency per API call | +200–500ms (token fetch round-trip) | \~0ms (in-memory or cache lookup) | | Auth server load | 1 token request per API call | 1 token request per \~2 hours | | Rate limit risk | Higher — more requests to auth server | Negligible | | Reliability | Additional point of failure per call | Token fetch failure is isolated | ## How Token Caching Works The following diagram illustrates the recommended flow. Your application checks for a cached token before each API call and only requests a new one when the cache is empty or the token has expired. ```mermaid sequenceDiagram participant Client as Your Application participant Cache as Token Cache (Redis/Memory) participant Auth as Auth0 (Token Endpoint) participant API as Snapdocs API Client->>Cache: Check for cached token Cache-->>Client: Cache MISS Client->>Auth: POST /oauth/token (client_credentials) Auth-->>Client: { access_token, expires_in: 7200 } Client->>Cache: Store token (TTL = expires_in - 60s buffer) Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Cache: Subsequent API call (within token lifetime) Client->>Cache: Check for cached token Cache-->>Client: Cache HIT ✅ Client->>API: API request with Bearer token API-->>Client: 200 OK Note over Client,Auth: Token fetched only when cache is empty or expired ``` ## Token Lifecycle Decision Flow Use this decision diagram to determine when your application should fetch a new token vs. reuse the cached one. ```mermaid flowchart TD A[API Call Needed] --> B{Cached token exists?} B -- No --> C[Fetch new token from Auth0] C --> D[Cache token with TTL = expires_in - 60s] D --> E[Make API call with Bearer token] B -- Yes --> F{Token expired or near expiry?} F -- No --> E F -- Yes --> C E --> G{Response 401 Unauthorized?} G -- No --> H[Process response] G -- Yes --> I[Invalidate cached token] I --> C ``` ## Implementation Guide ### Step 1: Store the Token with Expiration Metadata When you receive a token response from Auth0, store both the `access_token` and its calculated expiration time. > â„šī¸ **Buffer Time** > > We recommend subtracting a buffer (e.g., 60 seconds) from the `expires_in` value, which is in seconds. This ensures your application refreshes the token before it actually expires, avoiding failed API calls due to clock skew or in-flight request timing. A typical Auth0 token response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 7200 } ``` ### Step 2: Check Cache Before Every API Call Before making any API call, check your cache for a valid token. Only fetch a new one if the cache is empty or the token has expired. ### Step 3: Handle 401 Responses Gracefully If the API returns a `401 Unauthorized`, the token may have been revoked or is otherwise invalid. Invalidate your cached token, fetch a new one, and retry the request once. ## Code Examples ```csharp C# using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; public class SnapdocsTokenManager { private const int BufferSeconds = 60; private readonly string _clientId; private readonly string _clientSecret; private readonly string _audience; private readonly string _tokenUrl; private string _accessToken; private DateTime _expiresAt = DateTime.MinValue; private readonly SemaphoreSlim _semaphore = new(1, 1); private static readonly HttpClient _httpClient = new(); public SnapdocsTokenManager(string clientId, string clientSecret, string audience, string tokenUrl) { _clientId = clientId; _clientSecret = clientSecret; _audience = audience; _tokenUrl = tokenUrl; } public async Task GetTokenAsync() { if (IsTokenValid()) return _accessToken; await _semaphore.WaitAsync(); try { if (IsTokenValid()) return _accessToken; await FetchNewTokenAsync(); return _accessToken; } finally { _semaphore.Release(); } } public void Invalidate() { _accessToken = null; _expiresAt = DateTime.MinValue; } private bool IsTokenValid() => _accessToken != null && DateTime.UtcNow < _expiresAt; private async Task FetchNewTokenAsync() { var payload = new { client_id = _clientId, client_secret = _clientSecret, audience = _audience, grant_type = "client_credentials" }; var content = new StringContent( JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(_tokenUrl, content); response.EnsureSuccessStatusCode(); var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); _accessToken = json.RootElement.GetProperty("access_token").GetString(); var expiresIn = json.RootElement.TryGetProperty("expires_in", out var exp) ? exp.GetInt32() : 7200; _expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - BufferSeconds); } } ``` ```java Java import java.net.URI; import java.net.http.*; import java.time.Instant; import com.google.gson.JsonParser; public class SnapdocsTokenManager { private static final int BUFFER_SECONDS = 60; private final String clientId; private final String clientSecret; private final String audience; private final String tokenUrl; private String accessToken; private Instant expiresAt = Instant.EPOCH; private final Object lock = new Object(); public SnapdocsTokenManager(String clientId, String clientSecret, String audience, String tokenUrl) { this.clientId = clientId; this.clientSecret = clientSecret; this.audience = audience; this.tokenUrl = tokenUrl; } public String getToken() throws Exception { if (isTokenValid()) return accessToken; synchronized (lock) { if (isTokenValid()) return accessToken; fetchNewToken(); return accessToken; } } public void invalidate() { synchronized (lock) { accessToken = null; expiresAt = Instant.EPOCH; } } private boolean isTokenValid() { return accessToken != null && Instant.now().isBefore(expiresAt); } private void fetchNewToken() throws Exception { var body = String.format( "{\"client_id\":\"%s\",\"client_secret\":\"%s\"," + "\"audience\":\"%s\",\"grant_type\":\"client_credentials\"}", clientId, clientSecret, audience ); var request = HttpRequest.newBuilder() .uri(URI.create(tokenUrl)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); var json = JsonParser.parseString(response.body()).getAsJsonObject(); accessToken = json.get("access_token").getAsString(); int expiresIn = json.has("expires_in") ? json.get("expires_in").getAsInt() : 7200; expiresAt = Instant.now().plusSeconds(expiresIn - BUFFER_SECONDS); } } ``` ```python Python import time import threading import requests class SnapdocsTokenManager: """ Thread-safe OAuth2 token manager with automatic caching and refresh. Fetches a new token only when the cached token is expired or near expiry. """ BUFFER_SECONDS = 60 # Refresh 60s before actual expiry def __init__(self, client_id, client_secret, audience, token_url): self._client_id = client_id self._client_secret = client_secret self._audience = audience self._token_url = token_url self._access_token = None self._expires_at = 0 self._lock = threading.Lock() def get_token(self): """Return a valid access token, fetching a new one if needed.""" if self._is_token_valid(): return self._access_token with self._lock: # Double-check after acquiring lock if self._is_token_valid(): return self._access_token self._fetch_new_token() return self._access_token def invalidate(self): """Force token refresh on next call (e.g., after a 401).""" with self._lock: self._access_token = None self._expires_at = 0 def _is_token_valid(self): return ( self._access_token is not None and time.time() < self._expires_at ) def _fetch_new_token(self): response = requests.post(self._token_url, json={ 'client_id': self._client_id, 'client_secret': self._client_secret, 'audience': self._audience, 'grant_type': 'client_credentials', }) response.raise_for_status() data = response.json() self._access_token = data['access_token'] expires_in = data.get('expires_in', 7200) self._expires_at = time.time() + expires_in - self.BUFFER_SECONDS # --- Usage --- token_manager = SnapdocsTokenManager( client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET', audience='https://api.example.com', token_url='https://auth.example.com/oauth/token', ) # Every API call reuses the cached token headers = {'Authorization': f'Bearer {token_manager.get_token()}'} response = requests.get('https://api.example.com/v1/resource/123', headers=headers) # If you receive a 401, invalidate and retry if response.status_code == 401: token_manager.invalidate() headers = {'Authorization': f'Bearer {token_manager.get_token()}'} response = requests.get('https://api.example.com/v1/resource/123', headers=headers) ``` ```ruby Ruby require 'faraday' require 'json' class SnapdocsTokenManager BUFFER_SECONDS = 60 # Refresh 60s before actual expiry def initialize(client_id:, client_secret:, audience:, token_url:) @client_id = client_id @client_secret = client_secret @audience = audience @token_url = token_url @access_token = nil @expires_at = Time.at(0) @mutex = Mutex.new end def get_token return @access_token if token_valid? @mutex.synchronize do return @access_token if token_valid? fetch_new_token @access_token end end def invalidate! @mutex.synchronize do @access_token = nil @expires_at = Time.at(0) end end private def token_valid? !@access_token.nil? && Time.now < @expires_at end def fetch_new_token conn = Faraday.new(url: @token_url) response = conn.post do |req| req.headers['Content-Type'] = 'application/json' req.body = { client_id: @client_id, client_secret: @client_secret, audience: @audience, grant_type: 'client_credentials' }.to_json end raise "Token fetch failed: #{response.status}" unless response.success? data = JSON.parse(response.body) @access_token = data['access_token'] expires_in = (data['expires_in'] || 7200).to_i @expires_at = Time.now + expires_in - BUFFER_SECONDS end end # --- Usage --- token_manager = SnapdocsTokenManager.new( client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET', audience: 'https://api.example.com', token_url: 'https://auth.example.com/oauth/token' ) # Every API call reuses the cached token response = Faraday.get('https://api.example.com/v1/resource/123') do |req| req.headers['Authorization'] = "Bearer #{token_manager.get_token}" req.headers['Content-Type'] = 'application/json' end # If you receive a 401, invalidate and retry if response.status == 401 token_manager.invalidate! response = Faraday.get('https://api.example.com/v1/resource/123') do |req| req.headers['Authorization'] = "Bearer #{token_manager.get_token}" req.headers['Content-Type'] = 'application/json' end end ``` ## Choosing a Cache Backend Any of the following approaches will work. Choose the one that best fits your existing infrastructure: | Priority | Approach | Best For | Trade-offs | | -------- | ------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | | 1ī¸âƒŖ | **Redis / Memcached** | Multi-instance or distributed services | Shared across all instances; requires cache infrastructure | | 2ī¸âƒŖ | **In-memory (singleton)** | Single-instance apps, serverless with warm starts | Simple to implement; lost on restart; not shared across instances | | 3ī¸âƒŖ | **Database** | When neither Redis nor in-memory caching is available | Works with any existing database; slightly higher latency per lookup | > â„šī¸ **Use What You Have** > > If your application already has **Redis or Memcached**, that's the ideal choice — the token is shared across all instances and survives restarts. If not, an **in-memory singleton** is the simplest option and works well for single-instance apps. If neither is readily available, storing the token in a **database table** with an expiration timestamp is a perfectly valid approach — the small overhead of a database read is far less than fetching a new token from Auth0 on every call. ## Multi-Service / Distributed Architecture If your integration spans multiple services that each call Snapdocs APIs, consider a centralized token service: ```mermaid flowchart LR subgraph Your Infrastructure SA[Service A] --> TS[Token Service / Cache] SB[Service B] --> TS SC[Service C] --> TS TS --- Redis[(Redis / Shared Cache)] end TS -->|Fetch on cache miss| Auth[Auth0 Token Endpoint] SA -->|Bearer Token| API[Snapdocs API] SB -->|Bearer Token| API SC -->|Bearer Token| API ``` ## Estimated Implementation Timeline | Task | Estimate | | ---------------------------------------------------- | ----------- | | Implement `TokenManager` class with in-memory cache | 1–2 hours | | Replace direct token fetch calls with `TokenManager` | 1–2 hours | | Add 401 retry logic with token invalidation | 1 hour | | Unit tests for caching, expiry, and thread safety | 2–3 hours | | Integration / end-to-end validation | 1–2 hours | | **Total** | **\~1 day** | > â„šī¸ **Minimal Architectural Change** > > Token caching is a **client-side only** change. No modifications are needed on the Snapdocs side. The change is isolated to how your application manages its bearer token — wrap your existing token-fetch logic in a caching layer and reuse the token for its full lifetime. ## Summary 1. **Fetch a token once** from Auth0 using the `client_credentials` grant 2. **Cache the token** with a TTL of `expires_in - 60 seconds` (buffer for clock skew) 3. **Reuse the cached token** for all API calls within the TTL window 4. **On 401 response**, invalidate the cache and fetch a new token 5. **Thread safety** — use a mutex/lock to prevent concurrent token fetches --- ### Environments # Environments ## Demo The Demo environment is used to test your integration with Snapdocs eVault
| API | URL | | :------------------ | :------------------------------------------------------ | | Token API | `https://login.demo-eks.snpd.io/oauth/token` | | Audience | `https://evault-user-login.snpd.io/api/federated_apps/` | | Snapdocs eVault API | `https://demo.snapdocsevault.com` | ## Production Once your integration is complete, you can switch to the production environment
| API | URL | | :------------------ | :----------------------------------------------------------- | | Token API | `https://login.snapdocs.com/oauth/token` | | Audience | `https://evault-user-login.snapdocs.com/api/federated_apps/` | | Snapdocs eVault API | `https://www.snapdocsevault.com` | --- ## Secondary Integrations ### Integrating to Snapdocs eVault # Integrating to Snapdocs eVault # Introduction There are many secondary lenders and systems which want to automate the eVault back and forth for any loans that contain an eNote. Snapdocs eVault APIs support a robust integration for that automation. Below is a breakdown of the workflows and suggested Architecture details that should help you as you design your integration. Your integration to the Snapdocs eVault can include any of the below workflows. # Workflows ## eNote auto-validation The eNote auto-validation takes data from your system and validates against the data in the eNote. When doing an API integration you can either [submit data in bulk (csv)](https://developers.snapdocs.com/evault/reference/auto-val/operations/submitAutoValidationCsv), similar to the flow offered on the UI, or [submit data one eNote (MIN) at a time](https://developers.snapdocs.com/evault/reference/auto-val/operations/submitAutoValidationEntry). For bulk data submission, you would rely solely on webhook events to receive the result. The diagrams below show the flow for submitting data one eNote at at time (synchronous flow) ### Auto Validation Workflow - eNote already arrived When the eNote is already in the eVault, the auto-validation is triggered instantly and the results will be in the synchronous response. You will still receive a webook event as an event is triggered each time a validation is complete. If the validation fail, you can re-submit new data and run the validation again. ```mermaid sequenceDiagram participant EVaultIntegration as eVault Integration participant SnapdocsEVault as Snapdocs eVault participant MERS MERS->>SnapdocsEVault: eNote eDelivery SnapdocsEVault-->>EVaultIntegration: webhook: edelivery accepted distribution EVaultIntegration->>SnapdocsEVault: submit eNote data SnapdocsEVault->>EVaultIntegration: synchronous response with results SnapdocsEVault-->>EVaultIntegration: webhook: auto validation result (passed / failed) EVaultIntegration->>SnapdocsEVault: get auto validation result details opt After validation failure EVaultIntegration ->> SnapdocsEVault: submit updated eNote data SnapdocsEVault->>EVaultIntegration: synchronous response with result SnapdocsEVault-->>EVaultIntegration: webhook: auto validation result (cleared / failed) EVaultIntegration->>SnapdocsEVault: get auto validation result details end ``` ### Auto Validation Workflow - eNote arrives after request When the eNote data is submitted before the eNote has been eDelivered to the eVault, we save the data and the auto-validation is automatically triggered upon arrival of the eNote in the eVault. A webhook event will be sent when the auto-validation is complete. If the validation fail, you can re-submit new data and run the validation again. ```mermaid sequenceDiagram participant EVaultIntegration as eVault Integration participant SnapdocsEVault as Snapdocs eVault participant MERS EVaultIntegration->>SnapdocsEVault: submit eNote data SnapdocsEVault->>EVaultIntegration: synchronous response without result MERS->>SnapdocsEVault: eNote eDelivery SnapdocsEVault-->>EVaultIntegration: webhook: edelivery accepted distribution SnapdocsEVault->>SnapdocsEVault: run eNote validation SnapdocsEVault-->>EVaultIntegration: webhook: auto validation result (passed / failed) EVaultIntegration->>SnapdocsEVault: get auto validation result details opt After validation failure EVaultIntegration ->> SnapdocsEVault: submit updated eNote data SnapdocsEVault->>EVaultIntegration: synchronous response SnapdocsEVault-->>EVaultIntegration: webhook: auto validation result (cleared / failed) EVaultIntegration->>SnapdocsEVault: get auto validation result details end ``` ## eNote Transfer and eDelivery ### Incoming Transfer and eDelivery #### Transfer workflow This sequence of events is triggered when a partner initiate a transfer of rights to your MERS Org ID. ```mermaid sequenceDiagram participant EVaultIntegration as eVault Integration participant SnapdocsEVault as Snapdocs eVault participant MERS MERS->>SnapdocsEVault: transfer received SnapdocsEVault-->>EVaultIntegration: webhook: transfer received SnapdocsEVault->>MERS: transfer accepted MERS->>MERS: all parties accept transfer MERS->>SnapdocsEVault:transfer complete SnapdocsEVault-->>EVaultIntegration: webhook: transfer accepted ``` #### eDelivery workflow This sequence of events is triggered when a partner sends a copy of the note to your eVault. This is a separate action from the actual transfer or rights event though those two actions are usually performed simultaneously. There is no guaranteed order between the transfer and the eDelivery events for a given MIN. ```mermaid sequenceDiagram participant EVaultIntegration as eVault Integration participant SnapdocsEVault as Snapdocs eVault participant MERS MERS->>SnapdocsEVault: edelivery initiated SnapdocsEVault-->>EVaultIntegration: webhook: edelivery received SnapdocsEVault->>MERS: confirm eDelivery SnapdocsEVault-->>EVaultIntegration: webhook: edelivery accepted confirmation MERS->>SnapdocsEVault: deliver eNote file SnapdocsEVault->>MERS: accept edelivery SnapdocsEVault->>SnapdocsEVault: eNote saved to eVault SnapdocsEVault-->>EVaultIntegration: webhook: edelivery accepted distribution ``` ### Outgoing Transfer and eDelivery This sequence of events is triggered when a transfer of rights and an eNote eDelivery is initiated by you to one of your partners. When a [transfer is initiated](https://developers.snapdocs.com/evault/reference/mers-actions/operations/initiateTransfer), an eDelivery to the partners involved in the transfer is triggered automatically by the eVault unless specified otherwise. However, those are two separate actions to the MERS eRegistry. This flow diagram shows the happy path for outgoing transfer + eDelivery (no failure). There will be as many eDeliveries submitted to MERS as there are unique partners involved in the transfer or rights. ```mermaid sequenceDiagram participant EVaultIntegration as eVault Integration participant SnapdocsEVault as Snapdocs eVault participant MERS EVaultIntegration->>SnapdocsEVault: initiate transfer + eDelivery SnapdocsEVault->>MERS: submit transfer SnapdocsEVault-->EVaultIntegration: webhook: transfer submitted SnapdocsEVault->>MERS: submit edelivery SnapdocsEVault-->EVaultIntegration: webhook: edelivery submitted MERS->>MERS: all participant accept the transfer MERS->>SnapdocsEVault: transfer accepted SnapdocsEVault-->EVaultIntegration: webhook: transfer accepted MERS->>MERS: recipient accepted edelivery MERS->>SnapdocsEVault: edelivery accepted confirmation SnapdocsEVault-->EVaultIntegration: webhook: edelivery accepted confirmation MERS->>MERS: recipitent acknowledge reception of the enote file MERS->>SnapdocsEVault: edelivery accepted distribution SnapdocsEVault-->EVaultIntegration: webhook: edelivery accepted distribution ``` Note that the "accepted distribution" step in MERS is optional, hence receiving `edelivery_accepted_distribution` webhook event is not guaranteed. ## Snapdocs eVault event Handling If you look at the below architecture you can see that we suggest the listener listens for the eVault events, and offloads the work to appropriate handlers. Each handler would execute the updates in your Secondary System that makes sense for the event type, including making follow-on API calls to the eVault to gather more eNote details. # Architecture Diagram The below diagram shows how Snapdocs believes an integration between Secondary Loan Systems and the Snapdocs eVault should be built. The interactions are bidirectional, and the integration supports connections to submit to Snapdocs eVault in the case of Auto Validation requests or Transfers as well as listens for events such as the eDelivery and Transfer arrivals or Auto Validation results. Snapdocs recommends separating out the successful arrival from an event at a listener endpoint, sending the 200 response acknowledgement. The processing of that message can be managed by various handler snippets that will react appropriately depending on the event type, the specifics of rights holder expectations of your secondary system, and the capabilities of your backend system to accept data change. ![Diagram of a bidirectional integration between a Secondary Loan System and Snapdocs eVault](../images/eVault_API_diagram.jpg) --- ### Transfer of rights # Transfer of rights You can initiate the transfer of rights of an eNote using our API. To initiate a transfer you need to specify the eNote ID provided by Snapdocs closings and the MERS organization ID of your partners to transfer the note to. Snapdocs eVault automatically detects the MERS transfer action based on the right holders you specified in the payload (see table below). If the combination of rights you specified is not available, we return an error (400). In order to be able to submit a transfer you need to hold specific rights on a note, if our records indicate that you do not have the rights, we return an error (400). When doing a transfer, you can also specify to simultaneously eDeliver the eNote to all your partners involved in the transfer. One eDelivery request will be submitted to MERS per unique MERS organization ID. Both transfer and eDeliveries are asynchronous actions. Upon successfully submitting the transfer (and eDeliveries) we will return the "transmissions" (referring to MERS requests like transfers and eDeliveries) ids, you can use the GET endpoints to retrieve the statuses of those transmissions. ### Transfer actions * TransferControl: * `transfer_to_controller_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * TransferLocation: * `transfer_to_location_org_id` * TransferControlAndLocation: * `transfer_to_controller_org_id` * `transfer_to_location_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * TransferDelegatee (transfer of servicing): * `transfer_to_delegatee_org_id` * optional: `transfer_to_subservicer_org_id` * TransferAll: * `transfer_to_controller_org_id` * `transfer_to_location_org_id` * `transfer_to_delegatee_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * TransferControlAndDelegatee (transfer of Control and Servicing): * `transfer_to_controller_org_id` * `transfer_to_delegatee_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * TransferControlWithSecuredParty: * `transfer_to_controller_org_id` * `transfer_to_secured_party_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * optional: `transfer_to_secured_party_delegatee_org_id` * TransferControlAndLocationWithSecuredParty: * `transfer_to_controller_org_id` * `transfer_to_location_org_id` * `transfer_to_secured_party_org_id` * optional: `transfer_to_delegatee_for_transfers_org_id` * optional: `transfer_to_secured_party_delegatee_org_id` Note that `transfer_to_delegatee_org_id` references a transfer to the master servicer while `transfer_to_delegatee_for_transfers_org_id` references a transfer to the delegatee for transfers. ### Transfer statuses A GET on the transfer ID allows you to retrieve the status of a transfer | status | direction | description | | :----------------------------- | :----------------- | :--------------------------------------------------------------------------------------------------------------- | | initiated | outbound | the transfer was initiated | | mers\_responded\_failed | inbound / outbound | MERS returned a failure. Error messages can be seen in the `errors` array returned in the payload | | snapdocs\_error | inbound / outbound | A server error occurred in our system | | processing\_accept\_or\_reject | outbound | the transfer was submitted to MERS and is pending acceptance by the receiving parties | | expired | inbound / outbound | we have received a notice that the transfer has expired | | canceled | inbound / outbound | the transfer was canceled by one of the parties | | pending\_documents | inbound | the transfer can not be accepted before a copy of the note has been delivered to the eVault | | can\_accept\_or\_reject | inbound | the transfer is pending accept or reject from the recipient | | no\_rights\_to\_accept | inbound | a transfer notification was received but no action is required | | self\_accepted | inbound | as the recipient, you have accepted the transfer but it is still pending acceptance from all parties | | self\_rejected | inbound | as the recipient, you have rejected the transfer but it is still pending acceptance / rejection from all parties | | self\_rejected | outbound | temporary state where you initiated a cancel on the initiated transfer | | all\_accepted | inbound / outbound | all receiving parties have accepted the transfer. The transfer is complete | | all\_rejected | inbound / outbound | at least one receiving party has rejected the transfer. The transfer is complete but did not go through | ### eDelivery statuses A GET on the eDelivery ID allows you to retrieve the status of an eDelivery. Note that for inbound (received) eDeliveries there are two steps involved: first MERS send a pending notification, this notification is automatically confirmed by our system. After receiving the confirmation MERS sends the distribution. | status | direction | description | | :---------------------------------------------- | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | initiated | outbound | the eDelivery was initiated | | mers\_responded\_failed | inbound / outbound | MERS returned a failure | | snapdocs\_error | inbound / outbound | A server error occurred in our system | | can\_confirm\_or\_reject\_receipt | inbound | the pending notification was received and the eDelivery is awaiting confirmation | | confirming\_or\_rejecting\_receipt | inbound | the confirmation was submitted | | accepted\_confirmation | inbound / outbound | the confirmation was accepted | | rejected\_confirmation | inbound / outbound | the confirmation was rejected | | canceled\_confirmation | inbound / outbound | the eDelivery was canceled | | confirmed\_pending\_distribution | | | | distribution\_validation\_processing | inbound | the distribution was received, the distributed documents are being validated | | distribution\_has\_blocking\_validation\_errors | inbound | we identified blocking validation errors. Blocking errors include: the tamper seal on the note did not pass validation, the virus scanning of the included files failed | | can\_accept\_or\_reject\_distribution | inbound | the distribution was received and blocking validations passed, the distribution can be accepted or rejected | | accepting\_or\_rejecting\_distribution | inbound | temporary state where the distribution acceptance was submitted and we are still waiting for MERS response | | approved\_distribution | inbound / outbound | the distribution was approved (accepted) | | disapproved\_distribution | inbound / outbound | the distribution was rejected | | conditionally\_approved\_distribution | | | | expired | inbound / outbound | the eDelivery has expired | ### Webhooks Webhook transfer events are sent to the URLs configured for your application. We initiate a POST request to the URL, the payload contains the transfer id and the transfer status. Example payload: ``` { "event_type": "transfer_submitted", "status": "processing_accept_or_reject", "transfer_id": "8bbc6a97-6a90-48c6-859b-efda72a1cd36", "display_status": "pending", "event_timestamp": "2026-05-21T12:00:00Z" } ``` A webhook is sent for the following events | event type | description | transfer direction | | :-------------------------------- | :---------------------------------------------------------------------------------------------- | :----------------- | | transfer\_submitted | the transfer was submitted to MERS | outbound | | transfer\_failed | the transfer was submitted to MERS but MERS returned an error / failure | inbound / outbound | | transfer\_received | a transfer was received | inbound | | transfer\_accepted | the transfer was accepted by all the receiving parties | inbound / outbound | | transfer\_rejected | the transfer was rejected by one of the involved parties | inbound / outbound | | transfer\_expired | the transfer has expired before all the parties involved submitted their acceptance / rejection | inbound / outbound | | transfer\_canceled | the transfer was canceled by the submitting party | inbound / outbound | | edelivery\_submitted | the edelivery was submitted to MERS | outbound | | edelivery\_failed | the edelivery was submitted to MERS but MERS returned an error / failure | inbound / outbound | | edelivery\_accepted\_confirmation | the edelivery confimation was accepted | inbound / outbound | | edelivery\_accepted\_distribution | the edelivery distribution was accepted | inbound / outbound | | edelivery\_rejected\_confirmation | the edelivery confirmation was rejected | inbound / outbound | | edelivery\_rejected\_distribution | the edelivery disribution was rejected | inbound / outbound | | edelivery\_expired | the edelivery has expired | inbound / outbound | | edelivery\_canceled | the edelivery was canceled by the submitting party | inbound / outbound | The `display_status` is a simplified status of the transfer or eDelivery (this is what is typically showed in the transfers and eDeliveries tables in the UI). Possible values for `display_status` are: | value | description | direction | | :------- | :-------------------------------------------------------------------------------- | :----------------- | | accepted | The transfer or eDelivery was accepted (success) | inbound / outbound | | blocked | The eDelivery is blocked because of validation errors (failure) | inbound | | expired | The transfer or eDelivery has expired (failure) | inbound / outbound | | failed | An error occurred (failure) | inbound / outbound | | pending | The transfer or eDelivery is pending accept or reject from the involved parties | inbound / outbound | | rejected | The transfer or eDelivery was rejected from one of the involved parties (failure) | inbound / outbound | | canceled | The transfer or eDelivery was canceled | inbound / outbound | Note that this parameter is available both in the webhook payload and in the transfer and eDelivery object attributes. --- ### Registration # Registration You can initiate the registration of an eNote with MERS using our API. Registration enrolls an eNote in MERS and establishes your organization as the controller of the note. You can register one or multiple documents in a single request. Registration is an asynchronous action. Upon successfully submitting the registration request we return the registration `mers_action_id`. You can use the GET endpoint to retrieve the status of the registration. ### Registration actions Two action types are available when registering an eNote: * **Registration**: Standard eNote registration. Your organization is established as the controller. * **RegistrationAndTransferControl**: Registration combined with an immediate transfer of control to your broker. Used when a broker MERS organization is associated with the eMortgage package. ### Registration statuses A GET on the registration ID allows you to retrieve the status of a registration. | status | direction | description | | :---------------------- | :----------------- | :------------------------------------------------------------------------------------------------ | | initiated | outbound | the registration was initiated | | complete | inbound / outbound | the registration completed successfully | | mers\_responded\_failed | inbound / outbound | MERS returned a failure. Error messages can be seen in the `errors` array returned in the payload | | snapdocs\_error | inbound / outbound | A server error occurred in our system | --- ### MERS document types # MERS document types This is the list of document types allowed by MERS. A file can be added to an eNote with any of those document types. Any file with one of those document types is considered a supplemental document. Note that `Other` is also allowed as a document type but we can not perform the action of adding a file to an eNote with this type at the moment. However, one could get a document eDelivered with `document_type: 'Other'` ``` 203KConsultantReport 203KHomeownerAcknowledgement 203KInitialDrawRequest 203KMaximumMortgageWorksheet 203KRehabilitationAgreement AbstractNoticeAgreement AcknowledgementOfNoticeOfRightToCancel AffiliatedBusinessArrangementDisclosure AirportNoisePollutionAgreement AmendatoryClause AmortizationSchedule AppraisalRecertificationForm AppraisalReport ApprovalLetter ArticlesOfIncorporation AssignmentOfMortgage AssignmentOfRents AssignmentOfTrade AssumptionAgreement AssuranceOfCompletion AttorneyInFactAffidavit AutomatedClearingHouseDebitForm AutomatedUnderwritingFeedback AutomatedValueModelFeedback BaileeLetter BalloonRefinanceDisclosure BankDepositSlip BankStatement BankruptcyDischargeNotice Bid BirthCertificate BondCertificate BorrowerAcknowledgmentOfPropertyCondition BorrowerCorrespondence BorrowersCertification BrokerDisclosureStatement BrokerPriceOpinion BuildersCertification BuildingPermit BusinessLicense BuydownAgreement BuyingYourHomeSettlementCostsAndHelpfulInformation CABOCertification CancellationOfListing Check Checklist ChildSupportVerification CloseLineOfCreditRequest ClosingInstructions ComplianceAgreement ComplianceInspectionReport ConditionalCommitment CondominiumOccupancyCertificate ConservatorAndGuardianshipAgreement ConsolidationExtensionModificationAgreement ConstructionCostBreakdown ConsumerHandbookOnAdjustableRateMortgages ConveyanceDeed CooperativeAssignmentOfProprietaryLease CooperativeBylaws CooperativeOperatingBudget CooperativeProprietaryLease CooperativeRecognitionAgreement CooperativeStockCertificate CooperativeStockPower CosignerNotice CounselingCertification CreditAlertInteractiveVoiceResponseSystem CreditCardAuthorization CreditInsuranceAgreement CreditReport DeathCertificate DivorceDecree ElectronicFundsTransfer EnergyEfficientMortgageWorksheet EqualCreditOpportunityActForm EscrowAgreement EscrowForCompletionAgreement EscrowForCompletionLetter EscrowWaiver EstimateOfClosingCostsPaidToThirdParty EstoppelAgreement FACTACreditScoreDisclosure FederalApplicationInsuranceDisclosure FederalSaleOfInsuranceDisclosure FHAFiveDayWaiver FHALimitedDenialOfParticipationGeneralServicesAdministrationChecklist FHAMIPNettingAuthorization FHAMortgageCreditAnalysisWorksheet FHAReferralChecklist FHARefinanceMaximumMortgageWorksheet FinancialStatement FloodHazardNotice FloodInsuranceAgreement ForYourProtectionHomeInspection FreddieMacOwnedStreamlineRefinanceChecklist FundingTransmittal GeneralLoanAcknowledgement GiftLetter GoodFaithEstimate GroupSavingsAgreement HighCostWorksheet HoldHarmlessAgreement HomeBuyerEducationCertification HomeEquityConversionMortgageAntiChurningDisclosure HomeEquityConversionMortgageChoiceOfInsuranceOptionsForm HomeEquityConversionMortgageCounselingWaiverQualification HomeEquityConversionMortgageExtension HomeEquityConversionMortgageFaceToFaceCertification HomeEquityConversionMortgageLoanSubmissionSchedule HomeEquityConversionMortgageNearestLivingRelative HomeEquityConversionMortgageNoticeToBorrower HomeEquityConversionMortgagePaymentPlan HomeEquityLineFreezeLetter HomeownersAssociationCertification IdentityTheftDisclosure IncompleteApplicationNotice IndividualDevelopmentAccountStatement IRS1098 IRS1099MISC IRSW2 IRSW8 IRSW9 ItemizationOfAmountFinanced LandLeaseholdAgreement LastWillAndTestament LenderCorrespondence LineItemBudget LoanApplication LoanClosingNotice LoanPayoffRequest LoanStatement MarriageCertificate MilitaryDischargePapers MortgageInsuranceCertificate MortgageInsuranceConditionalCommitment MortgageInsuranceModification NameAffidavit NonDiplomatVerification NoteAddendum NoteAllonge NoticeOfActionTaken NoticeOfCompletion NoticeOfRightToCancel NoticeOfRightToCopyOfAppraisalReport NoticeToHomebuyer NoticeToLender OccupancyAgreement OccupancyCertification PartnershipAgreement PayStub PaymentHistory PaymentLetter PayoffStatement PermanentResidentID PersonalIdentification PersonalPropertyAppraisalReport PowerOfAttorney PreApplicationDisclosureNotice PrepaymentChargeOptionNotification PrequalificationLetter PrivacyDisclosure PropertyInspectionReport PropertyInsuranceBinder PropertyInsuranceDeclarationsPage PropertyInsurancePolicy PurchaseAgreement RateLockAgreement ReaffirmationAgreement Receipt RelocationBenefitsPackage RelocationBuyoutAgreement RentalAgreement RequestForCopyOfTaxReturn RequestForNoticeOfDefault RoadMaintenanceAgreement SatisfactionOfJudgment SatisfactionOfMortgage Section32DisclosureForm SecurityInstrument SecurityInstrumentModification SecurityInstrumentRider ServicingDisclosureStatement ServicingTransferStatement SettlementStatement SocialSecurityAwardLetter StandardFloodHazardDetermination StatementOfBorrowerBenefit StockCertificate SubordinationAgreement SubsidyAgreement Survey SurveyAffidavit TaxCertificate TaxReturn TenYearWarranty TitleAbstract TitleCommitment TitleInsuranceEndorsement TitleInsurancePolicy TrustAgreement TruthInLendingDisclosure UCC1Statement UCC3Statement UnderwritingTransmittal UtilityBill VACertificateOfEligibility VACertificateOfReasonableValue VACollectionPolicyNotice VAForeclosureBidLetter VAFundingFeeNotice VAInterestRateReductionRefinancingLoanWorksheet VALoanAnalysis VAReportAndCertificationOfLoanDisbursement VARequestForCertificationOfEligibilityForHomeLoanBenefit VAVerificationOfBenefitRelatedIndebtedness VerificationOfCredit VerificationOfDependentCare VerificationOfDeposit VerificationOfEmployment VerificationOfMortgageOrRent VerificationOfSecurities VolunteerEscrowPrepaymentDesignation WireInstructions WireTransferAuthorization WireTransferConfirmation Worksheet ``` --- ### Webhooks # Webhooks ## Introduction Webhooks are HTTP callbacks that allow you to receive real-time notifications about events that happen in our system. When an event occurs, our system sends an HTTP POST request to the configured URL, providing you with the data related to the event. This mechanism allows for asynchronous processing and integration with your application, enabling seamless data flow and event-driven architecture. ## Webhook Configuration ### Setting Up a Webhook Endpoint To start receiving webhook events, you need to set up an endpoint in your application that can accept HTTP POST requests. This endpoint should be capable of processing the incoming data and performing the necessary actions based on the event type. ### Registering the Webhook URL To start receiving events, you need to register your webhook URL with our system. Currently the webhook URL is set up in our system by your Snapdocs Implementation Specialist. There is no per-event subscription: a single URL receives every event your application's scopes cover. Plan your listener to route on `event_type` rather than expecting one endpoint per event. The full event list is in the [eVault Events Catalog](https://developers.snapdocs.com/evault/guides/secondary-integrations/change-status-webhook-events). ## Handling Webhook Events The type of events your application will receive will depend on the scope it has been granted during its creation * applications with scope `mers:basic` will receive all events related to MERS actions — registrations, transfers, eDeliveries, deactivations, assumptions, modifications, and other eRegistry actions * application with scope `auto_validation:basic` will receive all events related to auto validation ### Event Structure Each webhook event contains a JSON payload with information about the event. The event payload will never contain PII or sensitive information. We broadcast various identifiers and statuses. If more information is needed, your application can perform subsequent authenticated API calls. The payload will always include an `event_type` indicating the type of event, a `status` relevant to the event type, an`event_timestamp` and one or more relevant object IDs (e.g., `document_id`). Your application can use this ID to make subsequent requests to our API and retrieve more details about the objects. For example, for a transfer event, the event payload will be: ```Text json { "event_type": "transfer_submitted", "status": "processing_accept_or_reject", "transfer_id": "8bbc6a97-6a90-48c6-859b-efda72a1cd36", "display_status": "pending", "event_timestamp": "2026-05-21T12:00:00Z" } ``` For more details on webhooks payload see: * [transfer of rights](https://developers.snapdocs.com/evault/guides/secondary-integrations/transfer_of_rights#webhooks) for transfer and edelivery webhook events * [Auto-Validation](https://developers.snapdocs.com/evault/guides/secondary-integrations/auto_validation#webhooks) for auto-validation webhook events ### Handle requests * Handle duplicate events: Webhook endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent. * Verify events are sent from Snapdocs eVault: Use webhook signatures to verify if the events are sent from Snapdocs eVault. ### Webhook responses If the webhook arrives successfully, respond quickly with a 200 HTTP status to acknowledge the event was received. The response status should not reflect whether or not your application has successfully processed the event payload. If the webhook does not arrive successfully, respond with an error HTTP status code (`400`-`599`). If we receive an error code in the response, we will attempt to re-send the event up to 13 times over the course of 24 hours from the first attempt, with increasing durations between each subsequent attempt. ## Security To enhance the security of webhook events, we implement HMAC (Hash-based Message Authentication Code) for verifying the authenticity and integrity of the data transmitted between our server and your application. ### Overview HMAC combines a cryptographic hash function with a secret key to ensure that a message (or payload) is both authentic and unaltered. By using HMAC, we can provide a secure mechanism to verify that webhook events are sent by us and have not been tampered with during transmission. ### How it works 1. Secret key: We generate a unique secret key used to sign the webhook payload. This key will be provided to you through secure channels, it is known only to our server and your application. 2. Signature generation: When we send a webhook event, we compute an HMAC signature using a timestamp added to the payload and our secret key. 3. Signature verification: Your application verifies the signature to ensure the payload has not been altered and is indeed from us ### Implementation details When we send a webhook event to your endpoint, it will include the following HTTP headers: * `X-Authorization-Digest`: the algorithm that Snapdocs Connect uses to generate the signature, "HMACSHA256" * `X-Authorization-Timestamp`: the timestamp the message was created in ISO-8601 format, for example "2021-12-17T19:08:59Z" * `X-Authorization-Signature`: the base64 encoded HMAC signature to compare Here is the equivalent in bash of the request made to your application (where `payload` is the webhook event payload and `HEX_KEY` is the secret key): ```shell KEY_HEX="secretkeystring" payload='{"foo":"bar"}' now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") data="${now}${payload}" digest=$(echo -n $data | openssl dgst -sha256 -hmac ${KEY_HEX} -binary | base64) curl -X POST --header "X-Authorization-Digest: HMACSHA256" \ --header "X-Authorization-Timestamp: ${now}" \ --header "X-Authorization-Signature: ${digest}" \ --header 'Content-Type: application/json' \ --data ${payload} ``` Below are steps you can take to verify the HMAC signature: 1. Extract the digest and timestamp from the headers 2. Calculate the payload signature using the digest algorithm (in our case "HMACSHA256") and the secret key provided to you against the timestamp + request body 3. Extract the request signature from the header 4. Compare the request signature and the calculated signature Example verification code: ```ruby Ruby require 'openssl' require 'base64' timestamp = request.headers["X-Authorization-Timestamp"] request_signature = request.headers["X-Authorization-Signature"] data = request.body.read hash_bytes = OpenSSL::HMAC.digest('sha256', hmac_key, timestamp.concat(data)) computed_signature = Base64.strict_encode64(hash_bytes) if computed_signature == request_signature # normal process else # did not come from Snapdocs eVault ``` ```python Python import hmac import hashlib import base64 request_signature = request.headers["X-Authorization-Signature"] timestamp_bytes = bytes(request.headers["X-Authorization-Timestamp"], 'utf-8') hmac_key_bytes = bytes(hmac_key, 'utf-8') data_bytes = request.content hash_bytes = hmac.new(hmac_key_bytes, timestamp_bytes + data_bytes, hashlib.sha256).digest() computed_signature = base64.b64encode(hash_bytes).decode("utf-8") if computed_signature == request_signature: # normal process else: # did not come from Snapdocs eVault ``` ```java Java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; public class WebhookSecurityExample { public static void verifySignature(final String hmac_key, final String request_signature, final String timestamp, final String request_body) { try { final Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); final SecretKeySpec secret_key = new SecretKeySpec(hmac_key.getBytes(), "HmacSHA256"); sha256_HMAC.init(secret_key); final String data = timestamp + request_body; final String encoded_signature = Base64.encodeBase64String(sha256_HMAC.doFinal(data.getBytes())); final String computed_signature = StringUtils.newStringUsAscii(Base64.decodeBase64(encoded_signature)); if (computed_signature.equals(request_signature)) { // normal process } else { // did not come from Snapdocs eVault } } catch (Exception e) { System.out.println("Error"); } } } ``` ```csharp C# using System; using System.IO; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; public class SignatureVerification { public static void VerifyRequestSignature(HttpRequest request, string hmacKey) { // Read headers string timestamp = request.Headers["X-Authorization-Timestamp"]; string requestSignature = request.Headers["X-Authorization-Signature"]; // Read request body string requestBody; using (var reader = new StreamReader(request.Body, Encoding.UTF8)) { requestBody = reader.ReadToEnd(); } // Concatenate timestamp and request body string data = timestamp + requestBody; // Compute HMAC-SHA256 digest using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(hmacKey))) { byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(data)); // Encode the hash in Base64 string computedSignature = Convert.ToBase64String(hashBytes); if (computedSignature == requestSignature) { // Normal process Console.WriteLine("Signature is valid. Proceed with the process."); } else { // Did not come from Snapdocs eVault Console.WriteLine("Invalid signature. Request did not come from Snapdocs eVault."); } } } } ``` --- ### eVault Events Catalog # eVault Events Catalog This document describes webhook events emitted for MERS Actions so your system can react to eNote updates. --- ## Event names Event names use the format: ``` _ ``` ### Outcomes | Outcome | Meaning | |---|---| | `complete` | The action completed successfully. | | `failed` | The action failed. | | `notification_received` | A MERS notification was received, the action on the eNote was executed by one of your partners | ### Events | Event | Description | Common examples | |---|--------|---| | `deactivation` | An eNote was deactivated in MERS | `deactivation_complete`,
`deactivation_failed`,
`deactivation_notification_received` | | `deactivation_reversal` | The eNote deactivation was reversed | `deactivation_reversal_complete`,
`deactivation_reversal_failed`,
`deactivation_reversal_notification_received` | | `assumption` | An assumption was performed on the eNote | `assumption_complete`,
`assumption_failed`,
`assumption_notification_received` | | `assumption_reversal` | The previous assumption was reversed | `assumption_reversal_complete`,
`assumption_reversal_failed`,
`assumption_reversal_notification_received` | | `modification` | A modification was performed on the eNote | `modification_complete`,
`modification_failed`,
`modification_notification_received` | | `modification_reversal` | The previous modification was reversed | `modification_reversal_complete`,
`modification_reversal_failed`,
`modification_reversal_notification_received` | | `add_document_to_mers` | A supplemental doc was added to the eNote and MERS was notified of it | `add_document_to_mers_complete`,
`add_document_to_mers_failed`,
`add_document_to_mers_notification_received` | | `add_document_to_mers_reversal` | The latest added document was removed | `add_document_to_mers_reversal_complete`,
`add_document_to_mers_reversal_failed`,
`add_document_to_mers_reversal_notification_received` | | `change_partner` | A partner was updated on the eNote | `change_partner_complete`,
`change_partner_failed`,
`change_partner_notifcation_received` | | `change_data` | Other data change | `change_data_complete`,
`change_data_failed`,
`change_data_notification_received` | --- ### Events and actions Each event is associated with one or several possible actions | Event | Actions | |---|------| | `deactivation` | `ChargedOff`, `ConvertedToPaper`, `PaidOff`, `PaperReplacement`, `RegistrationReversal`, `TransferredToProprietaryRegistry` | | `deactivation_reversal` | `ChargedOffReversal`, `ConvertedToPaperReversal`, `PaidOffReversal`, `PaperReplacementReversal`, `TransferredToProprietaryRegistryReversal` | | `assumption` | `Assumption` | | `assumption_reversal` | `AssumptionReversal`| | `modification` | `Modification` | | `modification_reversal` | `ModificationReversal` | | `add_document_to_mers` | `AddDocument` | | `add_document_to_mers_reversal` | `DocumentReversal` | | `change_partner` | `AddDelegateeForTransfers`, `ReleaseDelegateeForTransfers`, `AddMasterServicer`, `ReleaseSubservicer` `AddSecuredParty`, `ReverseSecuredParty`, `ReleaseSecuredParty`, `AddSecuredPartyDelegatee`, `ReleaseSecuredPartyDelegatee` | | `change_data` | `Update` | > **Note**: > `ConvertedToPaper` and `ConvertedToPaperReversal` are not allowed actions through our API but notifications could still be sent for those if they happen through the UI or if triggered by one of your partners. ## Payload Webhook requests contain a JSON body with the following structure. ### Top-level fields | Field | Type | Description | |---|---|---| | `mers_action_id` | integer | Unique identifier for this MERS Action in eVault. | | `status` | string | Processing status (e.g. `complete`, `mers_responded_failed`, `snapdocs_error`). | | `display_status` | string | Customer-friendly status. `complete` or `failed`. | | `action` | string \| null | MERS action category (e.g. `ChargedOff`, `PaidOff`, `Assumption`, `AssumptionReversal`). | | `documents` | array | List of impacted documents (see below). | | `errors` | array (optional) | Present when one or more per-document errors exist. | ### `documents[]` Each entry includes: | Field | Type | Description | |---|---|---| | `min_number` | string | MERS MIN for the document. | | `document_id` | integer \| null | eVault document id (when available). | | `emortgage_package_id` | integer \| null | eMortgage package id (when available). | ### `errors[]` (optional) When present, errors are objects with keys such as: | Field | Type | Notes | |---|---|---| | `condition` | string | Example: `Error` | | `min_number` | string | The MIN associated with the error | | `code` | string | MERS error code | | `name` | string \| null | May be null | | `description` | string \| null | May be null | --- ## Example payloads ### Action completed Example event: `deactivation_complete` ```json { "mers_action_id": 12345, "status": "complete", "display_status": "complete", "action": "ChargedOff", "documents": [ { "min_number": "12345678901234567890", "document_id": 987, "emortgage_package_id": 654 } ] } ``` ### Action failed (with errors) Example event: `deactivation_failed` ```json { "mers_action_id": "d485dc53-2155-465f-a78e-3dd6002304c6", "status": "mers_responded_failed", "display_status": "failed", "action": "ChargedOff", "documents": [ { "min_number": "12345678901234567890", "document_id": 987, "emortgage_package_id": 654 } ], "errors": [ { "condition": "Error", "min_number": "12345678901234567890", "code": "123", "name": 'Error name from MERS', "description": 'Error description from MERS' } ] } ``` ### Notification received Example event: `assumption_reversal_notification_received` ```json { "mers_action_id": "6d3f0c13-e54f-42c7-a963-ce1fa9f271d5", "status": "complete", "display_status": "complete", "action": "AssumptionReversal", "documents": [ { "min_number": "12345678901234567890", "document_id": 987, "emortgage_package_id": 654 } ] } ``` --- ### Transfer and eDelivery Webhook payloads # Transfer and eDelivery Webhook payloads Below are more details about the webhooks payload for transfers and edeliveries. Please note, the webhook events do not have `min_number` and other document ids at the root JSON level, as the transaction type supports bulk send multi transaction interactions. Your listener should look for the MIN in either location. ### Outbound transfers This is a transfer initiated by your eVault ``` { "event_type": "transfer_submitted", "status": "processing_accept_or_reject", "display_status": "pending", "event_timestamp": "2026-05-21T12:00:00Z", "transfer_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ``` { "event_type": "transfer_accepted", "display_status": "accepted", "status": "all_accepted", "event_timestamp": "2026-05-21T12:00:00Z", "transfer_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ``` { "event_type": "transfer_failed", "display_status": "failed", "status": "mers_responded_failed", "event_timestamp": "2026-05-21T12:00:00Z", "transfer_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ], "errors": [ { "code": "5602", "condition": "Error", "description": "Error description from MERS", "name": "Error name from MERS", "min_number": "999938014023729867" } ] } ``` ``` { "event_type": "transfer_rejected", "display_status": "rejected", "status": "all_rejected", "event_timestamp": "2026-05-21T12:00:00Z", "transfer_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ### Outbound eDeliveries This is an eDelivery initiated by your eVault ``` { "event_type": "edelivery_submitted", "status": "initiated", "display_status": "pending", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ``` { "event_type": "edelivery_accepted_confirmation", "display_status": "accepted", "status": "accepted_confirmation", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ``` { "event_type": "edelivery_accepted_distribution", "display_status": "accepted", "status": "approved_distribution", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ``` { "event_type": "edelivery_failed", "display_status": "failed", "status": "mers_responded_failed", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "errors": [ { "code": "6103", "condition": "Error", "description": "Error description from MERS", "mers_org": "1234567", "name": "Error name from MERS" } ] } ``` ``` { "event_type": "edelivery_rejected_distribution", "status": "disapproved_distribution", "display_status": "rejected", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "documents": [ { "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22", "min_number": "999938023518486453" } ] } ``` ### Inbound transfers This is a transfer received by your eVault, initiated by a third party ``` { "event_type": "transfer_received", "display_status": "pending", "status": "pending_documents", "event_timestamp": "2026-05-21T12:00:00Z", "transfer_id": "xxx" } ``` ``` { "event_type": "transfer_accepted", "display_status": "accepted", "status": "all_accepted", "event_timestamp": "2026-05-21T12:00:00Z", "documents": [ { "document_id": "yyy", "emortgage_package_id": "zzz", "min_number": "999938144395888007" } ] } ``` For transfer accepted webhook events, the document information will be available only if the inbound eDelivery has been accepted first. ### Inbound eDeliveries This is an eDelivery received by your eVault, initiated by a third party ``` { "event_type": "edelivery_received", "display_status": "pending", "status": "can_confirm_or_reject_receipt", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx" } ``` ``` { "event_type": "edelivery_accepted_confirmation", "display_status": "accepted", "status": "accepted_confirmation", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx" } ``` ``` { "event_type": "edelivery_accepted_distribution", "display_status": "accepted", "status": "approved_distribution", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx", "documents": [ { "document_id": "yyy", "emortgage_package_id": "zzz", "min_number": "999938144395888007", "supplemental_document_count": 0 } ] } ``` `edelivery_accepted_distribution` indicates the document was delivered (saved) to the evault at which point the document and emortgage package ids are available. A document is associated with a MIN number whereas an emortgage package is a "version" of that MIN. A MIN is unique per document in your evault but there could be several emortgage packages for one document (or MIN). If there was a redraw of the note for example, there would be two emortgage packages associated with one document. The current "version" (or emortgage package) of the document is referenced by using `primary_package_id` in the document attributes. For each MIN `supplemental_document_count` indicates whether additional documents were delivered with the note. A supplemental document is defined as a file having a document type for the list provided by MERS in Appendix B of the eRegistry Programming interface (or see the [MERS document types section](https://developers.snapdocs.com/evault/guides/secondary-integrations/mers_document_types)) Note that we provide the document information in an array whereas there will only be one document (eNote) per eDelivery. This is because MERS technically allows eDelivery and Transfer of several documents at once but it is not used because not all eVault providers support this feature. ``` { "event_type": "edelivery_rejected_distribution", "display_status": "rejected", "status": "disapproved_distribution", "event_timestamp": "2026-05-21T12:00:00Z", "edelivery_id": "xxx" } ``` --- ### Registration Webhooks # Registration Webhooks ### Webhooks Webhook registration events are sent to the URLs configured for your application. We initiate a POST request to the URL. The payload contains the registration id, status, and details about the impacted documents. Example payload: ```json { "event_type": "registration_complete", "mers_action_id": "0b6f3a1e-4c9d-4f2a-9b7e-2d8c5e1a7f43", "status": "complete", "display_status": "complete", "action": "Registration", "documents": [ { "min_number": "12345678901234567890", "document_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "emortgage_package_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } ], "event_timestamp": "2026-05-21T12:00:00Z" } ``` A webhook is sent for the following events: | event type | description | direction | | :----------------------------------- | :----------------------------------------------------------------- | :----------------- | | registration\_complete | the registration was accepted by MERS | outbound | | registration\_notification\_received | a registration notification was received from one of your partners | inbound | | registration\_failed | the registration failed | inbound / outbound | The `display_status` is a simplified status (this is what is typically shown in the registrations table in the UI). Possible values for `display_status` are: | value | description | direction | | :------- | :-------------------------------------- | :----------------- | | complete | the registration completed successfully | inbound / outbound | | failed | an error occurred | inbound / outbound | Note that this parameter is available both in the webhook payload and in the registration object attributes. *** ## Payload reference ### Top-level fields | Field | Type | Description | | :---------------- | :--------------- | :------------------------------------------------------------------------------ | | `event_type` | string | The event that triggered the webhook (see event types above). | | `mers_action_id` | string (UUID) | Unique identifier for this registration in eVault. | | `status` | string | Processing status (e.g. `complete`, `mers_responded_failed`, `snapdocs_error`). | | `display_status` | string | Customer-friendly status. `complete` or `failed`. | | `action` | string \| null | Registration action type. `Registration` or `RegistrationAndTransferControl`. | | `documents` | array | List of impacted documents (see below). | | `errors` | array (optional) | Present when MERS returned one or more errors. | | `event_timestamp` | string | ISO 8601 timestamp of when the MERS response was received. | ### `documents[]` Each entry includes: | Field | Type | Description | | :--------------------- | :-------------------- | :------------------------------------- | | `min_number` | string | MERS MIN for the document. | | `document_id` | string (UUID) \| null | eVault document id (when available). | | `emortgage_package_id` | string (UUID) \| null | eMortgage package id (when available). | ### `errors[]` (optional) Present when `status` is `mers_responded_failed`. Each entry includes: | Field | Type | Description | | :------------ | :------------- | :--------------------------------- | | `condition` | string | Always `Error`. | | `min_number` | string | The MIN associated with the error. | | `code` | string | MERS error code. | | `name` | string \| null | MERS error name. | | `description` | string \| null | MERS error description. | *** ## Example payloads ### Registration complete ```json { "event_type": "registration_complete", "mers_action_id": "0b6f3a1e-4c9d-4f2a-9b7e-2d8c5e1a7f43", "status": "complete", "display_status": "complete", "action": "Registration", "documents": [ { "min_number": "12345678901234567890", "document_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "emortgage_package_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } ], "event_timestamp": "2026-05-21T12:00:00Z" } ``` ### Registration failed ```json { "event_type": "registration_failed", "mers_action_id": 12346, "status": "mers_responded_failed", "display_status": "failed", "action": "Registration", "documents": [ { "min_number": "12345678901234567890", "document_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "emortgage_package_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } ], "errors": [ { "condition": "Error", "min_number": "12345678901234567890", "code": "1234", "name": "Error name from MERS", "description": "Error description from MERS" } ], "event_timestamp": "2026-05-21T12:00:00Z" } ``` ### Registration notification received Sent when one of your partners registers an eNote on your behalf. ```json { "event_type": "registration_notification_received", "mers_action_id": 12347, "status": "complete", "display_status": "complete", "action": "Registration", "documents": [ { "min_number": "12345678901234567890", "document_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "emortgage_package_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } ], "event_timestamp": "2026-05-21T12:00:00Z" } ``` --- ### Other Mers Actions Webhooks # Other Mers Actions Webhooks
# MERS Actions webhook events This document describes webhook events emitted for MERS Actions so your system can react to eNote updates. *** ## Event names Event names use the format: ``` [event]_[outcome] ``` ### Outcomes | Outcome | Meaning | | ----------------------- | ---------------------------------------------------------------------------------------------- | | `complete` | The action completed successfully. | | `failed` | The action failed. | | `notification_received` | A MERS notification was received, the action on the eNote was executed by one of your partners | ### Events | Event | Description | Common examples | | --- | --- | --- | | `deactivation` | An eNote was deactivated in MERS | `deactivation_complete`,
`deactivation_failed`,
`deactivation_notification_received` | | `deactivation_reversal` | The eNote deactivation was reversed | `deactivation_reversal_complete`,
`deactivation_reversal_failed`,
`deactivation_reversal_notification_received` | | `assumption` | An assumption was performed on the eNote | `assumption_complete`,
`assumption_failed`,
`assumption_notification_received` | | `assumption_reversal` | The previous assumption was reversed | `assumption_reversal_complete`,
`assumption_reversal_failed`,
`assumption_reversal_notification_received` | | `modification` | A modification was performed on the eNote | `modification_complete`,
`modification_failed`,
`modification_notification_received` | | `modification_reversal` | The previous modification was reversed | `modification_reversal_complete`,
`modification_reversal_failed`,
`modification_reversal_notification_received` | | `add_document_to_mers` | A supplemental doc was added to the eNote and MERS was notified of it | `add_document_to_mers_complete`,
`add_document_to_mers_failed`,
`add_document_to_mers_notification_received` | | `add_document_to_mers_reversal` | The latest added document was removed | `add_document_to_mers_reversal_complete`,
`add_document_to_mers_reversal_failed`,
`add_document_to_mers_reversal_notification_received` | | `change_partner` | A partner was updated on the eNote | `change_partner_complete`,
`change_partner_failed`,
`change_partner_notification_received` | | `change_data` | Other data change | `change_data_complete`,
`change_data_failed`,
`change_data_notification_received` | *** ### Events and actions Each event is associated with one or several possible actions | Event | Actions | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deactivation` | `ChargedOff`, `ConvertedToPaper`, `PaidOff`, `PaperReplacement`, `RegistrationReversal`, `TransferredToProprietaryRegistry` | | `deactivation_reversal` | `ChargedOffReversal`, `ConvertedToPaperReversal`, `PaidOffReversal`, `PaperReplacementReversal`, `TransferredToProprietaryRegistryReversal` | | `assumption` | `Assumption` | | `assumption_reversal` | `AssumptionReversal` | | `modification` | `Modification` | | `modification_reversal` | `ModificationReversal` | | `add_document_to_mers` | `AddDocument` | | `add_document_to_mers_reversal` | `DocumentReversal` | | `change_partner` | `AddDelegateeForTransfers`, `ReleaseDelegateeForTransfers`, `AddMasterServicer`, `ReleaseSubservicer`, `AddSecuredParty`, `ReverseSecuredParty`, `ReleaseSecuredParty`, `AddSecuredPartyDelegatee`, `ReleaseSecuredPartyDelegatee` | | `change_data` | `Update` | > **Note**: `ConvertedToPaper` and `ConvertedToPaperReversal` are not allowed actions through our API but notifications could still be sent for those if they happen through the UI or if triggered by one of your partners. ## Payload Webhook requests contain a JSON body with the following structure. ### Top-level fields | Field | Type | Description | | ---------------- | ---------------- | ---------------------------------------------------------------------------------------- | | `mers_action_id` | string (UUID) | Unique identifier for this MERS Action in eVault. | | `status` | string | Processing status (e.g. `complete`, `mers_responded_failed`, `snapdocs_error`). | | `display_status` | string | Customer-friendly status. `complete` or `failed`. | | `action` | string \| null | MERS action category (e.g. `ChargedOff`, `PaidOff`, `Assumption`, `AssumptionReversal`). | | `documents` | array | List of impacted documents (see below). | | `errors` | array (optional) | Present when one or more per-document errors exist. | | `event_timestamp` | string | ISO 8601 timestamp of the event. | ### `documents[]` Each entry includes: | Field | Type | Description | | ---------------------- | --------------- | -------------------------------------- | | `min_number` | string | MERS MIN for the document. | | `document_id` | string (UUID) \| null | eVault document id (when available). | | `emortgage_package_id` | string (UUID) \| null | eMortgage package id (when available). | ### `errors[]` (optional) When present, errors are objects with keys such as: | Field | Type | Notes | | ------------- | -------------- | --------------------------------- | | `condition` | string | Example: `Error` | | `min_number` | string | The MIN associated with the error | | `code` | string | MERS error code | | `name` | string \| null | May be null | | `description` | string \| null | May be null | *** ## Example payloads ### Action completed Example event: `deactivation_complete` ``` { "mers_action_id": "0b6f3a1e-4c9d-4f2a-9b7e-2d8c5e1a7f43", "event_timestamp": "2026-05-21T12:00:00Z", "status": "complete", "display_status": "complete", "action": "ChargedOff", "documents": [ { "min_number": "12345678901234567890", "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22" } ] } ``` ### Action failed (with errors) Example event: `deactivation_failed` ``` { "mers_action_id": "d485dc53-2155-465f-a78e-3dd6002304c6", "event_timestamp": "2026-05-21T12:00:00Z", "status": "mers_responded_failed", "display_status": "failed", "action": "ChargedOff", "documents": [ { "min_number": "12345678901234567890", "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22" } ], "errors": [ { "condition": "Error", "min_number": "12345678901234567890", "code": "123", "name": "Error name from MERS", "description": "Error description from MERS" } ] } ``` ### Notification received Example event: `assumption_reversal_notification_received` ``` { "mers_action_id": "6d3f0c13-e54f-42c7-a963-ce1fa9f271d5", "event_timestamp": "2026-05-21T12:00:00Z", "status": "complete", "display_status": "complete", "action": "AssumptionReversal", "documents": [ { "min_number": "12345678901234567890", "document_id": "f4dc2c2f-be37-4382-b90d-78ea3a75d095", "emortgage_package_id": "71f067f2-2cdd-45bd-90b1-0ed2ea1f6c22" } ] } ``` --- ### Auto-Validation # Auto-Validation ## Overview The eNote auto-validation feature automatically verifies the consistency of data between an eNote and the data from your system of record. This data from your system of record can be uploaded through our API, either in bulk via a CSV file or individually. This feature helps maintain data accuracy, reducing manual verification efforts and minimizing errors. ## Configuration Your Snapdocs Implementation Specialist will help you enable and configure the auto-validation feature for your eVault. To ensure seamless integration with your existing system, the field names for the eNote data can be customized to match your system data fields. ## Data submission ### Bulk data submission in CSV format Prepare a CSV file with the column names set up during the configuration step. Additional columns can be present in the CSV and will be ignored. e.g. ```Text csv MIN,LOAN_NUMBER,FIRST_PAYMENT_DATE,LOAN_AMOUNT,NOTE_RATE,PI,MATURITY_DATE,STREET_ADDRESS,CITY,STATE,ZIP_CODE,BORR1_FIRST_NAME,BORR1_MIDDLE_NAME,BORR1_LAST_NAME,BORR2_FIRST_NAME,BORR2_MIDDLE_NAME,BORR2_LAST_NAME,MARGIN,MIN_INT_RATE,MAX_INT_RATE,FIRST_RATE_CHANGE_DATE MIN,LOAN_NUMBER,FIRST_PAYMENT_DATE,LOAN_AMOUNT,NOTE_RATE,PI,MATURITY_DATE,STREET_ADDRESS,CITY,STATE,ZIP_CODE,BORR1_FIRST_NAME,BORR1_MIDDLE_NAME,BORR1_LAST_NAME,BORR2_FIRST_NAME,BORR2_MIDDLE_NAME,BORR2_LAST_NAME,MARGIN,MIN_INT_RATE,MAX_INT_RATE,FIRST_RATE_CHANGE_DATE 999938075304902606,123,2023-04-04,750000,7.34,3415.34,2053-04-04,100 MONTGOMERY ST,SAN FRANCSISCO,CA,94104,DAVID,JOHN,GREEN,,,,,,, 999938081241015213,456,2023-04-15,324000,6.43,1500.00,2053-04-15,801 SAMPLE ST,MONROEVILLE,PA,15146,SALLY,,SIGNER,,,,,,, ``` The CSV file can be uploaded through the API endpoint ```Text bash POST /auto_validation_documents ``` The CSV file can not be larger than 50MB. ### Individual eNote data submission To upload individual eNote data for validation, use the following API endpoint: ```Text bash POST /auto_validation_entries/{min_number} ``` The API expects data in JSON format. Below is an example of the expected payload. The keys match the CSV headers set up during the configuration step: ```Text json { "MIN": "999938075304902606", "LOAN_NUMBER": "123", "FIRST_PAYMENT_DATE": "2023-04-04", "LOAN_AMOUNT": "750000", "NOTE_RATE": "7.34", "PI": "3415.34", "MATURITY_DATE": "2053-04-04", "STREET_ADDRESS": "100 MONTGOMERY ST", "CITY": "SAN FRANCSISCO", "STATE": "CA", "ZIP_CODE": "94104", "BORR1_FIRST_NAME": "DAVID", "BORR1_MIDDLE_NAME": "JOHN", "BORR1_LAST_NAME": "GREEN" } ``` Details on the auto validation result for this entry can be retrieved using the following API endpoint: ```Text bash GET /auto_validation_entries/{min_number} ``` ## Validation process ### How it works 1. **Data upload**: data is uploaded through CSV or individual entries 2. **Auto-Validation trigger**: the auto-validation is triggered when both the eNote and a matching entry for that MIN is present in the evault 3. **Results**: * for a bulk submission, the auto-validation results are calculated asynchronously * for individual entry submission the validation results are returned synchronously * the details of an auto-validation entry can be retrieved by using the GET endpoint for a given MIN. * If a webhook url has been set up for your application, an event is sent to notify you of the auto-validation result for a given MIN. ### Auto-validation statuses * **passed**: data matches without discrepancies * **failed**: some data from the entry did not match the eNote data * **cleared**: data matches without discrepancies after data was re-uploaded following a previously failed status on that MIN The auto-validation status is returned in the document details and emortgage package details that can be accessed through the following endpoints (see eNote Inventory API documentation): ```Text bash GET /documents/{document_id} ``` ```Text bash GET /emortgage_packages/{emortgage_package_id} ``` ### Error handling When the auto-validation fails, details of the mismatch data can be retrieved using the API endpoint: ```Text bash GET /auto_validation_entries/{min_number} ``` The auto-validation is run again by uploading a new entry with the same MIN or by getting a new version of the eNote eDelivered to the vault. If all errors are resolved by uploading new data the status is moved to **cleared** ## Webhooks When the validation of an eNote is completed, the system sends a webhook event to a pre-configured URL. This event contains the document id related to the eNote, the MIN and the auto-validation status. When a validation result is calculated, a webhook event is sent with the following payload: ```Text json { "event_type":"auto_validation_result", "document_id":"8165b5d8-7349-4830-9a12-8ea33a6b412a", "emortgage_package_id": "5279e4ca-cd04-49b3-9863-d94e9acc35fe", "min_number":"999938075304902606", "status":"passed", "event_timestamp": "2026-05-21T12:00:00Z" } ``` Possible statuses are `passed`, `failed`, `cleared`. --- ### Auto Validation Workflow # Auto Validation Workflow ## Asynchronous workflow This is achieved when uploading "bulk" data in a CSV format [Auto Validation (POST)](https://developers.snapdocs.com/evault/reference/auto-val/operations/submitAutoValidationCsv). Each row contains the data for a MIN number ### Scenario 1: MIN is already in eVault If the eNote is already in your eVault the autovalidation is automatically triggered after the data upload. Upon completion a webhook event is triggered that contains the status (result of the auto validation) (`passed`, `failed` or `cleared`) ### Scenario 2: Auto Validation data is uploaded before the eNote is in the eVault If the data is uploaded before getting the eNote delivered to the eVault, the data is persisted and the auto validation is triggered automatically after the eNote is saved to the vault. Upon completion a webhook event is triggered that contains the status (result of the auto validation) (`passed`, `failed` or `cleared`) ### Getting more detailed AutoValidation status on a MIN This is achieved through this endpoint: [Show auto-validation data for single enote (GET)](https://developers.snapdocs.com/evault/reference/auto-val/operations/getAutoValidationEntry) #### 1. Data for this MIN has not been uploaded The result will be 404: Not Found #### 2. Data for this MIN has been uploaded but the eNote is not in the eVault `auto_validated` is false, `document_errors` is an empty array and `emortgage_package_id` is null meaning there is no document matching this MIN and the MIN has not been auto validated. Sample response: ``` { "data": { "id": "38762fac-9011-4cc6-8fed-1f4f244318ff", "type": "auto_val_entries", "attributes": { "min_number": "999938007602270063", "auto_validated": false, "emortgage_package_id": null, "document_errors": [], "updated_at": "2025-09-04T21:28:35.095Z" } } } ``` #### 3. Data for this MIN has been uploaded and the Auto Validation failed `auto_validated` is false, `document_errors` is an array containing the error description and `emortgage_package_id` is the ID of the eNote in the eVault Sample response: ``` { "data": { "id": "1583d676-d65b-4562-8861-79f44e4439dd", "type": "auto_val_entries", "attributes": { "min_number": "999938142435612304", "auto_validated": false, "emortgage_package_id": "9ea24526-d938-4db1-93e2-9b7fb821d908", "document_errors": [ { "id": "6d4680a4-645d-491a-a5ac-e737461b9131", "data": { "data": "2020-04-01", "enote": "2019-04-01" }, "csv_header": "1st Payment Date", "code": "1070", "description": "is missing or does not match", "exception_type": "Fatal", "manually_resolved": false }, { "id": "c4806de7-0e32-468a-b533-b8e730e1e8ad", "data": { "data": "200 Montgomery Street", "enote": "100 Montgomery Street" }, "csv_header": "Street Address", "code": "1120", "description": "is missing or does not match", "exception_type": "Fatal", "manually_resolved": false } ], "updated_at": "2025-09-04T12:30:29.427Z" } } } ``` #### 4. Data for this MIN has been uploaded and the Auto Validation passed or cleared `auto_validated` is true, `document_errors` is an empty array and `emortgage_package_id` is the ID of the eNote in the eVault. Sample response: ``` { "data": { "id": "d72e5f7b-d0d5-43eb-9fe2-b9b512dfa1c0", "type": "auto_val_entries", "attributes": { "min_number": "999938088849853317", "auto_validated": true, "emortgage_package_id": "5846382f-fb22-4353-bcf4-afcef78e9e36", "document_errors": [], "updated_at": "2025-09-04T21:12:24.305Z" } } } ```
## Synchronous workflow This is achieved by sending the data for a given MIN in a json format [Submit auto-validation data (POST)](https://developers.snapdocs.com/evault/reference/auto-val/operations/submitAutoValidationEntry) ### Scenario 1. MIN is already in the eVault If the eNote is already in your eVault the autovalidation is automatically triggered and the result of the auto validation is returned synchronously. A webhook event is also triggered that contains the status (result of the auto validation) (`passed`, `failed` or `cleared`) When the auto validation succeeds, `auto_validated` is true, `document_errors` is an empty array and `emortgage_package_id` is the ID of the eNote in the eVault. Sample response ``` { "data": { "id": "642f512d-5d51-40c9-930e-b97a3fdf646b", "type": "auto_val_entries", "attributes": { "min_number": "999938016081385003", "auto_validated": true, "emortgage_package_id": "47ee4cfd-0e19-406a-96f8-602ff03ac06b", "document_errors": [], "updated_at": "2025-09-04T21:52:45.765Z" } } } ``` When the auto validation fails, `auto_validated` is false, `document_errors` is an array containing the error description and `emortgage_package_id` is the ID of the eNote in the eVault Sample response: ``` { "data": { "id": "36873a08-bf8f-4d69-bb9e-1254d028f0b1", "type": "auto_val_entries", "attributes": { "min_number": "999938037252081858", "auto_validated": false, "emortgage_package_id": "c0ff4dbe-d9c5-412b-859b-539d455fa9fb", "document_errors": [ { "id": "deb931b1-fc02-4310-b175-54802ff1dea8", "data": { "data": "Davide", "enote": "David" }, "csv_header": "Borrower 1 First Name", "code": "1040", "description": "is missing or does not match", "exception_type": "Fatal", "manually_resolved": false } ], "updated_at": "2025-09-04T22:01:50.754Z" } } } ```
### Scenario 2. Auto Validation data is uploaded before the eNote is in the eVault If the data is uploaded before getting the eNote delivered to the eVault, the data is persisted. The response has the record (auto validation entry) ID, `auto_validated` is false, `document_errors` is an empty array and `emorgage_package_id` is null indicating that there is no document corresponding to this MIN in the eVault. Sample response: ``` { "data": { "id": "3d04d284-d96e-4c01-b835-6bdbe6139652", "type": "auto_val_entries", "attributes": { "min_number": "999938016081385003", "auto_validated": false, "emortgage_package_id": null, "document_errors": [], "updated_at": "2025-09-04T21:46:20.306Z" } } } ``` The auto validation is triggered automatically when the eNote is saved to the eVault. Upon completion, a webhook event is triggered that contains the status (result of the auto validation) (`passed`, `failed` or `cleared`) --- ### Add Document # Add Document To add a document (supplemental doc) to an eNote, you must first upload a file through the `/reviewable_documents`endpoint. We will perform a virus scan on the uploaded file and store it in our system. The reviewable\_document is a temporary object where we store information on the file being uploaded before it is attached to an eNote. You must specify the MIN number of the eNote associated with the reviewable document and the document type. For a supplemental document we currently support the following document types: * AmortizationSchedule * AssumptionAgreement * AttorneyInFactAffidavit * BuydownAgreement * ConsolidationExtensionModificationAgreement * NoteAddendum * PowerOfAttorney * SecurityInstrument * SecurityInstrumentModification * TitleInsurancePolicy * TrustAgreement In order to be able to be added as a supplemental document, the file uploaded must be a pdf, an image (jpg, jpeg, png, tiff, tif), or an XML SMART Doc. The file must also be less than 200 MB in size. Use the reviewable document id provided in the response to initiate an `add_document` request. This action initiates an asynchronous request to MERS. To know the status of the request, use the `change_data_id` provided in the response. A change data object can have the following status: | status | description | | :---------------------- | :--------------------------------------------------- | | initiated | the request has been created | | in\_progress | the request has been sent to MERS | | complete | the request has completed with no errors | | mers\_responded\_failed | the request has completed but MERS returned an error | | snapdocs\_error | A server error has occurred in our system | When the request to MERS is complete without errors (status = complete) the document that was uploaded is attached to the eNote. When the request to MERS is complete with a failure (status = mers\_responded\_failed) a list of errors returned by MERS is provided in the `change_data` object, the document is not attached to the eNote. ---