Migrating From Organization Verifications to Compliance Reviews

This guide is for integrations currently using POST /organizations/{organizationId}/verification to onboard organizations for KYC (individual) or KYB (business). The new Compliance Reviews API replaces it with a smaller, composable surface that better reflects how reviews are actually evaluated. The legacy verification endpoint will continue to work during the migration window, but every new integration should target Compliance Reviews — and existing integrations should migrate at their next planned change.

The behavior of the system has not changed; only the API shape has. The same data you submit today still drives the same compliance decision.

What changes

Four things are different. The rest of this guide walks each one in detail.

#Old (verification)New (compliance review)
1One large request body containing every piece of KYC/KYB data.Several small attestation uploads, one per logical group of fields.
2Documents passed inline as base64-encoded documentContent data URIs.Documents uploaded to S3 via a presigned URL, then referenced by documentId.
3Validation errors all surface at submission time as a single 400.Per-attestation validation: each upload can be flagged or rejected independently.
4Submit is a side effect of POST /verification.Submit is an explicit POST .../submit call that can reject blocking failures or require you to acknowledge warnings.

The rest of the API — organizations, accounts, payouts — is unchanged. Only the onboarding submission path is migrating.

The Mural docs already cover the new flow end-to-end. Read Individual Compliance Review or Business Compliance Review before starting, then come back to this guide for the field-by-field mapping.

Change 1: One request → many attestations

The old endpoint took the entire KYC/KYB profile in a single body. The new flow breaks that body into discrete attestation types, each uploaded independently via PUT /api/compliance/{organizationId}/reviews/{reviewId}/attestations. Re-uploading the same type replaces the prior value, which is how you correct mistakes prior to submission.

Conceptually, one call becomes a short sequence:

Before — one request:

POST /organizations/{id}/verification
{
  "type": "business",
  "businessIdentificationInfo": { … },
  "businessDetails": { … },
  "financialDetails": { … },
  "formationDocuments": [ … ],
  "ultimateBeneficialOwners": [ … ],
  "controlPerson": { … }
}

After — a short sequence of small requests:

POST /api/compliance/{id}/reviews                                         ← create draft review
PUT  /api/compliance/{id}/reviews/{reviewId}/attestations                 ← N times, one per attestation type, or once with all attestation types
POST /api/compliance/{id}/associated-persons                              ← per UBO / control person
PUT  /api/compliance/{id}/associated-persons/{personId}/attestations      ← per UBO / control person
POST /api/compliance/{id}/reviews/{reviewId}/submit                       ← explicit submit

Business mapping (KYB)

The old KYCBusinessVerificationRequest body maps to the new attestation types like this. Each row is an upload to PUT /reviews/{reviewId}/attestations.

Old field (under KYCBusinessVerificationRequest)New attestation typeNew field
businessIdentificationInfo.businessNamebusinessIdentificationbusinessName
businessIdentificationInfo.registrationNumberbusinessIdentificationregistrationNumber
businessIdentificationInfo.dbaNamebusinessIdentificationdbaName
businessIdentificationInfo.taxIdentificationNumberbusinessTaxInfotaxIdentificationNumber, taxIdentificationNumberType
businessIdentificationInfo.businessAddressbusinessAddressregisteredAddress (+ optional operatingAddress)
businessIdentificationInfo.businessPhoneNumberbusinessContactInfophoneNumber
businessIdentificationInfo.businessEmailbusinessContactInfoemail
businessDetails.businessWebsitebusinessDetailsbusinessWebsite
businessDetails.legalStructurebusinessDetailslegalStructure
businessDetails.dateOfIncorporationbusinessDetailsdateOfIncorporation
businessDetails.industrybusinessDetailsindustry
businessDetails.businessDescriptionbusinessDetailsbusinessDescription
businessDetails.usageOfMuralbusinessDetailsintendedMuralUsage
businessDetails.transmittingCustomerFundsbusinessOperationstransmittingCustomerFunds
businessDetails.customerFundsTransmissionDetailsbusinessOperationscustomerFundsTransmissionDetails
financialDetails.highRiskBusinessActivitiesbusinessOperationshighRiskBusinessActivities
(new)businessOperationsprimaryTargetMarket, expectedCounterpartyCountries
financialDetails.primaryIncomeSourceCategorybusinessFinancialInfoprimaryIncomeSourceCategory
financialDetails.primaryIncomeSourceDescriptionbusinessFinancialInfoprimaryIncomeSourceDescription
financialDetails.annualRevenuebusinessFinancialInfoannualRevenue
financialDetails.estimatedMonthlyVolumebusinessFinancialInfoestimatedMonthlyVolume
formationDocuments[]businessFormationDocumentsformationDocuments[] (each: documentType + documentId)
uboOwnershipDocuments[] (international only)businessUboOwnershipDocumentsuboOwnershipDocuments[] (each: documentType + documentId)
businessProofOfAddress[] (international only)businessProofOfAddressDocumentsproofOfAddressDocuments[] (each: documentType + documentId)
flowOfFundsDoc (conditional: transmittingCustomerFunds=true)businessFlowOfFundsDocumentdocument (documentType + documentId)
controlPerson + ultimateBeneficialOwners[]businessAssociatedPersonsassociatedPersonIds[] (see below)

