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:

1
Create a session

POST /partner/api/v1/sessions returns a kyc_url for your user.

2
Open the link on the customer's phone

The hosted flow collects the ID document (photo or PDF), selfie and liveness check. See Hosted flow integration.

3
Get the result

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.

Example: create a session
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": {}
     }'
Getting an API key

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

  1. Your appYour server

    The customer taps "Verify my identity".

  2. Your serverSentinel

    POST /partner/api/v1/sessions with user_id, applicant and the four *_redirect URLs. The answer carries kyc_url: single-use, valid 30 minutes.

  3. Your serverYour app

    Hand kyc_url to the app. Do not open or fetch it on the way.

  4. Your appBrowser

    Open it once: a Custom Tab on Android, SFSafariViewController or ASWebAuthenticationSession on iOS (a WebView only with the checklist below).

  5. BrowserSentinel

    Consent, ID document, face. The first open spends the link's token and sets a session cookie in that browser.

  6. SentinelBrowser

    At the end, or when the customer leaves, the browser is sent to one of your redirect URLs with ?session_id=…&status=….

  7. BrowserYour app

    Your redirect URL is an Android App Link or an iOS universal link (or the ASWebAuthenticationSession callback), so the app comes back to the front and shows the outcome.

  8. SentinelYour server

    Webhook session.approved, session.rejected or session.completed (in review). This, or GET /partner/api/v1/sessions/{session_id}, is the decision.

kyc_url works once: never fetch it from a server

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.

statusWhenURL usedWhat the app should do
approvedThe onboarding was approved.success_redirectShow "verified", then confirm from your server before opening the account.
in_reviewA reviewer must decide (manual review).pending_redirect, else success_redirectShow "we are checking your documents"; wait for the webhook.
not_approvedThe onboarding was rejected.failure_redirectShow that it did not work and how to get help.
cancelledThe 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_redirectOffer 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.

Kotlin: Custom Tab
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.CAMERA in the manifest and ask for it at run time before opening the page.
  • setJavaScriptEnabled(true) and setDomStorageEnabled(true).
  • setMediaPlaybackRequiresUserGesture(false), or the camera preview does not start.
  • A WebChromeClient whose onPermissionRequest grants RESOURCE_VIDEO_CAPTURE to 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.
Kotlin: WebView
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.

The three body shapes
// 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

HTTPdetail.errorWhenWhat 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.
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.

