Contents
Sentinel Comply API
Identity verification for PayCard group systems: create a verification session, send your customer to the hosted flow (document, selfie and liveness), and receive the decision by webhook or by polling. This reference is generated from the running server, so it lists exactly the 19 Partner API endpoints this instance serves.
Sentinel Comply is an internal PayCard system. In this API a partner is a PayCard or parent-company system that onboards customers. Each system gets its own API key from a Sentinel administrator, who creates it in the console.
Base URL
https://comply.mapaycard.com
Authentication
API key in the X-API-Key header (Partner API)
Content Type
application/json; file uploads use multipart/form-data
Quick Start
An integration takes three steps:
POST /partner/api/v1/sessions returns a kyc_url for your user.
The hosted flow collects the ID document (photo or PDF), selfie and liveness check. See Hosted flow integration.
From a webhook, or with GET /partner/api/v1/sessions/{session_id}.
Authentication
Every Partner API request carries your API key in the X-API-Key
header. A missing or unknown key is answered with 401, a deactivated one
with 403.
Each key has its own request limit per minute, set on the key in the console
(60 unless changed). Every Partner API response reports where you stand in
X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset (seconds until the count restarts); over the limit
the answer is 429 with a Retry-After header.
The hosted KYC flow does not use your key. Opening the kyc_url once sets a
session cookie in the customer's browser, and the hosted page's calls are authorised by
that cookie, so the key never reaches the applicant's browser.
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_4411",
"reference": "PC-118822",
"applicant": {
"first_name": "Mamadou Saliou",
"last_name": "Diallo",
"date_of_birth": "1990-04-12",
"gender": "M"
},
"locale": "fr",
"success_redirect": "https://app.example.com/kyc/done",
"failure_redirect": "https://app.example.com/kyc/failed",
"cancel_redirect": "https://app.example.com/kyc/cancelled",
"pending_redirect": "https://app.example.com/kyc/pending",
"metadata": {}
}'
Create one on the API keys page of the console. The key is shown once, when it is created — only a hash of it is stored — so copy it then. The webhook URL and signing secret are set on the same key.
Hosted flow integration
The customer verifies their identity on a page this server hosts: you create a session
from your server, open its kyc_url on the customer's phone, and get them
back in your app when they finish. The decision reaches your server by webhook.
Sequence
- Your appYour server
The customer taps "Verify my identity".
- Your serverSentinel
POST /partner/api/v1/sessionswithuser_id,applicantand the four*_redirectURLs. The answer carrieskyc_url: single-use, valid 30 minutes. - Your serverYour app
Hand
kyc_urlto the app. Do not open or fetch it on the way. - Your appBrowser
Open it once: a Custom Tab on Android,
SFSafariViewControllerorASWebAuthenticationSessionon iOS (a WebView only with the checklist below). - BrowserSentinel
Consent, ID document, face. The first open spends the link's token and sets a session cookie in that browser.
- SentinelBrowser
At the end, or when the customer leaves, the browser is sent to one of your redirect URLs with
?session_id=…&status=…. - BrowserYour app
Your redirect URL is an Android App Link or an iOS universal link (or the
ASWebAuthenticationSessioncallback), so the app comes back to the front and shows the outcome. - SentinelYour server
Webhook
session.approved,session.rejectedorsession.completed(in review). This, orGET /partner/api/v1/sessions/{session_id}, is the decision.
The first request to kyc_url spends its token. A server-side fetch, a
link preview in a chat app, a URL shortener or a security scanner that opens it first
leaves the customer with "link already used". Pass it straight to the phone and open
it there. If it was spent or has expired, ask for a new one with
POST /partner/api/v1/sessions/{session_id}/refresh-token, or create the
session again with the same user_id: the open session is returned with a
new link and the customer resumes where they stopped.
One customer at a time per device. Opening a link ends any other
customer's verification on that browser: their page asks them to sign in again. On a
phone shared by two customers (an in-app browser keeps one cookie store), the first one
gets a new link from your app (create the session again with their
user_id) to resume or to see their outcome.
Return URLs
The browser is sent to your URL with session_id, status and, when
you gave one, reference added to its query string. No reason, code or score is
ever added. Without a URL for an outcome, the page tells the customer they can close it.
status | When | URL used | What the app should do |
|---|---|---|---|
approved | The onboarding was approved. | success_redirect | Show "verified", then confirm from your server before opening the account. |
in_review | A reviewer must decide (manual review). | pending_redirect, else success_redirect | Show "we are checking your documents"; wait for the webhook. |
not_approved | The onboarding was rejected. | failure_redirect | Show that it did not work and how to get help. |
cancelled | The customer left without finishing: they cancelled, declined consent, chose "Retour à PayCard" on an error page or "Revenir plus tard". The session may still be open. | cancel_redirect | Offer to continue later. To resume, create the session again with the same user_id: the open session comes back with a new link. |
status is for what the app shows only: anyone can type that URL. Grant nothing on it;
read the decision from the webhook or GET /partner/api/v1/sessions/{session_id}.
Opening the link on Android
Use a Custom Tab (androidx.browser): the camera, the photo picker and the
cookie work with no extra code, and the customer sees the real address. Make your redirect
URLs verified App Links
(an https intent filter with android:autoVerify="true" and an
assetlinks.json on your domain), so the redirect returns to the app.
CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
.launchUrl(context, Uri.parse(kycUrl))
Opening the link on iOS
Use ASWebAuthenticationSession with your redirect's callback (a custom scheme,
or an https callback on iOS 17.4 and later): the session closes by itself when
the browser reaches your redirect URL. SFSafariViewController also works, with
the redirect URLs as universal links. Both have the camera. A WKWebView needs
NSCameraUsageDescription, allowsInlineMediaPlayback = true,
mediaTypesRequiringUserActionForPlayback = [], and on iOS 15 and later a
WKUIDelegate that grants requestMediaCapturePermissionFor for this
server's origin.
If you must use an Android WebView
A WebView has none of this by default. Without every item below the camera stays black, the upload button does nothing, or the redirect opens inside the WebView.
- Declare
android.permission.CAMERAin the manifest and ask for it at run time before opening the page. setJavaScriptEnabled(true)andsetDomStorageEnabled(true).setMediaPlaybackRequiresUserGesture(false), or the camera preview does not start.- A
WebChromeClientwhoseonPermissionRequestgrantsRESOURCE_VIDEO_CAPTUREto this server's origin only. onShowFileChooser, for "choose a photo or PDF".shouldOverrideUrlLoading, to catch your redirect URLs and close the WebView.- Cookies on (
CookieManager.setAcceptCookie(true)): the session continues on a cookie after the first open.
val kycOrigin = "https://comply.mapaycard.com"
webView.settings.apply {
setJavaScriptEnabled(true)
setDomStorageEnabled(true)
setMediaPlaybackRequiresUserGesture(false)
}
CookieManager.getInstance().setAcceptCookie(true)
webView.webChromeClient = object : WebChromeClient() {
// The app must already hold android.permission.CAMERA.
override fun onPermissionRequest(request: PermissionRequest) {
val video = PermissionRequest.RESOURCE_VIDEO_CAPTURE
if (request.origin.toString().startsWith(kycOrigin) && video in request.resources) {
request.grant(arrayOf(video))
} else {
request.deny()
}
}
override fun onShowFileChooser(
view: WebView, callback: ValueCallback<Array<Uri>>, params: FileChooserParams,
): Boolean {
filePathCallback = callback // answered from the launcher's result
fileChooser.launch(params.createIntent())
return true
}
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
if (url.toString().startsWith("https://app.example.com/kyc/")) { // your *_redirect URLs
onKycReturn(url.getQueryParameter("session_id"), url.getQueryParameter("status"))
return true
}
return false
}
}
webView.loadUrl(kycUrl)
Errors
Errors use the HTTP status codes below and a JSON body with a detail key, in
one of three shapes. Branch on detail.error when it is there, never on
message: messages are English text for your logs and may change.
// 1. A coded error: detail is an object with error and message (and sometimes more keys)
{
"detail": {
"error": "SESSION_IN_REVIEW",
"message": "This customer has a verification in review. Wait for the decision.",
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
// 2. A plain error: detail is a string (401, 403, 404, 429, some 400)
{
"detail": "Session not found"
}
// 3. Validation (422): detail lists every failing field. On POST /sessions,
// error and message are added next to it for applicant and user_id.
{
"detail": [
{
"type": "value_error",
"loc": ["body", "applicant", "gender"],
"msg": "Value error, must be M or F",
"input": "X"
}
],
"error": "APPLICANT_INVALID",
"message": "applicant.gender: must be M or F"
}
Error codes
| HTTP | detail.error | When | What the app should do |
|---|---|---|---|
401 |
(string) | Any call: the API key is missing ("API key required") or unknown ("Invalid API key"). | Fix the key in your configuration. Do not retry. |
403 |
(string) | Any call: the API key is deactivated ("API key is disabled"). | Ask a Sentinel administrator. Do not retry. |
429 |
(string) | Any call: this key's requests per minute are used up. | Wait for the Retry-After seconds, then send the same request again. |
422 |
APPLICANT_REQUIRED |
Create: applicant (first_name, last_name, date_of_birth, gender) or user_id is missing: a payload from before onboarding v2. | Send the declared identity and your user_id. A code bug: do not retry as is. |
422 |
APPLICANT_INVALID |
Create: a value of applicant or user_id is present but not valid. message names the first one, e.g. "applicant.gender: must be M or F". | Correct that field (ask the customer again if they typed it). Do not retry as is. |
422 |
INVALID_REDIRECT_URL |
Create: a *_redirect is not an absolute https URL (http only outside production) or is too long. field names it. | Fix the URL in your configuration. |
422 |
APPLICANT_TOO_YOUNG |
Create: the declared age is below the youngest age that can be onboarded, even with a guardian. | Tell the customer they cannot open an account yet. Do not retry. |
422 |
MINORS_NOT_ACCEPTED |
Create: the declared age is below the minimum age and minors are not accepted. | Tell the customer they cannot open an account yet. Do not retry. |
422 |
GUARDIAN_REQUIRED |
Create: a minor, without guardian_user_id. | Ask for the parent's or guardian's PayCard account; send its user_id with guardian_consent true. |
422 |
GUARDIAN_IS_APPLICANT |
Create: guardian_user_id is the minor's own user_id. | Send the guardian's user_id. |
422 |
GUARDIAN_CONSENT_REQUIRED |
Create: a minor, and guardian_consent is not true. | Collect the guardian's consent in your app, then send guardian_consent true. |
409 |
GUARDIAN_NOT_VERIFIED |
Create: the guardian has no approved, adult verification under your key. | Have the guardian verify their own identity first, then create the minor's session again. |
409 |
SESSION_IN_REVIEW |
Create: this user_id has a session in manual review. session_id names it. | Show "verification in review" and wait for the webhook. Do not create again. |
503 |
SYSTEM_NOT_READY |
Create: the verification models are still loading, or an administrator has not opened onboarding yet. | Tell the customer to try again in a few minutes; retry later. |
503 |
PRIVACY_NOTICE_NOT_PUBLISHED |
Create: no privacy notice is published, so no one can consent. | Ask a Sentinel administrator to publish one; retry later. |
404 |
(string) | Session calls: no such session under your key ("Session not found"). | Check the session_id. Do not retry. |
400 |
(string) | List: the status filter is not a session status (message lists the valid ones). | Use one of the listed values. |
409 |
SESSION_CLOSED |
Refresh-token: the session is already submitted or decided. | Read the session (or wait for the webhook); no new link is needed. |
409 |
SESSION_EXPIRED |
Refresh-token: the session has expired. | Create a session again for the same user_id. |
409 |
RESET_NOT_ALLOWED |
Reset: the session is approved or in manual review. | Nothing to reset: read the decision. |
409 |
SESSION_NOT_SUBMITTED |
Screening: the session has no onboarding decision yet (or was cancelled or expired). | Request screening after the session is approved, rejected or in review. |
409 |
SCREENING_DISABLED |
Screening: screening is turned off on this server. | Ask a Sentinel administrator. |
409 |
SCREENING_NOT_CONFIGURED |
Screening: the OpenSanctions key is not set on this server. | Ask a Sentinel administrator. |
503 |
SCREENING_DISABLED |
Name screening: screening is turned off on this server (for an organization: every category but PEP). | Ask a Sentinel administrator. |
503 |
SCREENING_NOT_CONFIGURED |
Name screening: the OpenSanctions key is not set on this server. | Ask a Sentinel administrator. |
503 |
SCREENING_UNAVAILABLE |
Name screening: the matching service did not answer. Nothing was billed. | Retry; the name was not screened. |
503 |
QUEUE_UNAVAILABLE |
Screening: the task queue could not take the request. | Retry in a minute. |
409 |
SESSION_EXISTS |
Declared or imported dossier: this user_id has a submitted verification (approved, rejected or in review), or, for a declared dossier, an imported one. session_id, status and source name it. | Screen that session instead. Do not retry. |
404 |
USER_NOT_FOUND |
Screening by user_id: no session at all for this user_id under your key. | Check the user_id. |
409 |
NO_APPROVED_SESSION |
Screening by user_id: no approved session (latest names the most recent one and its status). Create with purpose renewal: no approved session or imported dossier for this user_id. | Screening: wait for the approval, or send allow_unapproved true. Renewal: the customer was never approved here; create an onboarding (no purpose) instead. |
409 |
SESSION_NOT_DECIDED |
Identity: the session is not approved or rejected yet (or is a declared dossier). status names it. | Read the identity after the session.approved or session.rejected webhook. |
404 |
HIT_NOT_FOUND |
Adjudication: the entity is not a screening hit of this session (for that subject and category). | Read the session's screening results and send an entity_id they list. |
409 |
AMBIGUOUS_HIT |
Adjudication: the entity is a hit in several places and subject or category is missing. | Send subject and category. |
422 |
REASON_REQUIRED |
Adjudication: the reason is under 10 characters. | Ask the officer for a real reason. Do not retry as is. |
404 |
(string) | Batch progress: no such batch under your key ("Batch not found"). | Check the batch_id from the batch's 202 answer. |
404 |
(string) | Webhook retry: no such delivery under your key ("Webhook not found"). | Check the webhook id from the webhook list. |
400 |
(string) | Webhook retry: the delivery already succeeded ("Webhook already delivered"). | Nothing to do. |
500 | (string) | An unexpected server error. | Retry later; report it if it persists. |
A 429 or 503 answered while the server restarts has the body
{"code": "…", "message": "…", "detail": "…"}, with a plain detail
string: retry after a short wait.
Webhooks
Set a webhook URL and signing secret on your API key in the console. Events for every
session created with that key are then POSTed to it as JSON. In production a
key without a signing secret gets no deliveries: an unsigned event is never sent.
{
"event_id": "3f6c2b1e-8a4d-4f7e-9c21-5d8e7a1b0c42",
"event_type": "session.approved",
"created_at": "2026-09-22T10:30:00",
"sequence": 7,
"data": {
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "PC-118822",
"user_id": "u_4411",
"status": "approved",
"generation": 0,
"source": "session",
"purpose": "onboarding",
"previous_session_id": null,
"risk_score": 12,
"risk_level": "low",
"approved_at": "2026-09-22T10:30:00",
"decided_by": "system",
"reason_codes": [],
"identity_match": "match"
}
}
- All timestamps are UTC (ISO 8601, no offset):
2026-09-22T10:30:00is 10:30 UTC. sequencecounts the events of one session, starting at 1. Deliveries can arrive late or out of order: store the highestsequenceapplied per session and apply an event only if its sequence is greater.- Every
datacarriessession_id,reference,user_id,status,generation,source,purpose,previous_session_id.statusis the onboarding status when the event was sent;generationcounts the session's resets;sourceissession, or a dossier's source (see Dossiers without verification);purposeisonboarding, orrenewalwithprevious_session_idnaming the approval it renews (see Renewals). risk_scoreis an integer from 0 to 100. Screening events reportstatusbut never change it.
Events
| Event | When | data keys besides the common ones |
|---|---|---|
session.created | A session was created through the Partner API (not sent when an open session for the same user_id was returned instead). Carries the session_id, your reference and user_id, and when the link and the session expire; never the kyc_url or access token. | created_at expires_at link_expires_at guardian_session_id |
session.approved | The onboarding was approved: by the system once the post-submit duplicate checks found nothing, or by a reviewer. decided_by says which (system or reviewer). Screening never causes it. | risk_score risk_level approved_at decided_by reason_codes identity_match previous_status previous_decision reassessed_after_screening |
session.rejected | The onboarding was rejected: at submit, at document capture (an identity that does not match the declared one, or an underage applicant), or by a reviewer. reason_codes lists the onboarding reasons; screening never causes it. | decision_reason reason_codes rejected_at decided_by identity_match |
session.completed | The applicant finished and the case needs a human decision (status manual_review), at submit or because the post-submit duplicate checks found a possible duplicate. review_reason_codes says why. The reviewer's decision follows as session.approved or session.rejected. | decision risk_score risk_level review_reason_codes identity_match previous_status |
session.duplicate_detected | A face match with another customer was found after submission (same name and date of birth: possible second account; different identity: possible identity fraud; identities that could not be compared reliably: a reviewer checks them, not a sign of fraud). The case is never approved or declined automatically because of it: it goes to manual review with a high priority. Carries the matched user_id when it is your customer, the match type and the similarity; never biometric data. | duplicates review_priority escalated |
session.duplicate_resolved | A reviewer resolved a face match: same person with a legitimate second account (the case is approved), same face used fraudulently (the case is rejected, and the existing customer's session receives this event with role existing_customer), or different people (cleared). Never carries the reviewer's reason. | duplicate_check_id role resolution match_type related_user_id related_session_id resolved_at decided_by open_count review_priority |
session.expired | An unfinished session ended without a decision: it timed out, the applicant cancelled, or the applicant declined consent. reason says which (timeout, applicant_cancelled or consent_declined). Create a new session to try again. | previous_status reason expired_at |
session.reset | The session was reissued (by you, a reviewer or the applicant's retry): its earlier evidence is archived and the applicant starts again. generation counts the reissues. | previous_status actor_type reset_at session_expires_at |
session.screening_completed | A requested sanctions, PEP and adverse-media screening finished with no open or confirmed hit. It never changes the onboarding status. | screening_status screening_outcome screening_flags categories |
session.screening_flagged | A requested screening found at least one hit to adjudicate (or a hit is already confirmed); see screening_flags and categories. It never changes the onboarding status. | screening_status screening_outcome screening_flags categories open_hit_count |
session.screening_failed | A requested screening could not be completed (the matching service was unreachable or refused the request). Nothing about the onboarding changes; request the screening again later. | screening_status error |
session.screening_match_confirmed | A reviewer confirmed a screening hit: the person is the listed entity. Carries the subject, category and entity_id, and the category's new state; never the reviewer's reason. | subject category entity_id category_state screening_status screening_outcome screening_flags categories adjudicated_at decided_by match_kind adverse_media_type adjudication_id officer_source |
session.screening_match_cleared | A reviewer cleared a screening hit as a false positive. Carries the subject, category and entity_id, and the category's new state; never the reviewer's reason. | subject category entity_id category_state screening_status screening_outcome screening_flags categories adjudicated_at decided_by match_kind adverse_media_type adjudication_id officer_source |
Headers
| Header | Value |
|---|---|
X-Webhook-ID | Unique per delivery; use it to ignore duplicates. |
X-Event-Type | The event name, also in the body. |
X-Webhook-Timestamp | Unix time the delivery was signed. |
X-Webhook-Signature | sha256=<hex>, present when the key has a signing secret (always in production). |
User-Agent | Sentinel-Webhook/1.0 |
Verifying a delivery
The signature is an HMAC-SHA256 of <timestamp>.<raw body>, keyed with
your signing secret. Compute it over the raw request body, compare in constant time,
and reject timestamps outside a short window (five minutes is typical) to stop replays.
import hashlib, hmac, time
def verify(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> bool:
timestamp = headers["X-Webhook-Timestamp"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
received = headers.get("X-Webhook-Signature", "").removeprefix("sha256=")
return hmac.compare_digest(expected, received)
Delivery and retries
Deliveries are sent in the background, so the request that triggered an event never waits on your endpoint. Answer with any 2xx status; anything else, or no answer within 15 seconds, counts as a failure.
Each delivery is attempted up to 5 times: once straight
away, then again after
1 min, 5 min, 15 min and 1 h.
The same event can arrive more than once, so use X-Webhook-ID
to ignore repeats. Deliveries can be listed and retried by hand with the webhook
endpoints below.
Face duplicates
After a session is submitted, its selfie is compared with the faces of every other
customer. A match with another user_id is never approved or declined
automatically: the case goes to manual review with a high priority, you receive
session.duplicate_detected, and a reviewer compares both people and
resolves the match (session.duplicate_resolved). A match with the same
user_id is a new attempt by the same customer and is not reported.
GET /partner/api/v1/sessions/{session_id} carries the result in
duplicates. It is null until the session is submitted.
{
"checked": true,
"review_priority": "high",
"open_count": 1,
"items": [
{
"duplicate_check_id": 41,
"match_type": "FACE_MATCHES_OTHER_IDENTITY",
"same_partner": true,
"matched_user_id": "u_9",
"matched_session_id": "5b1e2c4a-7d0f-4e8b-9a61-0c3d2f1e4b57",
"matched_session_ids": ["5b1e2c4a-7d0f-4e8b-9a61-0c3d2f1e4b57"],
"similarity": 0.812,
"detected_at": "2026-10-09T10:02:11",
"resolution": null,
"resolved_at": null
}
],
"flag": null
}
| Field | Meaning |
|---|---|
checked | false while the checks that run after submission are pending; items is then empty. |
review_priority | high while a match is unresolved or the session is flagged, else normal. |
open_count | Items with no resolution yet. |
items[].match_type | DUPLICATE_SAME_PERSON_NEW_ACCOUNT: same name and date of birth, possibly a second account. FACE_MATCHES_OTHER_IDENTITY: a different identity, read reliably on both documents, possibly identity fraud. FACE_MATCHES_UNVERIFIED_IDENTITY: the two identities could not be compared reliably (a document unreadable, or a difference resting on a doubtful reading); a reviewer decides, and it is not a sign of fraud on its own. Each type is also the session's reason code. |
items[].same_partner | true when the matched customer is yours. A customer of another partner is never named: matched_user_id and matched_session_id are then null and matched_session_ids is empty. |
items[].similarity | Face similarity (raw cosine, 0 to 1 in practice). No image, embedding, name, date or document number is ever sent. |
items[].resolution | null, or same_person_legitimate (the case is approved and the two accounts are linked), same_person_fraud (the case is rejected) or different_people (the match is set aside). A resolution is final; the reviewer's reason is never sent. |
flag | Set on an existing customer's session when a reviewer confirmed that a later applicant used this customer's face fraudulently: reason_code (LINKED_TO_FRAUD_CASE), flagged_at, related_session_id, related_user_id and reviewed_at. The session's decision is not changed by the flag. |
PDF documents
In the hosted flow, the ID document (front and back) and the proof of residence can be a photo or a PDF. A PDF must have at most 5 pages, weigh at most 10 MB and have no password. The server renders one page and checks it like a photo: an ID card scanned on a full page is cropped to the card, and the back of the card can be page 2 of the same PDF. The selfie and the liveness check are photos only. Nothing changes in the Partner API: results and webhooks are the same whatever the file type.
Renewals
When the ID document of a customer you already verified expires, create a session for the
same user_id with "purpose": "renewal" and send the customer through
the hosted flow as usual (new document, selfie, liveness). The customer must have an approved
session or an imported dossier under your key, otherwise the create answers
409 NO_APPROVED_SESSION; previous_session_id names that approval in
the create response, in every webhook and in GET /sessions/{id}.
The renewal is checked against the approval, and goes to manual review (never a rejection) with one of these reason codes:
| Reason code | When |
|---|---|
RENEWAL_SAME_DOCUMENT | The document is the one approved before: the same file, or the same number and expiry date. Nothing was renewed. |
RENEWAL_IDENTITY_CHANGED | The name or date of birth on the new document differs from the approved document's. |
RENEWAL_FACE_MISMATCH | The selfie does not match the face of the customer approved before. |
RENEWAL_NO_PRIOR_FACE | The approval has no face to compare with (an imported dossier). A reviewer compares the documents. |
The duplicate checks treat the customer's own sessions as expected, so a renewal is never a
duplicate of its approval. Once the renewal is approved, screening by user_id
uses it: there is nothing to import again.
Dossiers without verification
A customer can be screened without going through the hosted flow. The identity is
kept as a dossier: a session with no document image, no selfie and no
liveness check, whose source says where the identity came from.
Every session payload and every webhook carries source:
session for the hosted verification, declared for an
identity your app sent unverified, legacy_import for a KYC you approved
before Sentinel. For a dossier, dossier is set and
every onboarding.checks value is not_performed.
Declared dossier
POST /partner/api/v1/dossiers/declared keeps the identity a customer
declared, for a customer with no submitted verification. Its status is
unverified and it is never approved. Screen it with
POST /sessions/{session_id}/screening, or by user_id with
allow_unapproved. Sending the same user_id again updates
it: when the name, date of birth, nationality or document changed,
identity_changed is true, earlier results are stale, and
the next screening request screens again. A customer with a submitted
verification gets 409 SESSION_EXISTS. A verification the customer
submits later supersedes the dossier: dossier.superseded_by_session_id
names it, and the dossier stays readable. No webhook is sent for the dossier itself.
Imported dossier
POST /partner/api/v1/dossiers/import keeps a KYC you approved before
Sentinel as an approved dossier (source legacy_import,
onboarding.decided_by partner_import), with who approved it
and when in approval. approval.approved_at is UTC (an ISO
datetime without offset is read as UTC) or null when unknown, which
dossier.approved_at_unknown then says. No image is sent and no check is
run: it was approved by your officer, not verified here, and no
session.approved webhook is sent. Document images cannot be sent with an
import in this version. Screen it like any approved session.
The document number is checked against every other customer's dossier and
verification. A match with one of your own customers is returned in
duplicates, sent as
session.duplicate_detected and shown in the session's
duplicates.items with match_type
DOCUMENT_NUMBER_MATCH. It is reported once and never changes the
approval; it does not count in open_count. A number held by another
partner's customer is shown to Sentinel's reviewers only. Importing the same
user_id again updates the dossier. A customer's declared dossier becomes
the imported one with the same session_id
(superseded_declared), and keeps its screening when the identity is the
same. A customer with a submitted verification gets 409 SESSION_EXISTS.
Neither kind of dossier can be reset, and a Sentinel officer cannot approve or reject one.
document.type takes the values listed in Document types.
Screening a name
POST /partner/api/v1/screenings/names screens a name now, without a session:
the beneficiary of a transfer, a person or an organization. The answer comes in the
response. Nothing follows it: no webhook, no adjudication, no session.
// Request
{"name": "Mamadou Diallo", "country": "SN", "entity_type": "person", "reference": "TRF-2026-104233"}
// 200
{
"query_id": "6f1c2a…",
"screening_status": "completed",
"screening_outcome": "potential_match",
"results": [
{"category": "sanctions", "state": "potential_match", "matches": [
{"entity_id": "NK-…", "name": "…", "score": 91.0, "datasets": ["us_ofac_sdn"], "topics": ["sanction"], "url": "…"}
]},
{"category": "pep", "state": "no_match", "matches": []},
{"category": "adverse_media", "state": "no_match", "matches": []}
],
"billed": true
}
- One OpenSanctions query, billed like a session's:
GET /mecounts it inbilled_queries. A call answered 503 (SCREENING_UNAVAILABLE,SCREENING_DISABLED,SCREENING_NOT_CONFIGURED) is not billed. - A person is also compared with the national list, free; a hit
there has an
entity_idstarting withgn-local:and alocal_entrywith the act'sreferenceand thenotes(null on other matches). An organization is not screened for PEP, nor against the national list. countryis ISO 3166-1 alpha-2 (alpha-3 accepted), optional;referenceis yours, up to 100 characters;entity_typedefaults toperson. Scores are 0 to 100, as on a session's results.- Sentinel keeps the
query_id, yourreference, the entity type, the country and the outcome, for billing and audit. It never stores the name.
National list
Besides the OpenSanctions sources (the UN Security Council consolidated list, OFAC, EU, UK, PEPs, adverse media), every screening compares the people screened with Sentinel's local list: people designated by a Guinean act (an arrêté), entered by Sentinel's administrators. It starts empty: no Guinean national list was found published.
- A hit on it is a sanctions hit like any other:
entity_idstarts withgn-local:,datasetsis["gn_local_list"],topicsis["sanction"], andlocal_entrygives the act'sreferenceand thenotes. Adjudicate it as any hit. - It costs nothing: only the OpenSanctions query is billed.
- Editing the list screens no one. A person added to it takes effect at each customer's
next screening: the first screening of the account, the monthly
re-screening (
POST /screenings/batchwithrescreen: true) or a screening request withrescreen: true. Until then, a customer already screened is not compared with the new entry. A person taken off the list leaves their hits until a reviewer clears them.
Document types
The same values everywhere: document.type of a declared or imported
dossier, document_type of GET /sessions/{session_id}/identity
and of document_identity in GET /sessions/{session_id}.
Compare them exactly (lower case); a value not listed here is never sent.
| Value | Document | Comes from |
|---|---|---|
passport | Passport | The hosted flow, and dossiers. |
national_id | National ID card (carte nationale d'identité) | The hosted flow, and dossiers. |
drivers_license | Driving licence (permis de conduire). A Guinean licence's expiry is printed and read on its back. | The hosted flow, and dossiers. |
voter_card | Voter card (carte d'électeur) | Dossiers only: the hosted flow does not take it. |
other | Any other identity document; document_type_label says which (for example carte étudiant). | Dossiers; on a hosted session, a document read without a known type. |
Partner API
Server-to-server calls from a PayCard group system. Create a session with the customer's user_id and declared identity, redirect the customer to its kyc_url, then read the onboarding result or wait for a webhook. Sanctions, PEP and adverse-media screening runs only when you request it, and never changes the onboarding decision.
/partner/api/v1/dossiers/declared
Keep the identity a customer declared, unverified, so they can be screened before any verification (for example when they order a card).
The dossier's status is unverified and never changes to approved: no document image, selfie or liveness check is involved, and every onboarding check reads not_performed. Screen it with POST /sessions/{session_id}/screening, or by user_id with allow_unapproved. Sending the same user_id again updates the identity; if the name, date of birth, nationality or document changed, earlier screening results become stale (screening_stale) and the next screening request screens again.
A customer with a submitted verification, or an imported dossier, is refused (409 SESSION_EXISTS): screen that one. A verification the customer submits later supersedes the declared dossier, which is kept. No webhook is sent for the dossier itself.
Requires your API key in the X-API-Key header.
Request body application/json
user_id
string
required
^[A-Za-z0-9._:@-]{1,255}$.
reference
string
applicant
object
required
applicant.first_name
string
required
applicant.last_name
string
required
applicant.date_of_birth
string (date)
required
applicant.gender
string
required
M, F.
nationality
string
document
object
required
document.type
string
required
passport, national_id, drivers_license, voter_card, other.
document.number
string
required
document.type_label
string
curl -X POST "https://comply.mapaycard.com/partner/api/v1/dossiers/declared" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_4411",
"reference": "PC-118822",
"applicant": {
"first_name": "Mamadou Saliou",
"last_name": "Diallo",
"date_of_birth": "1990-04-12",
"gender": "M"
},
"nationality": "GN",
"document": {
"type": "national_id",
"number": "GN1234567"
}
}'
Response 200
session_id
string
user_id
string
reference
string
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source
string
session, declared, legacy_import.
created
boolean
identity_changed
boolean
false.
screening_stale
boolean
false.
/partner/api/v1/dossiers/import
Import a KYC you approved before Sentinel, so the customer can be screened.
The dossier is approved (decided_by partner_import) with no document image, selfie or liveness check: every onboarding check reads not_performed, and it was approved by your officer, not verified here. No session.approved webhook is sent. Screen it like any approved session.
The document number is checked against every other customer's dossier and verification. A match is returned in duplicates and sent as session.duplicate_detected (match_type DOCUMENT_NUMBER_MATCH); the approval is not changed. A match is reported once.
Importing the same user_id again updates the imported dossier; if the name, date of birth, nationality or document changed, earlier screening results become stale. A customer's declared dossier becomes the imported one, with the same session_id, and keeps its screening when the identity is the same. A customer with a submitted verification is refused (409 SESSION_EXISTS). A verification the customer submits later supersedes the imported dossier, which is kept.
Requires your API key in the X-API-Key header.
Request body application/json
user_id
string
required
^[A-Za-z0-9._:@-]{1,255}$.
reference
string
applicant
object
required
applicant.first_name
string
required
applicant.last_name
string
required
applicant.date_of_birth
string (date)
required
applicant.gender
string
required
M, F.
nationality
string
document
object
required
document.type
string
required
passport, national_id, drivers_license, voter_card, other.
document.number
string
required
document.type_label
string
document.issue_date
string (date)
document.expiry_date
string (date)
document.issuing_country
string
approval
object
required
approval.approved_at
string (date-time)
required
approval.approved_by
string
required
approval.source
string
required
curl -X POST "https://comply.mapaycard.com/partner/api/v1/dossiers/import" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_4411",
"reference": "PC-118822",
"applicant": {
"first_name": "Mamadou Saliou",
"last_name": "Diallo",
"date_of_birth": "1990-04-12",
"gender": "M"
},
"nationality": "GN",
"document": {
"type": "national_id",
"number": "GN1234567",
"issue_date": "2021-02-11",
"expiry_date": "2031-02-11",
"issuing_country": "GN"
},
"approval": {
"approved_at": "2024-03-02T10:15:00",
"approved_by": "agent.kyc@paycard.example",
"source": "paycard_legacy"
}
}'
Response 200
session_id
string
user_id
string
reference
string
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source
string
session, declared, legacy_import.
created
boolean
identity_changed
boolean
false.
screening_stale
boolean
false.
superseded_declared
boolean
false.
duplicates
array of object
duplicates[].duplicate_check_id
integer
duplicates[].match_type
string
duplicates[].same_partner
boolean
duplicates[].matched_user_id
string
duplicates[].matched_session_id
string
duplicates[].matched_session_ids
array of string
duplicates[].similarity
number
duplicates[].detected_at
string
duplicates[].resolution
string
duplicates[].resolved_at
string
/partner/api/v1/me
Get current API key's information and usage statistics.
Requires your API key in the X-API-Key header.
curl -X GET "https://comply.mapaycard.com/partner/api/v1/me" \
-H "X-API-Key: <your-api-key>"
/partner/api/v1/screenings/batch
Screen up to 500 customers in one call, as POST /users/{user_id}/screening would for each: the session that stands for the customer is resolved (with allow_unapproved as there), and a screening is queued unless one is running or a current result still answers (reused, nothing billed; rescreen forces a new one).
Each queued run is one billed query per person, runs are spread over time, and each sends the usual screening webhook for its session. Follow the batch with GET /screenings/batch/{batch_id}.
Requires your API key in the X-API-Key header.
Request body application/json
user_ids
array of string
required
rescreen
boolean
false.
allow_unapproved
boolean
false.
curl -X POST "https://comply.mapaycard.com/partner/api/v1/screenings/batch" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"user_ids": [
"u_4411",
"u_4412"
],
"rescreen": false,
"allow_unapproved": false
}'
/partner/api/v1/screenings/batch/{batch_id}
A batch's progress: done when every customer's screening has finished.
Each item gives the session screened and its screening_status now; read the result from GET /users/{user_id}/screening or the session.
Requires your API key in the X-API-Key header.
Path parameters
batch_id
string
required
curl -X GET "https://comply.mapaycard.com/partner/api/v1/screenings/batch/<batch_id>" \
-H "X-API-Key: <your-api-key>"
Response 200
batch_id
string
status
string
queued, running, done.
created_at
string (date-time)
rescreen
boolean
allow_unapproved
boolean
total
integer
queued
integer
running
integer
done
integer
reused
integer
skipped
integer
failed
integer
billed_queries
integer
items
array of object
items[].user_id
string
items[].session_id
string
items[].state
string
queued, running, done, reused, skipped, failed.
items[].error
string
items[].task_id
string
items[].billed_queries
integer
0.
items[].screening_status
string
/partner/api/v1/screenings/names
Screen a name now, without a session: the beneficiary of a transfer.
One OpenSanctions query, billed like a session's (GET /me counts it), answered in the response; a person is also compared with the Guinea local list (entity_id gn-local:...), free. An organization is not screened for PEP. Nothing follows: no webhook, no adjudication, no session. Sentinel keeps the query_id, your reference, the entity type, the country and the outcome, never the name.
Requires your API key in the X-API-Key header.
Request body application/json
name
string
required
country
string
entity_type
string
person, organization.
Default person.
reference
string
curl -X POST "https://comply.mapaycard.com/partner/api/v1/screenings/names" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Mamadou Diallo",
"country": "SN",
"entity_type": "person",
"reference": "TRF-2026-104233"
}'
Response 200
query_id
string
screening_status
string
screening_outcome
string
clear, potential_match.
results
array of object
results[].category
string
sanctions, pep, adverse_media.
results[].state
string
no_match, potential_match.
results[].matches
array of object
results[].matches[].entity_id
string
results[].matches[].name
string
results[].matches[].score
number
0.
results[].matches[].datasets
array of string
results[].matches[].topics
array of string
results[].matches[].url
string
results[].matches[].local_entry
object
billed
boolean
/partner/api/v1/sessions
List all KYC sessions for this API key.
Requires your API key in the X-API-Key header.
Query parameters
status
string
reference
string
page
integer
1.
per_page
integer
20.
curl -X GET "https://comply.mapaycard.com/partner/api/v1/sessions" \
-H "X-API-Key: <your-api-key>"
Response 200
sessions
array of object
sessions[].session_id
string
sessions[].reference
string
sessions[].user_id
string
sessions[].status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
sessions[].source
string
session, declared, legacy_import.
Default session.
sessions[].dossier
object
sessions[].dossier.source
string
session, declared, legacy_import.
sessions[].dossier.superseded_by_session_id
string
sessions[].dossier.superseded_at
string
sessions[].dossier.approved_at
string
sessions[].dossier.approved_at_unknown
boolean
sessions[].dossier.approved_by
string
sessions[].dossier.approval_source
string
sessions[].purpose
string
onboarding, renewal.
Default onboarding.
sessions[].previous_session_id
string
sessions[].risk_level
string
sessions[].risk_score
integer
sessions[].decision
string
sessions[].decision_reason
string
sessions[].created_at
string (date-time)
sessions[].updated_at
string (date-time)
sessions[].completed_at
string (date-time)
sessions[].document_verified
boolean
false.
sessions[].face_matched
boolean
false.
sessions[].liveness_passed
boolean
false.
sessions[].sanctions_clear
boolean
sessions[].screening_status
string
sessions[].screening
object
sessions[].screening.screening_status
string
sessions[].screening.screening_outcome
string
clear, potential_match, confirmed_match, cleared.
sessions[].screening.screening_flags
object
sessions[].screening.categories
array of object
sessions[].screening.results
array of object
sessions[].screening.adjudications
array of object
sessions[].extracted_data
object
total
integer
page
integer
per_page
integer
total_pages
integer
/partner/api/v1/sessions
Create a KYC verification session for a customer, or get their open one.
Send the identity the customer declared (applicant) and your stable customer id (user_id). If the customer already has an open session, it is returned (reused: true) with a new link, and the declared identity is updated. A minor needs guardian_user_id, whose approved verification is linked, and guardian_consent: true.
For a new ID document of a customer you already verified, send purpose: renewal. The user_id must have an approved session or imported dossier (else 409 NO_APPROVED_SESSION); previous_session_id names it, and the renewal goes to review when its face, identity or document says it is not a renewal of that approval.
Redirect the customer to kyc_url. The link works once, within 30 minutes; the session can be resumed for 72 hours.
Requires your API key in the X-API-Key header.
Request body application/json
user_id
string
required
^[A-Za-z0-9._:@-]{1,255}$.
reference
string
applicant
object
required
applicant.first_name
string
required
applicant.last_name
string
required
applicant.date_of_birth
string (date)
required
applicant.gender
string
required
M, F.
purpose
string
onboarding, renewal.
Default onboarding.
guardian_user_id
string
^[A-Za-z0-9._:@-]{1,255}$.
guardian_consent
boolean
locale
string
fr, en.
success_redirect
string
failure_redirect
string
cancel_redirect
string
pending_redirect
string
metadata
object
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_4411",
"reference": "PC-118822",
"applicant": {
"first_name": "Mamadou Saliou",
"last_name": "Diallo",
"date_of_birth": "1990-04-12",
"gender": "M"
},
"locale": "fr",
"success_redirect": "https://app.example.com/kyc/done",
"failure_redirect": "https://app.example.com/kyc/failed",
"cancel_redirect": "https://app.example.com/kyc/cancelled",
"pending_redirect": "https://app.example.com/kyc/pending",
"metadata": {}
}'
Response 200
session_id
string
kyc_url
string
access_token
string
expires_at
string (date-time)
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
reused
boolean
false.
link_expires_at
string (date-time)
session_expires_at
string (date-time)
guardian_session_id
string
purpose
string
onboarding, renewal.
Default onboarding.
previous_session_id
string
/partner/api/v1/sessions/{session_id}
Get a session: its onboarding decision and checks, how the declared identity compares with the document, what the document says, its screening (a separate axis that never changes the onboarding), and the face matches with other customers found after submission.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
curl -X GET "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>" \
-H "X-API-Key: <your-api-key>"
Response 200
session_id
string
reference
string
user_id
string
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source
string
session, declared, legacy_import.
Default session.
dossier
object
dossier.source
string
session, declared, legacy_import.
dossier.superseded_by_session_id
string
dossier.superseded_at
string
dossier.approved_at
string
dossier.approved_at_unknown
boolean
dossier.approved_by
string
dossier.approval_source
string
purpose
string
onboarding, renewal.
Default onboarding.
previous_session_id
string
risk_level
string
risk_score
integer
decision
string
decision_reason
string
created_at
string (date-time)
updated_at
string (date-time)
completed_at
string (date-time)
document_verified
boolean
false.
face_matched
boolean
false.
liveness_passed
boolean
false.
sanctions_clear
boolean
screening_status
string
screening
object
screening.screening_status
string
screening.screening_outcome
string
clear, potential_match, confirmed_match, cleared.
screening.screening_flags
object
screening.categories
array of object
screening.categories[].subject
string
screening.categories[].category
string
screening.categories[].state
string
screening.categories[].stale
boolean
false.
screening.categories[].screened_at
string
screening.categories[].hit_count
integer
0.
screening.categories[].open_hit_count
integer
0.
screening.results
array of object
screening.results[].category
string
screening.results[].subject
string
screening.results[].match_found
boolean
screening.results[].highest_score
number
0.
screening.results[].matches
array of object
screening.results[].screened_at
string
screening.adjudications
array of object
screening.adjudications[].match_kind
string
pep, rca.
screening.adjudications[].adverse_media_type
string
wanted, crime, debarment, regulatory, other.
screening.adjudications[].adverse_media_types
array of string
screening.adjudications[].subject
string
screening.adjudications[].category
string
screening.adjudications[].entity_id
string
screening.adjudications[].decision
string
screening.adjudications[].decided_at
string
screening.adjudications[].decided_by
string
reviewer.
extracted_data
object
onboarding
object
onboarding.status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
onboarding.decision
string
onboarding.reason_codes
array of string
onboarding.decided_by
string
onboarding.decided_at
string (date-time)
onboarding.submitted_at
string (date-time)
onboarding.final
boolean
false.
onboarding.risk_level
string
onboarding.risk_score
integer
onboarding.generation
integer
0.
onboarding.session_expires_at
string (date-time)
onboarding.consent
object
onboarding.consent.notice_version
integer
onboarding.consent.consented_at
string (date-time)
onboarding.consent.biometric_consent
boolean
onboarding.checks
object
onboarding.checks.document
string
pass, fail, missing, not_performed.
onboarding.checks.face_match
string
pass, fail, missing, not_required, attempts_exhausted, not_performed.
onboarding.checks.liveness
string
pass, fail, missing, not_required, attempts_exhausted, not_performed.
onboarding.checks.identity_match
string
match, close, partial_mismatch, mismatch, unverified, not_provided, not_performed.
onboarding.checks.age
string
adult, minor, unknown, not_performed.
onboarding.guardian
object
onboarding.guardian.guardian_user_id
string
onboarding.guardian.guardian_session_id
string
onboarding.guardian.guardian_status
string
onboarding.guardian.guardian_consent_attested
boolean
false.
onboarding.guardian.attested_at
string (date-time)
onboarding.guardian.guardian_screening_outcome
string
clear, potential_match, confirmed_match, cleared.
identity_match
object
identity_match.result
string
identity_match.fields
object
identity_match.fields.<key>.result
string
identity_match.fields.<key>.score
number
identity_match.fields.<key>.detail
string
identity_match.mismatch_count
integer
0.
identity_match.checked_at
string
document_identity
object
document_identity.surname
string
document_identity.given_names
string
document_identity.full_name
string
document_identity.date_of_birth
string
document_identity.sex
string
document_identity.nationality
string
document_identity.document_type
string
document_identity.document_number_last4
string
document_identity.expiry_date
string
document_identity.issuing_country
string
duplicates
object
duplicates.checked
boolean
duplicates.review_priority
string
duplicates.open_count
integer
0.
duplicates.items
array of object
duplicates.items[].duplicate_check_id
integer
duplicates.items[].match_type
string
duplicates.items[].same_partner
boolean
duplicates.items[].matched_user_id
string
duplicates.items[].matched_session_id
string
duplicates.items[].matched_session_ids
array of string
duplicates.items[].similarity
number
duplicates.items[].detected_at
string
duplicates.items[].resolution
string
duplicates.items[].resolved_at
string
duplicates.flag
object
duplicates.flag.reason_code
string
duplicates.flag.flagged_at
string
duplicates.flag.related_session_id
string
duplicates.flag.related_user_id
string
duplicates.flag.reviewed_at
string
/partner/api/v1/sessions/{session_id}/identity
The identity read from the session's ID document, in full: document type and number, dates, issuing country, nationality, names, date of birth and sex.
Only for an approved or rejected session; before that (and for a declared dossier) it answers 409 SESSION_NOT_DECIDED. Every read is recorded in the audit log under your API key.
Dates are YYYY-MM-DD and countries ISO alpha-2; a value that was not read, or not recognised, is null. No image is ever returned.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
curl -X GET "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/identity" \
-H "X-API-Key: <your-api-key>"
Response 200
session_id
string
user_id
string
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source
string
session, declared, legacy_import.
document_type
string
document_type_label
string
document_number
string
issue_date
string
expiry_date
string
issuing_country
string
nationality
string
surname
string
given_names
string
full_name
string
date_of_birth
string
sex
string
M, F.
/partner/api/v1/sessions/{session_id}/refresh-token
Generate a new link for an open session.
Useful if the link expired (30 minutes) before the customer opened it. The previous link stops working; a customer already in the flow keeps going until the new link is opened.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/refresh-token" \
-H "X-API-Key: <your-api-key>"
/partner/api/v1/sessions/{session_id}/reset
Reissue a session: the customer starts again with the same session id.
Allowed for open, expired and rejected sessions. Nothing is deleted: the earlier documents, selfies and checks are archived and stay available to reviewers, and screening results are kept. A new link is returned; the old link and the customer's current page stop working. A session.reset webhook follows.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
Request body application/json
reason
string
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/reset" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"reason": "string"
}'
/partner/api/v1/sessions/{session_id}/screening
Request sanctions, PEP and adverse-media screening for a submitted session.
Screening is not run automatically: each request is one billed OpenSanctions query per person (two with include_guardian on a minor's session). The result arrives by webhook (session.screening_completed, session.screening_flagged or session.screening_failed), or read it from GET /sessions/{session_id}. It never changes the onboarding decision.
Asking again while a screening runs returns the running task; asking again once it has finished returns the stored result without a new query, unless rescreen is true or the identity screened has changed since.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
Request body application/json
rescreen
boolean
false.
include_guardian
boolean
false.
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/screening" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"rescreen": false,
"include_guardian": false
}'
/partner/api/v1/sessions/{session_id}/screening/adjudications
Record your compliance officer's decision on a screening hit: confirmed (it is this person) or cleared (a false positive), with a reason.
It is the same decision a reviewer makes in Sentinel's console: the screening state is re-derived at once (no new query is billed) and session.screening_match_confirmed or session.screening_match_cleared is sent, with decided_by partner and this adjudication_id. The onboarding decision is never changed. The latest decision on a hit is the one in force, so sending another changes it. The reason and the officer are kept encrypted and are never returned.
Requires your API key in the X-API-Key header.
Path parameters
session_id
string
required
Request body application/json
subject
string
applicant, guardian.
category
string
sanctions, pep, adverse_media.
entity_id
string
required
decision
string
required
confirmed, cleared.
reason
string
required
officer
object
required
officer.name
string
required
officer.email
string
required
^[^@\s]+@[^@\s]+$.
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/screening/adjudications" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"subject": "applicant",
"category": "sanctions",
"entity_id": "NK-abc123",
"decision": "cleared",
"reason": "Different date of birth and nationality",
"officer": {
"name": "Aïssatou Bah",
"email": "a.bah@mapaycard.com"
}
}'
Response 200
adjudication_id
string
session_id
string
subject
string
category
string
entity_id
string
decision
string
category_state
string
screening_status
string
screening_outcome
string
clear, potential_match, confirmed_match, cleared.
screening_flags
object
webhook_event
string
/partner/api/v1/users/{user_id}/screening
The screening of a customer's latest approved session (same resolution as the POST).
Requires your API key in the X-API-Key header.
Path parameters
user_id
string
required
^[A-Za-z0-9._:@-]{1,255}$.
Query parameters
allow_unapproved
boolean
false.
curl -X GET "https://comply.mapaycard.com/partner/api/v1/users/<user_id>/screening" \
-H "X-API-Key: <your-api-key>"
Response 200
user_id
string
session_id
string
screening
object
screening.screening_status
string
screening.screening_outcome
string
clear, potential_match, confirmed_match, cleared.
screening.screening_flags
object
screening.categories
array of object
screening.categories[].subject
string
screening.categories[].category
string
screening.categories[].state
string
screening.categories[].stale
boolean
false.
screening.categories[].screened_at
string
screening.categories[].hit_count
integer
0.
screening.categories[].open_hit_count
integer
0.
screening.results
array of object
screening.results[].category
string
screening.results[].subject
string
screening.results[].match_found
boolean
screening.results[].highest_score
number
0.
screening.results[].matches
array of object
screening.results[].screened_at
string
screening.adjudications
array of object
screening.adjudications[].match_kind
string
pep, rca.
screening.adjudications[].adverse_media_type
string
wanted, crime, debarment, regulatory, other.
screening.adjudications[].adverse_media_types
array of string
screening.adjudications[].subject
string
screening.adjudications[].category
string
screening.adjudications[].entity_id
string
screening.adjudications[].decision
string
screening.adjudications[].decided_at
string
screening.adjudications[].decided_by
string
reviewer.
/partner/api/v1/users/{user_id}/screening
Request screening for a customer, by your user_id.
Screens the customer's latest approved session (with allow_unapproved, the latest rejected or in-review one when none is approved). Answers 409 NO_APPROVED_SESSION, with the latest session, when there is none. Otherwise it behaves as POST /sessions/{session_id}/screening.
Requires your API key in the X-API-Key header.
Path parameters
user_id
string
required
^[A-Za-z0-9._:@-]{1,255}$.
Request body application/json
rescreen
boolean
false.
include_guardian
boolean
false.
allow_unapproved
boolean
false.
curl -X POST "https://comply.mapaycard.com/partner/api/v1/users/<user_id>/screening" \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"rescreen": false,
"include_guardian": false,
"allow_unapproved": false
}'
/partner/api/v1/validate
Validate the API key and return info.
Useful for testing API key configuration.
Requires your API key in the X-API-Key header.
curl -X GET "https://comply.mapaycard.com/partner/api/v1/validate" \
-H "X-API-Key: <your-api-key>"
Response 200
valid
boolean
partner_id
string
partner_name
string
is_test_mode
boolean
false.
/partner/api/v1/webhooks
List webhook delivery attempts.
Requires your API key in the X-API-Key header.
Query parameters
status
string
page
integer
1.
per_page
integer
20.
curl -X GET "https://comply.mapaycard.com/partner/api/v1/webhooks" \
-H "X-API-Key: <your-api-key>"
Response 200
webhooks
array of object
webhooks[].id
string
webhooks[].event_type
string
webhooks[].status
string
webhooks[].attempts
integer
webhooks[].created_at
string (date-time)
webhooks[].delivered_at
string (date-time)
webhooks[].last_error
string
total
integer
/partner/api/v1/webhooks/{webhook_id}/retry
Manually retry a failed webhook delivery.
Requires your API key in the X-API-Key header.
Path parameters
webhook_id
string
required
curl -X POST "https://comply.mapaycard.com/partner/api/v1/webhooks/<webhook_id>/retry" \
-H "X-API-Key: <your-api-key>"
Internal: used by the hosted page (do not call)
Hosted KYC flow
Called by the hosted verification page in the applicant's browser. You do not call these yourself. Opening the kyc_url (once, within 30 minutes of creating or refreshing the session) sets a session cookie, and these calls are authorised by that cookie; the token never travels again. Once a session is submitted or decided, the uploads answer 409: an applicant cannot replace evidence under a decision. Submitting is idempotent: a submit sent again answers 200 with the submission's latest task. The ID document and the proof of residence can be photos (JPG, PNG) or a PDF of at most 5 pages and 10 MB without a password; the server renders the page and checks it like a photo. The selfie and the liveness frames are photos only. Errors carry a machine-readable code that the page translates: an unsupported file type, a document type not allowed for the ID's country or a back side sent without its front is a 400.
/api/v1/kyc/{session_id}/consent
Record the applicant's consent to the notice they were shown.
Once per session (it survives a reissue); a repeat answers the recorded consent. A session that consented while liveness and face match were off gives its biometric consent here once one is on.
Path parameters
session_id
string
required
Request body application/json
notice_id
string
required
biometric_consent
boolean
false.
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/consent" \
-H "Content-Type: application/json" \
-d '{
"notice_id": "string",
"biometric_consent": false
}'
Response 200
consented
boolean
true.
notice_version
integer
consented_at
string (date-time)
next_step
string
document.
/api/v1/kyc/{session_id}/document
Upload and process ID document (front or back side) - single file at a time.
The answer says only whether the document is complete and what comes next: the extracted values and check results stay on the server.
`client_quality_override=true` with `client_quality_codes` (comma-separated PHOTO_* codes) records that the applicant sent a photo the page's device check flagged; the reviewer sees it. It changes no check.
A PDF is accepted for the ID and the proof of residence: one page is rendered to a PNG in a sandboxed child process, and that PNG is the upload from then on. The original PDF is kept, encrypted, as evidence.
The attempt is counted when the upload arrives, so uploads sent at once cannot pass the cap together, and given back when the upload ends with no outcome (targeted review): the read never completed (DOC_READ_UNAVAILABLE), or the client closed the connection before the document was judged (its timer ran out on a slow connection). Such an upload leaves nothing: no document, no file, no answer anyone sees, so giving it back tells the applicant nothing a counted attempt would. A read is not started for a client already gone; once one has run, the attempt is given back only MAX_DOCUMENT_READS_GIVEN_BACK times per session (targeted review, round 2): the reads stay bounded by the cap.
Path parameters
session_id
string
required
Request body multipart/form-data
document_type
string
required
country_code
string
required
side
string
front.
front_document_id
string
client_quality_override
string
client_quality_codes
string
file
file
required
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/document" \
-F "document_type=<document_type>" \
-F "country_code=<country_code>" \
-F "file=@/path/to/image.jpg"
Response 200
document_id
string
document_type
string
passport, national_id, drivers_license, other, proof_of_residency, parental_consent, guardian_id.
side
string
front, back.
Default front.
requires_back_side
boolean
false.
front_document_id
string
document_complete
boolean
false.
next_step
string
document_back.
attempts_left
integer
0.
pdf_page_count
integer
extraction_confidence
number
extraction_method
string
country_code_detected
string
country_mismatch
boolean
full_name
string
date_of_birth
string
nationality
string
document_number_masked
string
expiry_date
string
sex
string
is_valid
boolean
validation_errors
array of string
mrz_valid
boolean
field_confidences
object
field_confidences.<key>.value
string
field_confidences.<key>.confidence
number
field_confidences.<key>.source
string
field_confidences.<key>.validated
boolean
false.
image_quality
object
image_quality.blur_score
number
image_quality.contrast_score
number
image_quality.brightness
number
image_quality.resolution
array of integer
image_quality.overall_quality
number
preprocessing_applied
array of string
/api/v1/kyc/{session_id}/liveness
Submit liveness frames for a challenge this session was issued.
Only failures count against the allowance (a passive result that asks for a head turn does not). Once it is used up, the session continues to the selfie and the case goes to review (liveness_attempts_exhausted).
Path parameters
session_id
string
required
Request body multipart/form-data
challenge_id
string
required
challenge_type
string
files
array of file
required
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/liveness" \
-F "challenge_id=<challenge_id>" \
-F "files=<files>"
Response 200
check_id
string
passed
boolean
code
string
attempts_left
integer
0.
attempts_exhausted
boolean
false.
liveness_valid_until
string (date-time)
/api/v1/kyc/{session_id}/liveness/challenge
Issue a liveness challenge.
It is stored, bound to this session and its generation, and can be used once within 10 minutes. An unused one of the same type with more than half its life left is given again rather than a new one.
Path parameters
session_id
string
required
Query parameters
type
string
passive.
curl -X GET "https://comply.mapaycard.com/api/v1/kyc/<session_id>/liveness/challenge"
Response 200
challenge_id
string
challenge_type
string
instructions
string
timeout_seconds
integer
30.
expires_at
string (date-time)
expires_in_seconds
integer
attempts_left
integer
/api/v1/kyc/{session_id}/privacy-notice
The published privacy notice, in the page language.
Path parameters
session_id
string
required
Query parameters
lang
string
curl -X GET "https://comply.mapaycard.com/api/v1/kyc/<session_id>/privacy-notice"
Response 200
id
string
version
integer
lang
string
title
string
summary
string
body
string
biometric_consent_label
string
controller_name
string
controller_address
string
contact_email
string
dpo_contact
string
supervisory_authority
string
retention_period
string
published_at
string
/api/v1/kyc/{session_id}/selfie
Upload a selfie and match it with the ID photo.
The selfie must come from the person who just passed liveness: it is compared with that check's reference frame first. A mismatch the applicant can fix by retaking answers an error with the attempts left; once the allowance is used up the last selfie is kept and a reviewer decides.
Path parameters
session_id
string
required
Request body multipart/form-data
file
file
required
liveness_check_id
string
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/selfie" \
-F "file=@/path/to/image.jpg"
Response 200
verification_id
string
match_passed
boolean
code
string
attempts_left
integer
0.
attempts_exhausted
boolean
false.
next_step
string
processing.
/api/v1/kyc/{session_id}/set-country
Set the country that issued the user's ID document.
This determines which document types are allowed: - If ID country matches organization country: all document types allowed - If ID country differs: only passport is allowed, plus a proof of residence
Locked once a current front of the ID exists: the residence rule and the allowed types would change under the documents already captured.
Path parameters
session_id
string
required
Request body application/json
country_code
string
required
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/set-country" \
-H "Content-Type: application/json" \
-d '{
"country_code": "GN"
}'
Response 200
id_country
string
matches_organization
boolean
allowed_document_types
array of string
organization_country
string
/api/v1/kyc/{session_id}/status
Current KYC session status.
The decision reason and the risk score are never sent to the applicant.
Path parameters
session_id
string
required
curl -X GET "https://comply.mapaycard.com/api/v1/kyc/<session_id>/status"
Response 200
session_id
string
status
string
pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired.
risk_level
string
low, medium, high, critical.
risk_score
integer
decision
string
decision_reason
string
created_at
string (date-time)
expires_at
string (date-time)
next_step
string
id_country
string
allowed_document_types
array of string
/api/v1/kyc/{session_id}/submit
Submit KYC for final verification.
Refused with INCOMPLETE until every required item is there: a session can no longer be approved with no selfie, no liveness or half an ID. Then makes an instant onboarding decision and queues the post-submit checks (duplicate detection), which may only escalate a system approval to review. Sanctions, PEP and adverse-media screening runs only when a partner or an admin requests it, and never changes this decision.
Idempotent: a submitted or decided session answers its latest post-submit task. Poll GET /api/v1/kyc/{session_id}/task/{task_id} for it.
Path parameters
session_id
string
required
curl -X POST "https://comply.mapaycard.com/api/v1/kyc/<session_id>/submit"
Response 200
session_id
string
task_id
string
status
string
polling_url
string
session_status
string
/api/v1/kyc/{session_id}/task/{task_id}
Get status of the post-submit checks.
Poll this endpoint after submitting for verification. The task progresses through: pending -> running -> completed/failed. Its result is never sent to the applicant, and no other task of the session (a screening) is answered.
Path parameters
session_id
string
required
task_id
string
required
curl -X GET "https://comply.mapaycard.com/api/v1/kyc/<session_id>/task/<task_id>"
Response 200
task_id
string
session_id
string
status
string
progress
integer
0.
result
object
error
string
created_at
string (date-time)
completed_at
string (date-time)
Health
Unauthenticated probes for load balancers and orchestration.
/health
Health of the process and its dependencies: a SELECT 1 on the database and a PING to Redis (the task broker), each with a short timeout.
503 when either fails, so a monitor sees an outage the process alone hides.
curl -X GET "https://comply.mapaycard.com/health"
Response 200
status
string
version
string
database
string
redis
string
sanctions_data_updated
string (date-time)
/health/live
Kubernetes liveness probe - app is running.
Returns 200 if the application process is alive and responding.
curl -X GET "https://comply.mapaycard.com/health/live"
/health/ready
Kubernetes readiness probe - app is ready for traffic.
Returns 200 if the application is ready to accept user traffic. Returns 503 if models are not loaded or system is not marked ready.
curl -X GET "https://comply.mapaycard.com/health/ready"
OpenAPI specification
The machine-readable schema this page is built from. Load it into any OpenAPI tool (Postman, Insomnia, a code generator) to get a client for this API.