The two businessOperations fields primaryTargetMarket and expectedCounterpartyCountries are new and required for FULL. Plan to collect them before migrating — they are not derivable from the old body.

Associated persons (UBOs and control person)

In the old API, controlPerson and each entry in ultimateBeneficialOwners[] were full nested KYC objects embedded in the business request. In the new API, each person is a first-class resource you create before attaching them to the review:

  1. POST /api/compliance/{organizationId}/associated-persons to create the person and get back an id. Use relationships: ["CONTROL_PERSON"], ["BENEFICIAL_OWNER"], or both for a person filling both roles.
  2. PUT /api/compliance/{organizationId}/associated-persons/{associatedPersonId}/attestations to upload that person's attestations — associatedPersonPersonalInfo, associatedPersonContactInfo, associatedPersonResidentialAddress, associatedPersonTaxInfo, associatedPersonIdentityDocument for both tiers, plus associatedPersonProofOfAddress for FULL.
  3. Add the returned ids to the review's businessAssociatedPersons attestation under associatedPersonIds[].

The full field-level mapping for an associated person's attestations mirrors the individual KYC mapping below — the only difference is the route they are uploaded to.

Individual mapping (KYC)

The old KYCIndividualOrganizationVerificationRequest body maps as follows. Both tiers (light and standard) use these same attestation types — standard requires all of them, light collapses the first three into a single individualPersonalInfoLight attestation.

Old fieldNew attestation type (standard)New field
individualPersonalInfo.nameindividualPersonalInfoname
individualPersonalInfo.birthDateindividualPersonalInfobirthDate
individualPersonalInfo.nationalityindividualPersonalInfonationality
isPoliticallyExposedindividualPersonalInfoisPoliticallyExposed
individualPersonalInfo.individualEmailindividualContactInfoemail
individualPersonalInfo.individualPhoneNumberindividualContactInfophoneNumber
individualPersonalInfo.individualResidentialAddressindividualResidentialAddressaddress
individualPersonalInfo.taxIdNumberindividualTaxInfotaxIdentificationNumber, taxIdentificationNumberType
individualIdentityInfo.governmentIdindividualIdentityDocumentissuingCountry, governmentIdNumber, governmentId (discriminated by type, with documentId)
individualFinancialInformation.sourceOfFundsindividualFinancialInfosourceOfFunds
individualFinancialInformation.currentEmploymentStatusindividualFinancialInfoemploymentStatus
individualFinancialInformation.primaryAccountPurposeindividualFinancialInfoprimaryAccountPurpose
individualFinancialInformation.latestOccupationindividualFinancialInfooccupation
individualFinancialInformation.monthlyUsdVolumeindividualFinancialInfomonthlyUsdVolume
individualFinancialInformation.isTransactingOnBehalfOfOthersindividualFinancialInfoisTransactingOnBehalfOfOthers
individualFinancialInformation.transactionCurrencies[]individualFinancialInfotransactionCurrencies[]
individualFinancialInformation.sourceOfFundsDocumentindividualSourceOfFunds (conditional)sourceOfFundsDocuments[] (each: documentType + documentId)
individualProofOfAddress (conditional)individualProofOfAddress (conditional)proofOfAddressDocuments[] (each: documentType + documentId)

For light tier, the name, birthDate, nationality (optional), phoneNumber, and residentialAddress fields combine into a single individualPersonalInfoLight attestation — that is the entire payload required to onboard.

Change 2: Document URLs → upload endpoints