Envelope
{
  "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:00 is 10:30 UTC.
  • sequence counts the events of one session, starting at 1. Deliveries can arrive late or out of order: store the highest sequence applied per session and apply an event only if its sequence is greater.
  • Every data carries session_id, reference, user_id, status, generation, source, purpose, previous_session_id. status is the onboarding status when the event was sent; generation counts the session's resets; source is session, or a dossier's source (see Dossiers without verification); purpose is onboarding, or renewal with previous_session_id naming the approval it renews (see Renewals).
  • risk_score is an integer from 0 to 100. Screening events report status but never change it.

Events

EventWhendata keys besides the common ones
session.createdA 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.approvedThe 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.rejectedThe 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.completedThe 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_detectedA 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_resolvedA 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.expiredAn 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.resetThe 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_completedA 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_flaggedA 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_failedA 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_confirmedA 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_clearedA 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

HeaderValue
X-Webhook-IDUnique per delivery; use it to ignore duplicates.
X-Event-TypeThe event name, also in the body.
X-Webhook-TimestampUnix time the delivery was signed.
X-Webhook-Signaturesha256=<hex>, present when the key has a signing secret (always in production).
User-AgentSentinel-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.

Python
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.

duplicates
{
  "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
}
FieldMeaning
checkedfalse while the checks that run after submission are pending; items is then empty.
review_priorityhigh while a match is unresolved or the session is flagged, else normal.
open_countItems with no resolution yet.
items[].match_typeDUPLICATE_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_partnertrue 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[].similarityFace similarity (raw cosine, 0 to 1 in practice). No image, embedding, name, date or document number is ever sent.
items[].resolutionnull, 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.
flagSet 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 codeWhen
RENEWAL_SAME_DOCUMENTThe document is the one approved before: the same file, or the same number and expiry date. Nothing was renewed.
RENEWAL_IDENTITY_CHANGEDThe name or date of birth on the new document differs from the approved document's.
RENEWAL_FACE_MISMATCHThe selfie does not match the face of the customer approved before.
RENEWAL_NO_PRIOR_FACEThe 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 /me counts it in billed_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_id starting with gn-local: and a local_entry with the act's reference and the notes (null on other matches). An organization is not screened for PEP, nor against the national list.
  • country is ISO 3166-1 alpha-2 (alpha-3 accepted), optional; reference is yours, up to 100 characters; entity_type defaults to person. Scores are 0 to 100, as on a session's results.
  • Sentinel keeps the query_id, your reference, 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_id starts with gn-local:, datasets is ["gn_local_list"], topics is ["sanction"], and local_entry gives the act's reference and the notes. 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/batch with rescreen: true) or a screening request with rescreen: 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.

ValueDocumentComes from
passportPassportThe hosted flow, and dossiers.
national_idNational ID card (carte nationale d'identité)The hosted flow, and dossiers.
drivers_licenseDriving licence (permis de conduire). A Guinean licence's expiry is printed and read on its back.The hosted flow, and dossiers.
voter_cardVoter card (carte d'électeur)Dossiers only: the hosted flow does not take it.
otherAny 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.

POST

/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
Your stable customer id. Sending it again updates the customer's declared dossier. Pattern ^[A-Za-z0-9._:@-]{1,255}$.
reference string
Your reference.
applicant object required
The identity the customer declared.
applicant.first_name string required
Given names, middle names included (1-100 characters).
applicant.last_name string required
Surname (1-100 characters).
applicant.date_of_birth string (date) required
YYYY-MM-DD; not in the future, at most 120 years ago.
applicant.gender string required
M or F, as on the ID document (lower case is accepted). One of: M, F.
nationality string
ISO 3166-1 alpha-2 (or alpha-3) code; screened when given.
document object required
The ID document the customer declared.
document.type string required
passport, national_id, drivers_license, voter_card or other. One of: passport, national_id, drivers_license, voter_card, other.
document.number string required
The full document number (1-64 characters).
document.type_label string
What the document is, in words, when type is other (for example carte étudiant).
Example request
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
Screen it with POST /sessions/{session_id}/screening.
user_id string
reference string
status string
unverified for a declared dossier, approved for an import. One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source string
One of: session, declared, legacy_import.
created boolean
False when the customer's dossier was updated.
identity_changed boolean
The name, date of birth, nationality or document differs from the one kept before. Default false.
screening_stale boolean
Screening results exist and were screened from the previous identity: screen again. Default false.
POST

/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
Your stable customer id. Importing it again updates the customer's imported dossier. Pattern ^[A-Za-z0-9._:@-]{1,255}$.
reference string
Your reference.
applicant object required
The identity as your approved KYC holds it.
applicant.first_name string required
Given names, middle names included (1-100 characters).
applicant.last_name string required
Surname (1-100 characters).
applicant.date_of_birth string (date) required
YYYY-MM-DD; not in the future, at most 120 years ago.
applicant.gender string required
M or F, as on the ID document (lower case is accepted). One of: M, F.
nationality string
ISO 3166-1 alpha-2 (or alpha-3) code; screened when given.
document object required
The ID document of the approved KYC.
document.type string required
passport, national_id, drivers_license, voter_card or other. One of: passport, national_id, drivers_license, voter_card, other.
document.number string required
The full document number (1-64 characters).
document.type_label string
What the document is, in words, when type is other (for example carte étudiant).
document.issue_date string (date)
YYYY-MM-DD.
document.expiry_date string (date)
YYYY-MM-DD.
document.issuing_country string
ISO 3166-1 alpha-2 (or alpha-3) code of the issuing country.
approval object required
Who approved it on your side, and when.
approval.approved_at string (date-time) required
UTC; an ISO datetime without offset is read as UTC. null when unknown (old records).
approval.approved_by string required
The officer who approved it (name or email).
approval.source string required
Where the approval was made.
Example request
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
Screen it with POST /sessions/{session_id}/screening.
user_id string
reference string
status string
unverified for a declared dossier, approved for an import. One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source string
One of: session, declared, legacy_import.
created boolean
False when the customer's dossier was updated.
identity_changed boolean
The name, date of birth, nationality or document differs from the one kept before. Default false.
screening_stale boolean
Screening results exist and were screened from the previous identity: screen again. Default false.
superseded_declared boolean
The customer's declared dossier became this imported one (same session_id). Default false.
duplicates array of object
Other customers whose dossier has this document number, found by this import (match_type DOCUMENT_NUMBER_MATCH). The approval is not changed.
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
GET

/partner/api/v1/me

Get current API key's information and usage statistics.

Requires your API key in the X-API-Key header.

Example request
curl -X GET "https://comply.mapaycard.com/partner/api/v1/me" \
  -H "X-API-Key: <your-api-key>"
POST

/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
1 to 500 of your customer ids; a repeated one is screened once.
rescreen boolean
Screen again even a customer whose current result still answers (billed). Default false.
allow_unapproved boolean
Without an approved session, use the latest rejected or in-review one, else a declared dossier. Default false.
Example request
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
     }'
GET

/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
Example request
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
One of: queued, running, done.
created_at string (date-time)
rescreen boolean
allow_unapproved boolean
total integer
queued integer
running integer
done integer
Finished: screened by this batch, or answered by a current result (reused).
reused integer
skipped integer
failed integer
billed_queries integer
OpenSanctions queries this batch's runs made.
items array of object
items[].user_id string
items[].session_id string
The session screened.
items[].state string
One of: queued, running, done, reused, skipped, failed.
items[].error string
items[].task_id string
items[].billed_queries integer
Default 0.
items[].screening_status string
The session's screening_status now.
POST

/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
The full name as you hold it.
country string
ISO 3166-1 alpha-2 (or alpha-3) code of the country linked to the name.
entity_type string
An organization is not screened for PEP, nor against the Guinea local list. One of: person, organization. Default person.
reference string
Your transaction reference, kept with the query.
Example request
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
One of: clear, potential_match.
results array of object
results[].category string
One of: sanctions, pep, adverse_media.
results[].state string
One of: no_match, potential_match.
results[].matches array of object
results[].matches[].entity_id string
results[].matches[].name string
results[].matches[].score number
Default 0.
results[].matches[].datasets array of string
results[].matches[].topics array of string
results[].matches[].url string
results[].matches[].local_entry object
A local-list hit: why the person is listed.
billed boolean
GET

/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
Default 1.
per_page integer
Default 20.
Example request
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
One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
sessions[].source string
session (the hosted verification), declared (unverified, S9) or legacy_import (S2). One of: session, declared, legacy_import. Default session.
sessions[].dossier object
Null for a hosted session.
sessions[].dossier.source string
One of: session, declared, legacy_import.
sessions[].dossier.superseded_by_session_id string
The customer's submitted hosted session that replaced this dossier.
sessions[].dossier.superseded_at string
sessions[].dossier.approved_at string
Imported: when you approved it (UTC).
sessions[].dossier.approved_at_unknown boolean
Imported: the approval date was not known.
sessions[].dossier.approved_by string
Imported: who approved it on your side.
sessions[].dossier.approval_source string
Imported: paycard_legacy.
sessions[].purpose string
onboarding, or renewal of an approved customer's document. One of: onboarding, renewal. Default onboarding.
sessions[].previous_session_id string
For a renewal: the customer's approval it is checked against.
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
Default false.
sessions[].face_matched boolean
Default false.
sessions[].liveness_passed boolean
Default false.
sessions[].sanctions_clear boolean
sessions[].screening_status string
sessions[].screening object
Screening state and latest results for a session. It never changes the onboarding.
sessions[].screening.screening_status string
not_requested, pending, in_progress, completed, flagged or error.
sessions[].screening.screening_outcome string
Null until screened. One of: 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
POST

/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
Your stable customer id (letters, digits and . _ : @ -). An open session for the same user_id is returned instead of a new one. Pattern ^[A-Za-z0-9._:@-]{1,255}$.
reference string
Partner's reference ID for this session.
applicant object required
The identity the customer declared.
applicant.first_name string required
Given names, middle names included (1-100 characters).
applicant.last_name string required
Surname (1-100 characters).
applicant.date_of_birth string (date) required
YYYY-MM-DD; not in the future, at most 120 years ago.
applicant.gender string required
M or F, as on the ID document (lower case is accepted). One of: M, F.
purpose string
renewal: a new ID document for a customer you already verified. The user_id must have an approved session or imported dossier (else 409 NO_APPROVED_SESSION); the renewal is checked against it (face, identity, document) and goes to review when they differ. One of: onboarding, renewal. Default onboarding.
guardian_user_id string
Required for a minor: the user_id of the parent or guardian, who must already have an approved verification. Ignored for an adult. Pattern ^[A-Za-z0-9._:@-]{1,255}$.
guardian_consent boolean
You attest that the guardian consented. Must be true for a minor.
locale string
fr or en. Without it (or with another value) the phone's language applies, else French. One of: fr, en.
success_redirect string
https URL to send the applicant to when approved.
failure_redirect string
https URL to send the applicant to when not approved.
cancel_redirect string
https URL to send the applicant to when they leave without finishing.
pending_redirect string
https URL for a case in review (defaults to success_redirect).
metadata object
Additional metadata to store.
Example request
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
URL to redirect user to for KYC verification.
access_token string
Single-use link token (valid for 30 minutes).
expires_at string (date-time)
When the link stops working (same as link_expires_at).
status string
One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
reused boolean
True when the customer's open session was returned. Default false.
link_expires_at string (date-time)
session_expires_at string (date-time)
Until when the session can be resumed.
guardian_session_id string
The guardian's approved session, for a minor.
purpose string
One of: onboarding, renewal. Default onboarding.
previous_session_id string
For a renewal: the customer's approval it is checked against.
GET

/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
Example request
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
One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source string
session (the hosted verification), declared (unverified, S9) or legacy_import (S2). One of: session, declared, legacy_import. Default session.
dossier object
Null for a hosted session.
dossier.source string
One of: session, declared, legacy_import.
dossier.superseded_by_session_id string
The customer's submitted hosted session that replaced this dossier.
dossier.superseded_at string
dossier.approved_at string
Imported: when you approved it (UTC).
dossier.approved_at_unknown boolean
Imported: the approval date was not known.
dossier.approved_by string
Imported: who approved it on your side.
dossier.approval_source string
Imported: paycard_legacy.
purpose string
onboarding, or renewal of an approved customer's document. One of: onboarding, renewal. Default onboarding.
previous_session_id string
For a renewal: the customer's approval it is checked against.
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
Default false.
face_matched boolean
Default false.
liveness_passed boolean
Default false.
sanctions_clear boolean
screening_status string
screening object
Screening state and latest results for a session. It never changes the onboarding.
screening.screening_status string
not_requested, pending, in_progress, completed, flagged or error.
screening.screening_outcome string
Null until screened. One of: 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
not_screened, no_match, potential_match, confirmed or cleared.
screening.categories[].stale boolean
Screened from an identity that has since changed. Default false.
screening.categories[].screened_at string
screening.categories[].hit_count integer
Default 0.
screening.categories[].open_hit_count integer
Default 0.
screening.results array of object
screening.results[].category string
screening.results[].subject string
screening.results[].match_found boolean
screening.results[].highest_score number
Default 0.
screening.results[].matches array of object
screening.results[].screened_at string
screening.adjudications array of object
screening.adjudications[].match_kind string
PEP hits: pep when the person holds the position (role.pep), rca when they are a relative or close associate of one (role.rca only). Null in other categories. One of: pep, rca.
screening.adjudications[].adverse_media_type string
Adverse-media hits: the main sub-type, most serious first: wanted, crime (crime.*), debarment, regulatory (reg.*), else other. Null in other categories. One of: wanted, crime, debarment, regulatory, other.
screening.adjudications[].adverse_media_types array of string
Adverse-media hits: every sub-type the topics carry, most serious first.
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
Default reviewer.
extracted_data object
onboarding object
The onboarding decision (document, face, liveness, declared identity). Screening never changes it.
onboarding.status string
One of: 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
system, reviewer or partner_import.
onboarding.decided_at string (date-time)
onboarding.submitted_at string (date-time)
onboarding.final boolean
False while a system decision may still be escalated by the post-submit checks. Default false.
onboarding.risk_level string
onboarding.risk_score integer
onboarding.generation integer
Default 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
Each check on the current evidence.
onboarding.checks.document string
The ID document's checks. One of: pass, fail, missing, not_performed.
onboarding.checks.face_match string
The selfie against the ID photo. One of: pass, fail, missing, not_required, attempts_exhausted, not_performed.
onboarding.checks.liveness string
The liveness check. One of: pass, fail, missing, not_required, attempts_exhausted, not_performed.
onboarding.checks.identity_match string
The declared identity against the document (overall result). One of: match, close, partial_mismatch, mismatch, unverified, not_provided, not_performed.
onboarding.checks.age string
From a confirmed document date of birth. One of: 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
Default false.
onboarding.guardian.attested_at string (date-time)
onboarding.guardian.guardian_screening_outcome string
The guardian's own screening outcome (not screened again for the minor). One of: clear, potential_match, confirmed_match, cleared.
identity_match object
How the declared identity compares with the document. Never the values themselves.
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
Default 0.
identity_match.checked_at string
document_identity object
What the ID document says. No image, and only the last 4 characters of its number.
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
Face matches with other customers, found after submission (null before submit).
duplicates.checked boolean
duplicates.review_priority string
duplicates.open_count integer
Default 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
This customer's session was linked to a confirmed fraud case by a reviewer.
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
GET

/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
Example request
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
One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired, unverified.
source string
One of: session, declared, legacy_import.
document_type string
passport, national_id, drivers_license, voter_card or other.
document_type_label string
For other: what the document is.
document_number string
The full document number.
issue_date string
YYYY-MM-DD, or null when not read.
expiry_date string
YYYY-MM-DD, or null when not read (a driving licence's is read on its back).
issuing_country string
ISO 3166-1 alpha-2, or null.
nationality string
ISO 3166-1 alpha-2, or null when not recognised.
surname string
given_names string
full_name string
date_of_birth string
YYYY-MM-DD, or null when not read.
sex string
One of: M, F.
POST

/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
Example request
curl -X POST "https://comply.mapaycard.com/partner/api/v1/sessions/<session_id>/refresh-token" \
  -H "X-API-Key: <your-api-key>"
POST

/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
Example request
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"
     }'
POST

/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
Screen again even though the session already has a current result. Each screening is a billed OpenSanctions query per person. Default false.
include_guardian boolean
Also screen the guardian linked to a minor's session (one more billed query). Default false.
Example request
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
     }'
POST

/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
Who the hit is on. Needed when the entity is a hit in several places. One of: applicant, guardian.
category string
The hit's category. Needed when the entity is a hit in several places. One of: sanctions, pep, adverse_media.
entity_id string required
The OpenSanctions entity id of the hit.
decision string required
confirmed: it is this person. cleared: a false positive. One of: confirmed, cleared.
reason string required
Why, at least 10 characters. Kept encrypted; never returned by the API.
officer object required
Your compliance officer who made the decision. Kept encrypted, shown only in Sentinel's console.
officer.name string required
officer.email string required
Pattern ^[^@\s]+@[^@\s]+$.
Example request
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
Also in the adjudication webhook: match your officer record by it.
session_id string
subject string
category string
entity_id string
decision string
category_state string
The hit's category for this person, after the decision.
screening_status string
screening_outcome string
One of: clear, potential_match, confirmed_match, cleared.
screening_flags object
webhook_event string
session.screening_match_confirmed or session.screening_match_cleared, also sent.
GET

/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
Pattern ^[A-Za-z0-9._:@-]{1,255}$.

Query parameters

allow_unapproved boolean
Default false.
Example request
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 state and latest results for a session. It never changes the onboarding.
screening.screening_status string
not_requested, pending, in_progress, completed, flagged or error.
screening.screening_outcome string
Null until screened. One of: 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
not_screened, no_match, potential_match, confirmed or cleared.
screening.categories[].stale boolean
Screened from an identity that has since changed. Default false.
screening.categories[].screened_at string
screening.categories[].hit_count integer
Default 0.
screening.categories[].open_hit_count integer
Default 0.
screening.results array of object
screening.results[].category string
screening.results[].subject string
screening.results[].match_found boolean
screening.results[].highest_score number
Default 0.
screening.results[].matches array of object
screening.results[].screened_at string
screening.adjudications array of object
screening.adjudications[].match_kind string
PEP hits: pep when the person holds the position (role.pep), rca when they are a relative or close associate of one (role.rca only). Null in other categories. One of: pep, rca.
screening.adjudications[].adverse_media_type string
Adverse-media hits: the main sub-type, most serious first: wanted, crime (crime.*), debarment, regulatory (reg.*), else other. Null in other categories. One of: wanted, crime, debarment, regulatory, other.
screening.adjudications[].adverse_media_types array of string
Adverse-media hits: every sub-type the topics carry, most serious first.
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
Default reviewer.
POST

/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
Pattern ^[A-Za-z0-9._:@-]{1,255}$.

Request body application/json

rescreen boolean
Screen again even though the session already has a current result. Each screening is a billed OpenSanctions query per person. Default false.
include_guardian boolean
Also screen the guardian linked to a minor's session (one more billed query). Default false.
allow_unapproved boolean
Without an approved session, use the latest rejected or in-review one. Default false.
Example request
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
     }'
