Welcome to the Salv AML API documentation.
The Salv API is built on HTTP. Our API is RESTful. It has predictable resource URLs. It returns HTTP response codes to indicate errors. It also accepts and returns JSON in the HTTP body. You can use your favorite HTTP/REST library for your programming language to use Salv API.
API definition to import into Postman can be downloaded at https://docs.salv.com/api/public.yaml
Contact our sales team at sales@salv.com to sign up for service.
While the integration process may wary in each case, on average the steps to use the service look as follows:
Alternatively, all the monitoring alerts, screening alerts and risk levels are available in Salv UI, along with a lot of features to help manage and organize the process of examining the alerts and risks.
In order to use the API, you need to generate client credentials using Salv UI. Never share your secret keys. Keep them guarded and secure.
We use OAuth2 client credentials flow to issue our API tokens. By default our API tokens have expiration time of 50 years, so effectively they never expire. Please do not make any assumptions about the content of the access_token. At the moment we use a JWT token, but it can change to any other string with the future updates.
An API token can be invalidated using Salv UI by deleting the client credentials that were used to generate the token.
Please make sure you only request it once per reasonable amount of time,
as oauth/token
endpoint has a rate limit of 10 requests per minute per IP address.
Token URL for sandbox environment is https://demo.salv.com/oauth/token
clientCredentials
https://app.salv.com/oauth/token
aml
- Can use AML API
$ curl -X POST -u "{clientId}:{clientSecret}" -d "grant_type=client_credentials" "https://app.salv.com/oauth/token"
{
"access_token": "{yourBearerToken}",
"expires_in": 1576799999,
"token_type": "Bearer"
}
import requests
resp = requests.post('https://app.salv.com/oauth/token', auth=({clientId}, {clientSecret}), data={'grant_type': 'client_credentials'})
print(resp.json())
{
"access_token": "{yourBearerToken}",
"expires_in": 1576799999,
"token_type": "Bearer"
}
All requests which return a response with status code 500-599 should be retried after some interval.
If a retry of a failed request fails again, it should be retried again until it succeeds. Retrying a request that repeatedly fails should be done at increasingly longer interval.
If a request returns a response with status code 429, it means that the client has exceeded the rate limit specified in contract.
The request should be retried after some interval.
Format the csv file and add all the mandatory data fields indicated in the User Manual
Upload the csv file via API. Note: the upload API response contains the id that will to be used in status check API call (step 3). When a data file has been successfully uploaded, an HTTP response with 202 status code is returned. A response with 202 status code indicates that the file has been uploaded successfully and the system will try to process the data. It does not guarantee that data will be successfully processed.
What will happen next?
First comes the validation phase:
Note: during this validation phase we do not check if the transaction has been added to the system before.
reason
field in a human readable text. Second (if no errors occurred) comes the upload phase: our system will start uploading and saving data in batches of 1000, and in case of transaction file will check for duplicates and if the person is present in the system.
If no errors occur, then the file is uploaded successfully.
If an error occurs, then when step no 3 (check status API call) is done, then:
and,
Example:
Csv file has 200 000 rows. There is a duplicate id on row 87 456. In validation phase duplicate errors are not checked thus the file will move on to the upload phase. First 87 000 rows are uploaded without any errors. Rows starting from 87 001 will not get uploaded because there is a duplicate error in the batch (the patch of 1000 that gets checked). System will show that there is a duplicate, will show the id of the duplicate Person or Transaction and will show the last uploaded Person or Transaction id and the last id that was not added.
Example of an upload phase error message:
{
"status": "PARTIALLY_COMPLETED",
"reason": "The upload appears to contain transactions (id ameio-1234567) that have already been added to Salv. Please make sure that your upload does not contain any duplicates or transactions that have already been added to Salv. The first transaction not added to Salv with this upload has id fmeio-1234567. Last processed and saved row was with id: dseio-1234567"
}
In order to check the status of the file upload either use get status of data upload API call or check Data upload page on app.salv.com. This will provide the status and any further information about the errors.
Upload a list of person, person relation, or transaction data in a CSV format. Files up to 100M are supported. File processing happens asynchronously.
type required | string (ApiDataUploadType) Enum: "PERSON" "PERSON_RELATION" "TRANSACTION" "TRANSACTION_UPDATE" Example: type=TRANSACTION Which type of data to upload |
file required | string <binary> |
{- "id": 123,
- "fileName": "transactions-2021-11-29.csv",
- "type": "TRANSACTION"
}
This section contains all operations managing the state of persons and transactions.
There are three endpoints for creating or modifying a person, make sure to choose the right one for your task:
ID of a person or a transaction is a case-sensitive text field in our system. Salv does not transform the ID in any way. Only the following characters are allowed: Alphanumerics a-zA-Z0-9
, hyphens -
, underscores _
, colons :
, dots .
.
All requests must use ISO-8601 formatted timestamps. That includes defined fields (e.g. timestamp property in "Create new transaction" request) and custom key-value pairs (e.g. under attributes property in "Create new person" request).
For any data passed in person or transaction objects, the keys must adhere to the following rules:
This example shows how to upload data to use it later for monitoring or screening.
Add a new person, according to the documentation at addPerson(POST).
If the person has already been added to Salv, then skip this step.
curl --request POST \
--url https://app.salv.com/api/v2/persons \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json' \
--data '{
"id": "bbt12345",
"type": "INDIVIDUAL",
"attributes": {
"first_name": "Sheldon",
"last_name": "Cooper",
"dob": "1983-05-12",
"city": "Pasadena",
"street_address": "2311 North Los Robles Avenue",
"country": "US",
"onboarding_date": "2019-04-16",
"email": "[email protected]",
"phone_number": "+1582693582",
"id_document": "passport",
"id_country": "US",
"gender": "F"
}
}'
import requests
url = "https://app.salv.com/api/v2/persons"
payload = {
"id": "bbt12345",
"type": "INDIVIDUAL",
"attributes": {
"first_name": "Sheldon",
"last_name": "Cooper",
"dob": "1983-05-12",
"city": "Pasadena",
"street_address": "2311 North Los Robles Avenue",
"country": "US",
"onboarding_date": "2019-04-16",
"email": "[email protected]",
"phone_number": "+1582693582",
"id_document": "passport",
"id_country": "US",
"gender": "F"
}
}
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
Add a new transaction, according to the documentation at addTransaction(POST).
curl --request POST \
--url https://app.salv.com/api/v1/persons/bbt12345/transactions \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json' \
--data '{
"id": "aeio-1234567",
"attributes": {
"sender_name": "Sheldon Cooper",
"sender_account": "GB123456789",
"receiver_name": "Leonard Hofstadter",
"receiver_account": "DE987654321",
"amount": "15678.34",
"type": "wire_transfer",
"direction": "I",
"timestamp": "2022-01-20T08:48:39.000Z"
}
}'
import requests
url = "https://app.salv.com/api/v1/persons/bbt12345/transactions"
payload = {
"id": "aeio-1234567",
"attributes": {
"sender_name": "Sheldon Cooper",
"sender_account": "GB123456789",
"receiver_name": "Leonard Hofstadter",
"receiver_account": "DE987654321",
"amount": "15678.34",
"type": "wire_transfer",
"direction": "I",
"timestamp": "2022-01-20T08:48:39.000Z"
}
}
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
Attributes supported maximum nesting depth is 2.
id required | string non-empty External unique person ID |
type required | string (ApiPersonType) Enum: "INDIVIDUAL" "PRIVATE" "BUSINESS" "LEGAL" "UNDEFINED" Use
|
required | object Key value pair where key is For For individual persons attributes For legal persons attributes A typical attribute for legal persons is In addition to the typical attributes listed here, custom attributes may also be used. Timestamps may contain up to 6 digits after seconds. |
{- "id": "bbt12345",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16T11:10:55.123456Z",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}
}
Partial person update. Only attributes present in request will be added/updated. Attributes supported maximum nesting depth is 2.
This endpoint also supports optional person type update.
In case the operation is successful there will be an empty response.
$ curl --request PATCH \
--url https://app.salv.com/api/v2/persons/{personId} \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json' \
--data '{
"type": "LEGAL",
"attributes": {
"company_name": "Best Vacuum Cleaning Ltd."
}
}'
import requests
payload = {
"attributes": {
"phone_number": "(198)747-8241x9209",
"shareholders": [{
"id": 1,
"name": "Rose Doe",
"dob": "1970-01-01",
"email": "[email protected]"
}]
}
}
response = requests.post(
'https://app.salv.com/api/v2/persons/{personId}',
auth = ({clientId}, {clientSecret}),
json = payload)
response.raise_for_status()
personId required | string non-empty External unique person ID |
type | string (ApiPersonType) Enum: "INDIVIDUAL" "PRIVATE" "BUSINESS" "LEGAL" "UNDEFINED" Use
|
required | object Key value pair where key is For |
{- "attributes": {
- "last_name": "Doe",
- "dob": "1970-01-01"
}
}
Person update. Attributes in the request will be used to replace existing ones and type will be updated. Attributes supported maximum nesting depth is 2.
In case the operation is successful there will be an empty response.
$ curl --request PUT \
--url https://app.salv.com/api/v2/persons/{personId} \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json' \
--data '{
"type": "LEGAL",
"attributes": {
"company_name": "Best Vacuum Cleaning Ltd."
}
}'
import requests
payload = {
"type": "INDIVIDUAL",
"attributes": {
"phone_number": "(198)747-8241x9209",
"shareholders": [{
"id": 1,
"name": "Rose Doe",
"dob": "1970-01-01",
"email": "[email protected]"
}]
}
}
response = requests.post(
'https://app.salv.com/api/v2/persons/{personId}',
auth = ({clientId}, {clientSecret}),
json = payload)
response.raise_for_status()
personId required | string non-empty External unique person ID |
type required | string (ApiPersonType) Enum: "INDIVIDUAL" "PRIVATE" "BUSINESS" "LEGAL" "UNDEFINED" Use
|
required | object Key value pair where key is For |
{- "type": "INDIVIDUAL",
- "attributes": {
- "last_name": "Doe",
- "dob": "1970-01-01"
}
}
NB! If there are more than 10000 results then the API shows the number of total results as 10000 and you can't paginate past the 10000th result.
page required | integer >= 0 Default: 0 Number of the page |
size required | integer [ 1 .. 50 ] Default: 20 Size of the page |
query | string [ 1 .. 200 ] characters Search query |
attribute | string non-empty Search query attribute |
category | string Enum: "COUNTERPARTY" "CUSTOMER" Person category |
orderBy | string [ 1 .. 200 ] characters Order attribute |
direction | string (ApiSortDirection) Enum: "ASC" "DESC" Order direction |
operator | string (ApiSearchOperator) Default: "IS_EXACTLY" Enum: "IS_EXACTLY" "CONTAINS" "STARTS_WITH" Example: operator=IS_EXACTLY Search query operator which defines whether the search is using the exact term or matches only part of the string |
{- "number": 0,
- "size": 20,
- "total": 1,
- "content": [
- {
- "id": "bbt12345",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}, - "status": "NEW",
- "createdTime": "2019-08-24T14:15:22Z",
- "snoozeConfiguration": {
- "id": 1,
- "personId": 1,
- "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0,
- "totalAmount": 0,
- "note": { }
}, - "category": "CUSTOMER"
}
]
}
Use Create new person v2 instead.
id required | string non-empty External unique person ID |
type required | string (ApiPersonType) Enum: "INDIVIDUAL" "PRIVATE" "BUSINESS" "LEGAL" "UNDEFINED" Use
|
object Person attributes key value pair. Keys |
{- "id": "bbt12345",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16T11:10:55.123456Z",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}
}
{- "id": "bbt12345",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}
}
personId required | string non-empty ID of person to return |
{- "id": "bbt12345",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}, - "status": "NEW",
- "createdTime": "2019-08-24T14:15:22Z",
- "snoozeConfiguration": {
- "id": 1,
- "personId": 1,
- "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0,
- "totalAmount": 0,
- "note": { }
}, - "category": "CUSTOMER"
}
Use Patch person v2 instead.
personId required | string non-empty ID of person to return |
id required | string non-empty External unique person ID |
type | string (ApiPersonType) Enum: "INDIVIDUAL" "PRIVATE" "BUSINESS" "LEGAL" "UNDEFINED" Use
|
object Person attributes key value pair | |
status | string Enum: "NEW" "REVIEWED" "MONITORING" "SUSPENDED" "TERMINATED" "TERMINATED_AND_REPORTED" Person current status |
createdTime | string <date-time> |
object (ApiSnoozeConfiguration) | |
Array of objects (ApiSnoozeConfiguration) Configurations for scenario based snooze configurations | |
category | string (ApiPersonCategory) Value: "COUNTERPARTY" |
{- "id": "user-00000009",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}, - "status": "NEW",
- "createdTime": "2019-08-24T14:15:22Z",
- "snoozeConfiguration": {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}, - "scenarioSnoozeConfigurations": [
- {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}
], - "category": "COUNTERPARTY"
}
{- "id": "user-00000009",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}, - "status": "NEW",
- "createdTime": "2019-08-24T14:15:22Z",
- "snoozeConfiguration": {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}, - "scenarioSnoozeConfigurations": [
- {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}
], - "category": "COUNTERPARTY"
}
NB! If there are more than 10000 results then the API shows the number of total results as 10000 and you can't paginate past the 10000th result.
personId required | string non-empty ID of person |
page required | integer >= 0 Default: 0 Number of the page |
size required | integer [ 1 .. 50 ] Default: 20 Size of the page |
query | string [ 1 .. 200 ] characters Search query |
orderBy | string [ 1 .. 200 ] characters Order attribute |
direction | string (ApiSortDirection) Enum: "ASC" "DESC" Order direction |
{- "number": 0,
- "size": 0,
- "total": 0,
- "content": [
- {
- "id": "transaction-000000000009",
- "attributes": {
- "sender_name": "John Doe",
- "sender_account": "EE106637001834923964",
- "receiver_name": "Jane Doe",
- "receiver_account": "EE926693048477018590",
- "amount": "100.99",
- "type": "Bank Transfer",
- "direction": "O",
- "status": "Completed",
- "reference": "payment",
- "timestamp": "2014-11-23T03:04:50.000000Z"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "personId": "string"
}
]
}
personId required | string non-empty Example: bbt12345 ID of person |
id required | string non-empty External unique transaction ID |
object Transaction attributes key value pair.
Attributes Other attributes vary depending on the kind of transaction. Please see the examples on the right hand side for inspiration. Value for |
{- "id": "aeio-1234567",
- "attributes": {
- "sender_name": "Sheldon Cooper",
- "sender_account": "GB123456789",
- "receiver_name": "Leonard Hofstadter",
- "receiver_account": "DE987654321",
- "amount": "15678.34",
- "type": "wire_transfer",
- "direction": "I",
- "timestamp": "2022-01-20T08:48:39Z"
}
}
{- "id": "aeio-1234567",
- "personId": "bbt12345",
- "attributes": {
- "sender_name": "Sheldon Cooper",
- "sender_account": "GB123456789",
- "receiver_name": "Leonard Hofstadter",
- "receiver_account": "DE987654321",
- "amount": "15678.34",
- "type": "wire_transfer",
- "direction": "I",
- "timestamp": "2022-01-20T08:48:39Z"
}
}
personId required | string non-empty ID of person |
transactionId required | string non-empty ID of transaction to return |
{- "id": "aeio-1234567",
- "personId": "bbt12345",
- "attributes": {
- "sender_name": "Sheldon Cooper",
- "sender_account": "GB123456789",
- "receiver_name": "Leonard Hofstadter",
- "receiver_account": "DE987654321",
- "amount": "15678.34",
- "type": "wire_transfer",
- "direction": "I",
- "timestamp": "2022-01-20T08:48:39Z"
}
}
Updates transaction. Currently status is the only attribute that can be updated.
personId required | string non-empty Example: user-00000009 ID of person |
transactionId required | string non-empty Example: transaction-000000000009 ID of transaction |
id required | string non-empty External unique transaction ID |
object Transaction attributes key value pairs. Adding completely new keys or updating |
{- "id": "transaction-000000000009",
- "attributes": {
- "status": "COMPLETED"
}
}
{- "id": "transaction-000000000009",
- "attributes": {
- "sender_name": "John Doe",
- "sender_account": "EE106637001834923964",
- "receiver_name": "Jane Doe",
- "receiver_account": "EE926693048477018590",
- "amount": "100.99",
- "type": "Bank Transfer",
- "direction": "O",
- "status": "Completed",
- "reference": "payment",
- "timestamp": "2014-11-23T03:04:50.000000Z"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "personId": "string"
}
personId required | string non-empty ID of person |
{- "statuses": [
- {
- "id": 0,
- "assigner": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "status": "NEW",
- "startTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "createdTime": "2019-08-24T14:15:22Z"
}
], - "snoozeHistory": [
- {
- "id": 0,
- "personId": 0,
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}, - "eventType": "CREATE"
}
], - "latestSanctionCheck": {
- "sanctionAlertId": 0,
- "createdTime": "2019-08-24T14:15:22Z"
}
}
You can set up webhook configuration on the webhooks page.
Please note that you must have an administrator role.
If sending the webhook fails, we'll retry sending it until it is accepted. Retry attempts happen regularly for up to 7 days, at increasing time intervals:
After that, retries happen every 8 hours for the following 7 days. For tackling transient errors (like a momentary network glitch) we retry every request 3 times with 1 second interval.
The timeout limit for webhooks is 1 second for connect and 9 seconds for read timeout.
Salv supports sending out certain webhooks based on the following event triggers:
ALERT
-> ALERT_CREATED
ALERT_STATUS
-> ALERT_STATUS_UPDATED
SCREENING_ALERT
-> SCREENING_ALERT_CREATED
SCREENING_ALERT_STATUS
-> SCREENING_ALERT_STATUS_UPDATED
FINAL_RISK
-> FINAL_RISK_UPDATED
PERSON_STATUS
-> PERSON_STATUS_UPDATED
PERSON
-> The name of the succeeding decision rule. Example: PERSON_CLEARED
TRANSACTION
-> The name of the succeeding decision rule. Example: TRANSACTION_CLEARED
CUSTOM_LIST_RECORD
-> CUSTOM_LIST_RECORD_CREATED
, CUSTOM_LIST_RECORD_UPDATED
, CUSTOM_LIST_RECORD_ARCHIVED
When configuring decision webhook rules, every time an alert related to a person or a transaction is changed, this set of rules will be run on all the related alerts for that person/transaction.
If any of the rules succeeds, it will send out a webhook with the name of that rule in action parameter. For more information, check Salv user manual.
action | string |
createdTime | string <date-time> Event created time |
required | object (AlertCreatedEventMeta) |
{- "action": "ALERT_CREATED",
- "createdTime": "2019-08-24T14:15:22Z",
- "metadata": {
- "alertId": 1001,
- "personId": "user-0000007",
- "transactionId": "transaction-0000005",
- "entityType": "TRANSACTION",
- "scenarioType": "OFFLINE",
- "hits": [
- {
- "reason": "High incoming sum",
- "details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
- "scenarioHandle": "1fea3186-ff7a-4602-866c-1f39e5f63fbf"
}
]
}
}
Endpoints to add, update, delete and get custom list records.
Custom Lists can be used in two ways:
customListId required | string <uuid> ID of the custom list |
page required | integer >= 0 Default: 0 Number of the page |
size required | integer [ 1 .. 50 ] Default: 20 Size of the page |
{- "number": "0,",
- "size": "20,",
- "total": 1223,
- "content": [
- {
- "id": "sdf12345",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "foo": "bar",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "gender": "M",
- "bank_account_numbers": [
- "AR45500231529936926440",
- "CY91660740448833368"
]
}
}
]
}
customListId required | string <uuid> ID of the custom list |
id required | string non-empty External unique record ID |
required | object Key value pair where key is |
{- "id": "sdf12345",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "foo": "bar",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "gender": "M",
- "bank_account_numbers": [
- "AR45500231529936926440",
- "CY91660740448833368"
]
}
}
{- "id": "record-00000009",
- "attributes": {
- "property1": { },
- "property2": { }
}
}
customListId required | string <uuid> ID of the custom list |
id required | string non-empty ID of the record to be fetched |
{- "id": "record-00000009",
- "attributes": {
- "property1": { },
- "property2": { }
}
}
customListId required | string <uuid> ID of the custom list |
id required | string non-empty ID of the record to be updated |
id required | string non-empty External unique record ID |
required | object Key value pair where key is |
{- "id": "sdf12345",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "foo": "bar",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "gender": "M",
- "bank_account_numbers": [
- "AR45500231529936926440",
- "CY91660740448833368"
]
}
}
{- "id": "record-00000009",
- "attributes": {
- "property1": { },
- "property2": { }
}
}
Monitoring allows clients to run various checks (called scenarios) against their persons and transactions. There are three scenario types:
All those scenarios are configured in Salv UI. For each pair of triggered scenario and person/transaction one monitoring alert is created. Those alerts are stored by Salv and can later be accessed in UI or by API.
Transaction monitoring runs all real-time (ONLINE) monitoring scenarios and returns response in realtime. This flow can be used to block a transaction based on transaction monitoring checks.
Add a person and a transaction as shown in Example: Uploading person and transaction.
Perform transaction monitoring according to the documentation at runTransactionMonitoringChecks(POST).
curl --request POST \
--url https://app.salv.com/api/v1/transactions/transactionId/monitoring-checks \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json'
import requests
url = "https://app.salv.com/api/v1/transactions/transactionId/monitoring-checks"
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, headers=headers)
print(response.text)
In this case you should block the transaction using your internal logic.
{
"results": [
{
"reason": "High incoming sum",
"details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
"score": 100,
"weight": 1,
"scenarioType": "ONLINE",
"alertId": 321
},
{
"reason": "Outgoing transfer is 80% of the total balance",
"details": "Amount: 7785.0, balance: 9693.43",
"score": 100,
"weight": 1,
"scenarioType": "ONLINE",
"alertId": 543
}
]
}
In this case you should process the transaction as no alert was created.
{
"results": []
}
Transaction monitoring runs all real-time (ONLINE) monitoring scenarios and returns response in realtime. This flow can be used to block a transaction based on transaction monitoring checks.
Add a person and a transaction as shown in Example: Uploading person and transaction.
Perform transaction monitoring according to the documentation at runTransactionMonitoringChecks(POST).
curl --request POST \
--url https://app.salv.com/api/v1/transactions/transactionId/monitoring-checks \
--header 'Authorization: Bearer {yourBearerToken}' \
--header 'Content-Type: application/json'
import requests
url = "https://app.salv.com/api/v1/transactions/transactionId/monitoring-checks"
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, headers=headers)
print(response.text)
In this case you should block the transaction using your internal logic.
{
"results": [
{
"reason": "High incoming sum",
"details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
"score": 100,
"weight": 1,
"scenarioType": "ONLINE",
"alertId": 321
},
{
"reason": "Outgoing transfer is 80% of the total balance",
"details": "Amount: 7785.0, balance: 9693.43",
"score": 100,
"weight": 1,
"scenarioType": "ONLINE",
"alertId": 543
}
]
}
In this case you should process the transaction as no alert was created.
{
"results": []
}
Person & Transaction monitoring.
Real-time (ONLINE) scenarios should be used when an alert created by the scenario should block the transaction. Example: Real-time (pre-processing) transaction monitoring
Post-event (OFFLINE) scenarios should be used when an alert created should NOT block the transaction. Example: Simple integration
In the rare occasion that a monitoring check fails with error code 500, the transaction should be blocked. In that case Salv has not been able to verify that the transaction is safe to process.
personId required | string non-empty ID of a person |
all-results | boolean Return all results regardless of scenario score |
{- "results": [
- {
- "reason": "High incoming sum",
- "details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
- "score": 100,
- "weight": 1,
- "scenarioType": "ONLINE",
- "alertId": 321
}
]
}
transactionId required | string non-empty ID of transaction |
{- "results": [
- {
- "reason": "High incoming sum",
- "details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
- "score": 100,
- "weight": 1,
- "scenarioType": "ONLINE",
- "alertId": 321
}
]
}
alertId required | integer <int64> >= 1 ID of alert to get |
{- "id": 1,
- "priority": "LOW",
- "status": "string",
- "statusId": 0,
- "person": {
- "id": "user-00000009",
- "type": "INDIVIDUAL",
- "attributes": {
- "first_name": "Sheldon",
- "last_name": "Cooper",
- "dob": "1983-05-12",
- "city": "Pasadena",
- "street_address": "2311 North Los Robles Avenue",
- "country": "US",
- "onboarding_date": "2019-04-16",
- "phone_number": "+1582693582",
- "id_document": "passport",
- "id_country": "US",
- "gender": "F"
}, - "status": "NEW",
- "createdTime": "2019-08-24T14:15:22Z",
- "snoozeConfiguration": {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}, - "scenarioSnoozeConfigurations": [
- {
- "id": 1,
- "personId": 1,
- "scenarioHandle": "2b6cfdc0-f204-4ff5-84cc-164ddf495815",
- "scenarioReason": "string",
- "createdBy": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z",
- "amount": 0.1,
- "totalAmount": 0.1,
- "note": {
- "id": 0,
- "content": "string"
}
}
], - "category": "COUNTERPARTY"
}, - "score": {
- "results": [
- {
- "reason": "High incoming sum",
- "details": "Sum >= 5 000€ (Sum: 19 694.05) in last 30 days",
- "score": 100,
- "weight": 1,
- "scenarioType": "ONLINE",
- "alertId": 321
}
]
}, - "assignee": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "assignments": [
- {
- "id": 0,
- "assignee": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "assigner": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "startTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z"
}
], - "statuses": [
- {
- "id": 0,
- "assigner": {
- "id": 0,
- "email": "string",
- "name": "string"
}, - "status": "string",
- "statusId": 0,
- "createdTime": "2019-08-24T14:15:22Z",
- "startTime": "2019-08-24T14:15:22Z",
- "endTime": "2019-08-24T14:15:22Z"
}
], - "notes": [
- {
- "id": 1,
- "content": "string",
- "user": {
- "id": 1,
- "email": "string",
- "name": "string",
- "inactive": true,
- "locked": true,
- "password": "pa$$word",
- "intercomUserHash": "string",
- "intercomUserEmail": "string",
- "roles": [
- {
- "id": 1,
- "name": "string"
}
], - "organisation": {
- "id": 1,
- "name": "string",
- "inactive": true,
- "currency": "EUR"
}
}, - "createdTime": "2019-08-24T14:15:22Z",
- "alertId": 1,
- "sanctionAlertId": 1,
- "bridgeAlertId": 1,
- "statusChangeId": 1
}
], - "transaction": {
- "id": "transaction-000000000009",
- "attributes": {
- "sender_name": "John Doe",
- "sender_account": "EE106637001834923964",
- "receiver_name": "Jane Doe",
- "receiver_account": "EE926693048477018590",
- "amount": "100.99",
- "type": "Bank Transfer",
- "direction": "O",
- "status": "Completed",
- "reference": "payment",
- "timestamp": "2014-11-23T03:04:50.000000Z"
}, - "createdTime": "2019-08-24T14:15:22Z",
- "personId": "string"
}, - "entityType": "PERSON"
}
Screening allows clients to check their persons and transactions against screening lists, such as sanctions lists, PEPs (politically exposed persons) lists and adverse media lists. It is also possible to configure custom lists to check persons and transactions against them as well.
There are two ways to use screening.
To quickly check some name against the screening lists without uploading any data or creating alerts use screening search endpoint.
To configure more intricate checks first upload person and transaction data to Salv. That will enable automatic person re-screening and allow you to configure a lot of screening details. Then it will be possible to run screening with all that configuration for any particular person or transaction using the API.
Transaction screening checks transaction sender and recipient names against screening lists
Add a new transaction, according to the documentation at addTransaction(POST).
Perform transaction screening according to the documentation at checkTransaction(POST).
curl -X POST \
https://app.salv.com/api/v1/transactions/000000000000009/screening-checks \
-H 'Authorization: Bearer {yourBearerToken}' \
-H 'Content-Type: application/json'
import requests
url = "https://app.salv.com/api/v1/transactions/id/screening-checks"
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, headers=headers)
print(response.text)
In this case you should block the transaction using your internal logic.
{
"matches": [
{
"inputData": "Hassan Bahadlri",
"matchData": "{"id": "67c79e6f14245df0791a307154512e8a097d6a57", "schema": "Person", "properties": {"name": ["Hassan Bahadori"], "lastName": ["Bahadori"], "firstName": ["Hassan"]}}",
"sanctionList": "US OFAC sanctions",
"score": 99,
"checkType": "SANCTION",
"alertId": 321
"referenceGroupCodes": ["EUCON", "OFACAIS"]
},
{
"inputData": "Gazprom Neft",
"matchData": "{"id": "303a2ec82f2c5c48e051bfd7e3822baba5b9ca06", "schema": "LegalEntity", "properties": {"name": ["Gazprom Neft"]}}",
"sanctionList": "CH SECO sanctions",
"score": 100,
"checkType": "SANCTION"
"alertId": 123,
"referenceGroupCodes": null
}
]
}
In this case you should process the transaction as no alert was created.
{
"matches": []
}
Person screening checks person name against screening lists
Add a new person, according to the documentation at addPerson(POST).
If the person has already been added to Salv, then skip this step.
Perform person screening according to the documentation at checkPerson(POST).
curl -X POST \
https://app.salv.com/api/v1/persons/000007/screening-checks \
-H 'Authorization: Bearer {yourBearerToken}' \
-H 'Content-Type: application/json'
import requests
url = "https://app.salv.com/api/v1/persons/id/screening-checks"
headers = {
"Content-Type": "application/json",
"Authorization": "{yourBearerToken}"
}
response = requests.request("POST", url, headers=headers)
print(response.text)
{
"matches": [
{
"inputData": "Hassan Bahadlri",
"matchData": "{"id": "67c79e6f14245df0791a307154512e8a097d6a57", "schema": "Person", "properties": {"name": ["Hassan Bahadori"], "lastName": ["Bahadori"], "firstName": ["Hassan"]}}",
"sanctionList": "US OFAC sanctions",
"score": 99,
"checkType": "SANCTION",
"alertId": 321
"referenceGroupCodes": ["EUCON", "OFACAIS"]
},
{
"inputData": "Hassan Bahadlri",
"matchData": "{"id": "303a2ec82f2c5c48e051bfd7e3822baba5b9ca06", "schema": "Person", "properties": {"name": ["Hassan Bahadlri"], "wikidataId": ["Q85387153231"]}, "organizations": []}",
"sanctionList": "PEP every politician",
"score": 100,
"checkType": "PEP",
"alertId": 123,
"referenceGroupCodes": null
}
]
}
{
"matches": []
}
Screening checks for transaction and person. If your field names do not match the example, make sure to configure this under mappings.
Transaction is screened by one or all attributes - reference, receiver_bank_bic, sender_bank_bic, sender_bank_name, receiver_bank_name, from_name and to_name. Selection of screened attributes come from configuration.
id required | string external ID of the transaction to screen |
{- "matches": [
- {
- "alertId": 0,
- "inputData": "John Doe",
- "matchData": "{\"id\": \"67c79e6f14245df0791a307154512e8a097d6a57\", \"schema\": \"Person\", \"properties\": {\"name\": [\"John Doe\"], \"lastName\": [\"Doe\"], \"firstName\": [\"John\"]}}",
- "sanctionList": "US OFAC sanctions",
- "listCode": "BAN_LIST",
- "score": 99,
- "checkType": "SANCTION",
- "highlight": "John Smith",
- "referenceGroupCodes": [
- "EUCON",
- "OFACAIS"
]
}
]
}
Person is screened by first_name + last_name and company_name attribute.
id required | string external ID of the person to screen |
{- "matches": [
- {
- "alertId": 0,
- "inputData": "John Doe",
- "matchData": "{\"id\": \"67c79e6f14245df0791a307154512e8a097d6a57\", \"schema\": \"Person\", \"properties\": {\"name\": [\"John Doe\"], \"lastName\": [\"Doe\"], \"firstName\": [\"John\"]}}",
- "sanctionList": "US OFAC sanctions",
- "listCode": "BAN_LIST",
- "score": 99,
- "checkType": "SANCTION",
- "highlight": "John Smith",
- "referenceGroupCodes": [
- "EUCON",
- "OFACAIS"
]
}
]
}
Screening search can be used to check any name against screening lists without first uploading a person or a transaction.
Checks if provided name matches a sanctioned person or entity.
name required | string >= 3 characters Screening name |
matchEntityType | string Enum: "PERSON" "ENTITY" "ALL" Sanction record type |
searchAllLists | boolean When true, screens against all sanction lists |
screeningTypes required | Array of strings (ApiDowJonesSanctionType) Items Enum: "SANCTION" "PEP" "ADVERSE_MEDIA" "ADVERSE_MEDIA_ENTITY" Screening types that need to be screened |
djListScreeningSubType | string Enum: "FUZZY_MATCH" "STARTS_WITH" "EXACT_MATCH" Screening match type |
object (CustomListScreeningSearchData) |
{- "name": "John Doe",
- "matchEntityType": "PERSON",
- "searchAllLists": false,
- "screeningTypes": [
- "SANCTION"
], - "djListScreeningSubType": "FUZZY_MATCH",
- "customListSearch": {
- "screeningLists": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "screeningFieldIds": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "score": 0,
- "prefixMatchLimit": 0,
- "screeningSubType": "FUZZY_MATCH"
}
]
}
]
}
}
[- {
- "id": "string",
- "type": "PERSON",
- "identities": [
- {
- "value": "string",
- "type": "NAME"
}
], - "countryRelations": [
- {
- "countryCode": "string",
- "relationType": "COUNTRY"
}
], - "relatedEntities": [
- {
- "id": "string",
- "type": "ROLE",
- "typeDetails": "string",
- "relation": "string",
- "identities": [
- {
- "value": "string",
- "type": "NAME"
}
], - "since": "string",
- "upTo": "string",
- "roles": [
- {
- "recordExternalId": "string",
- "type": "string",
- "title": "string",
- "sinceDay": "string",
- "sinceMonth": "string",
- "sinceYear": "string",
- "toDay": "string",
- "toMonth": "string",
- "toYear": "string",
- "occupation": "string"
}
]
}
], - "representedInLists": [
- {
- "code": "string",
- "name": "string"
}
], - "recordDate": "string",
- "descriptions": [
- "string"
], - "score": 0,
- "screeningType": "SANCTION",
- "matchData": "string",
- "sanctionList": "string",
- "identificationDetails": {
- "property1": "string",
- "property2": "string"
}, - "highlight": "string",
- "matchProvider": "DOW_JONES"
}
]
Screening alert is created when a particular field of a particular person matches against one of the screening lists. Screening alert has a list of hits, each of which represent one matched record from screening list. Some of the hits can be cleared automatically according to the configured clearance rules and goodlists.
Returns paginated list of screening alert hits by alert id
screeningAlertId required | integer <int64> >= 1 ID of the screening alert for which to return hits |
page required | integer >= 0 Default: 0 Number of the page |
size required | integer [ 1 .. 50 ] Default: 20 Size of the page |
{- "number": 0,
- "size": 1,
- "total": 1,
- "content": [
- {
- "id": "sdf12345",
- "listName": "Special Interest Person (SIP)",
- "listProvider": "DOW_JONES",
- "listType": "ADVERSE_MEDIA",
- "score": 98,
- "attributes": {
- "aliases": "Lingjia Huang~Ling-Chia Huang~Ling Chia Huang~Ling Jia Huang",
- "associations": [
- {
- "association": {
- "associateId": "4286543",
- "ex": false,
- "name": "Siou-Yue Jheng",
- "relationship": "Associated Special Interest Person",
- "type": "PublicFigure"
}, - "roles": [ ]
}
], - "descriptions": {
- "Description1": "Special Interest Person (SIP)",
- "Description2": "Financial Crime"
}, - "identificationDetails": {
- "HM Treasury Group ID": "7584",
- "SECO SSID": "1147",
- "OFAC Unique ID": "7847",
- "OFAC Program ID": "IRAQ2",
- "DFAT Reference Number": "653",
- "EU Sanctions Programme Indicator": "IRQ",
- "UN Permanent Reference No.": "IQi.005",
- "EU Consolidated Electronic List ID": "29"
}, - "mainList": "Special Interest Person (SIP)",
- "referenceGroupCodes": [
- "EUCON",
- "OFACAIS"
], - "name": "Ling-Jia Huang",
- "notes": "Keywords: tax evasion\nPeople mentioned: Huang Ling-Jia, Jheng Jing-Tai\nCompanies mentioned: Toki Choi\nProfile:\nHuang Ling-Jia, an accountant with internet fashion retailer Toki Choi, was one of 11 individuals given a suspended indictment and ordered to pay between TWD 10,000 and TWD 800,000 to the national treasury by the Taipei District Prosecutors Office on May 6, 2015, for their roles in helping Toki Choi chairman Jheng Jing-Tai to evade TWD 13.25m in company taxes.",
- "overview": "{\"id\":\"4286548\",\"properties\":{\"name\":[\"Ling-Jia Huang\"],\"aliases\":[\"Lingjia Huang\",\"Ling-Chia Huang\",\"Ling Chia Huang\",\"Ling Jia Huang\"],\"Citizenship\":[\"Taiwan\"],\"Country of Reported Allegation\":[\"Taiwan\"],\"Resident of\":[\"Taiwan\"],\"gender\":\"Female\",\"deceased\":\"No\"},\"referenceCodes\":null,\"recordDate\":\"2015-05-31T00:00:00\",\"type\":\"PERSON\",\"descriptions\":{\"Description1\":\"Special Interest Person (SIP)\",\"Description2\":\"Financial Crime\"},\"activeStatus\":\"Active\"}",
- "recordDate": 1433019600,
- "roles": [
- {
- "occupation": "International Sporting Organisation Officials",
- "sinceDay": "17",
- "sinceMonth": "Apr",
- "sinceYear": "1960",
- "title": "Alternate Member, Board of Directors, European Investment Bank (EIB)",
- "toDay": null,
- "toMonth": null,
- "toYear": null,
- "type": "Primary Occupation"
}
], - "type": "PERSON"
}
}
]
}
Risks levels are assigned to persons according to configured risk rules. It is up to client how to interpret each particular level.
Risk rules are configured in Salv UI.
Person risk is scored every time one of the following triggering events occurs:
Person risk scoring happens in the background and the service does not guarantee any time frame for it.
Risk levels can be overridden by client operators for any particular person. They would need to choose one of the preconfigured override reasons, which are also configured in Salv UI.
personId required | string non-empty ID of person |
{- "finalRisk": "LOW",
- "risks": [
- {
- "id": 0,
- "level": "LOW",
- "factor": "Outgoing amount",
- "details": "Outgoing amount is over €100 000",
- "weight": 2,
- "createdTime": "2019-08-24T14:15:22Z"
}
], - "activeOverrideLevel": "LOW",
- "activeOverrideReason": "Sanctioned"
}
Salv has multiple properties that are associated with individual alerts and influence alert management process based on their assignment. Using the initial alert ID and TYPE that are generated after different checks are triggered and communicated via API or WEBHOOKS, it is possible to programmatically modify these properties to streamline your business logic.
Attention: To start using this API, the client id used for authentication needs to be created after 16.05.2023. If your integration currently uses credentials created before this date, you must create a NEW credentials pair, and replace the existing one.
Updates specified alert status, optionally includes a note to go with the update.
alertType required | string (ApiAlertType) Enum: "MONITORING" "SCREENING" "BRIDGE" |
alertId required | integer <int64> >= 0 Salv alert ID, provided after calling screening or monitoring checks endpoint |
status required | string non-empty Alert status CODE, configured in Salv admin UI |
note | string non-empty Optional note to be added together with alert status change |
{- "alertType": "MONITORING",
- "alertId": 1234567,
- "status": "FALSE_POSITIVE",
- "note": "Documents provided and matched by customer"
}
Deprecated. Will be removed and replaced with a new more flexible endpoint.
id required | string external ID of the person to check |
{- "hasUnresolvedAlerts": true
}
Deprecated. Will be removed and replaced with a new more flexible endpoint.
id required | string external ID of the transaction to check |
{- "hasUnresolvedAlerts": true
}