The old endpoint accepted documents inline as base64 data URIs in documentContent. The new flow uploads documents to S3 first and then references them by documentId:

  1. POST /api/documents/generate-upload-url with the filename and MIME contentType. The response includes a documentId, a presigned S3 uploadUrl, and a set of signed form fields.
  2. POST the file directly to S3 as multipart/form-data. Send every entry from uploadUrlFormFields as a form field, and append file last — S3 ignores anything after the file part.
  3. Pass the returned documentId into the attestation that references it (e.g. inside businessFormationDocuments.formationDocuments[], or inside individualIdentityDocument.governmentId).

The full flow — including the JavaScript reference implementation, the 15-minute presigned URL expiry, the 10 MB file size cap, and the allowed content types — is documented in Document Uploads.

This is the single biggest payload change. The old endpoint's largest field by volume was always documentContent; the new payloads are small JSON bodies with UUID references.

Change 3: Per-attestation validation failures

In the legacy flow, validation issues only surfaced at submit time as a single 400. In the new flow, each attestation is validated as it lands, and the review carries a per-attestation validationStatus you can inspect at any time via GET /api/compliance/{organizationId}/reviews/{reviewId}.

A validation failure on an attestation has two fields you need to read:

  • failureCode — a stable enum identifying the failure (for example, INCORRECT_DOCUMENT).
  • severity — either BLOCKING or WARNING.

The difference matters at submit time (see Change 4):

  • BLOCKING failures cannot be acknowledged. You must re-upload the affected attestation with corrected data before submission will succeed.
  • WARNING failures can be cleared by re-uploading, or acknowledged at submit time.

Re-uploading the same attestation type replaces the prior version and re-runs validation. Continue iterating until every attestation either passes or carries only WARNING-severity failures you intend to acknowledge.

Change 4: Submit is explicit and can fail

The old endpoint conflated "save my data" and "submit for review" — every successful POST /verification immediately handed the request off to compliance. The new flow separates them: attestation uploads are independent of submission, and you submit explicitly with POST /api/compliance/{organizationId}/reviews/{reviewId}/submit.

Submission can fail in two ways:

422 — MissingAttestationsException

Returned when one or more required attestations have not yet been uploaded. The response body lists exactly which attestationTypes are still required and which have already been uploaded — on the review itself or on any associated person. The fix is to upload the missing attestations and re-submit.

412 — AttestationValidationsRejectedException

Returned when one or more uploaded attestations carry validation failures. The response body identifies the offending attestations along with their failures[] (failureCode + severity).

There are two paths forward:

  1. Re-upload to clear the failures. This is the only path for BLOCKING failures and is also valid for WARNING failures.

  2. Acknowledge the warnings. If every remaining failure has severity: "WARNING" and you accept the risk, retry submission with acknowledgeWarnings: true in the request body:

    curl --request POST \
         --url https://api.muralpay.com/api/compliance/{organizationId}/reviews/{reviewId}/submit \
         --header 'accept: application/json' \
         --header 'authorization: Bearer $API-KEY' \
         --header 'content-type: application/json' \
         --data '{ "acknowledgeWarnings": true }'

    The flag is scoped to that single submit attempt. If new WARNINGs appear after a subsequent re-upload, you will need to acknowledge them again.

BLOCKING failures are never acknowledgeable — passing acknowledgeWarnings: true while at least one BLOCKING failure remains still returns 412.

On success the review transitions from draft to inReview and is evaluated asynchronously. Poll GET /api/compliance/{organizationId}/reviews/{reviewId} for the decision.

Suggested migration sequence

A low-risk path for an existing integration:

  1. Implement document uploads against POST /api/documents/generate-upload-url. This is the largest behavioral change and is safe to ship in isolation — old verifications continue to work while you build it.
  2. Build the new submission flow in a feature branch or behind a flag, mirroring the existing onboarding code path. Use the field mapping tables above to decompose your current payload into attestations.
  3. Wire the new validation/warning UX (Changes 3 and 4) — the failure shapes are new and your client will need to surface them to end users.
  4. Cut new organizations over to the compliance review flow first; leave in-flight verifications on the legacy endpoint until they reach a terminal status.
  5. Sunset the legacy code path once your traffic on POST /organizations/{id}/verification has drained.

If you are starting fresh, skip the legacy endpoint entirely and follow the Individual Compliance Review or Business Compliance Review guides directly.