GET

/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.

Example request
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
Default false.
GET

/partner/api/v1/webhooks

List webhook delivery attempts.

Requires your API key in the X-API-Key header.

Query parameters

status string
page integer
Default 1.
per_page integer
Default 20.
Example request
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
POST

/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
Example request
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.

POST

/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
Default false.
Example request
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
Default true.
notice_version integer
consented_at string (date-time)
next_step string
Default document.
POST

/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
Default front.
front_document_id string
client_quality_override string
client_quality_codes string
file file required
Example request
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
One of: passport, national_id, drivers_license, other, proof_of_residency, parental_consent, guardian_id.
side string
One of: front, back. Default front.
requires_back_side boolean
Default false.
front_document_id string
document_complete boolean
Default false.
next_step string
Default document_back.
attempts_left integer
Default 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
Default false.
image_quality object
Image quality metrics.
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
POST

/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
Example request
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
Default 0.
attempts_exhausted boolean
Default false.
liveness_valid_until string (date-time)
GET

/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, or head_turn for the step-up. Default passive.
Example request
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
Default 30.
expires_at string (date-time)
expires_in_seconds integer
attempts_left integer
GET

/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
Example request
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
POST

/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
Example request
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
Default 0.
attempts_exhausted boolean
Default false.
next_step string
Default processing.
POST

/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
ISO 2-letter country code.
Example request
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
GET

/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
Example request
curl -X GET "https://comply.mapaycard.com/api/v1/kyc/<session_id>/status"

Response 200

session_id string
status string
One of: pending, document_uploaded, selfie_uploaded, liveness_completed, screening_in_progress, manual_review, approved, rejected, expired.
risk_level string
One of: 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
POST

/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
Example request
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
GET

/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
Example request
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
Default 0.
result object
error string
created_at string (date-time)
completed_at string (date-time)

Health

Unauthenticated probes for load balancers and orchestration.

GET

/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.

Example request
curl -X GET "https://comply.mapaycard.com/health"

Response 200

status string
version string
database string
redis string
sanctions_data_updated string (date-time)
GET

/health/live

Kubernetes liveness probe - app is running.

Returns 200 if the application process is alive and responding.

Example request
curl -X GET "https://comply.mapaycard.com/health/live"
GET

/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.

Example request
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.