Skip to content

feat(payouts): surface vop response - #2074

Open
heykulthe wants to merge 3 commits into
mainfrom
feat/vop-changes
Open

feat(payouts): surface vop response#2074
heykulthe wants to merge 3 commits into
mainfrom
feat/vop-changes

Conversation

@heykulthe

@heykulthe heykulthe commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

Surfaces Deutsche Bank's Verification-of-Payee (VoP) outcome on the payout eligibility response, so the caller can tell why a payee was refused and can reconcile the check afterwards.

Motivation and Context

A VoP refusal was previously indistinguishable from any other ineligible payee, the caller got a status and nothing else, so a merchant could not be told whether the IBAN failed to match the payee name, or the payee simply could not be checked. NMTC and NOAP now carry the verdict and Deutsche Bank's own explanation.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies application configuration/environment variables

How did you test it?

Payout Create Response:

Payouts that result in VOP MTCH/CMTC result in additional info being surfaced to merchant in metadata whilst if it's a NMTC/NOAP, error_code and error_message fields are populated.

Create Payout Response (CMTC case):

{
    "payout_id": "payout_dcTH1SFAsaRkixQitBss",
    "merchant_id": "merchant_1786014217",
    "merchant_order_reference_id": null,
    "amount": 101,
    "currency": "EUR",
    "connector": "deutschebank",
    "payout_type": "bank",
    "payout_method_data": {
        "bank": {
            "iban": "DE945************80002",
            "bank_name": "Deutsche Bank",
            "bank_country_code": "DE",
            "bank_city": null,
            "bic": "DEU*****237",
            "account_holder_name": "John Doe"
        }
    },
    "source_bank_data": {
        "iban": "DE991************89001",
        "bank_name": null,
        "bank_country_code": null,
        "bank_city": null,
        "bic": "DEU*****237",
        "account_holder_name": "ABC Inc."
    },
    "billing": {
        "address": {
            "city": "Frankfurt",
            "country": "DE",
            "line1": "Taunusanlage 12",
            "line2": null,
            "line3": null,
            "zip": "60325",
            "state": "HE",
            "first_name": "John",
            "last_name": "Doe",
            "origin_zip": null
        },
        "phone": null,
        "email": null
    },
    "auto_fulfill": true,
    "customer_id": "payout_customer121",
    "customer": {
        "id": "payout_customer121",
        "name": "John Doe",
        "email": "payout_customer@example.com",
        "phone": "999999999",
        "phone_country_code": "+49",
        "customer_document_details": null
    },
    "client_secret": "payout_payout_dcTH1SFAsaRkixQitBss_secret_hZxcgLlpCZkJ9h1T8oRd",
    "return_url": null,
    "business_country": null,
    "business_label": null,
    "description": "any-purpose",
    "billing_descriptor": null,
    "entity_type": "Individual",
    "recurring": false,
    "metadata": {
        "vop_status": "CMTC",
        "additional_info": "The IBAN almost matches the name of the payee. This is the name of the account holder that belongs to the IBAN: CRON HOLDING GMBH. If you authorize the order anyway, this may result in the money being transferred to a payment account whose owner is not the payee you specified.",
        "eligibility_reference_id": "65992296-71d9-58fa-bc79-3f159e4a33bf"
    },
    "merchant_connector_id": "mca_l9oDd6NGEAsvyptN4H3C",
    "status": "pending",
    "error_message": null,
    "error_code": null,
    "profile_id": "pro_jaLf7s7GauSiowdxrCEY",
    "created": "2026-08-06T22:38:26.592Z",
    "connector_transaction_id": "4EA1FDCFE05957C8BBEBCFF7D98B1BD6",
    "priority": null,
    "payout_link": null,
    "email": "payout_customer@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+49",
    "unified_code": null,
    "unified_message": null,
    "payout_method_id": null
}

Get Payout Response (NMTC case)

{
   "payout_id": "payout_7AKlp8jmxZtCHm6iGuQX",
   "merchant_id": "merchant_1786014217",
   "merchant_order_reference_id": null,
   "amount": 101,
   "currency": "EUR",
   "connector": "deutschebank",
   "payout_type": "bank",
   "payout_method_data": {
       "bank": {
           "iban": "DE945************80002",
           "bank_name": "Deutsche Bank",
           "bank_country_code": "DE",
           "bank_city": null,
           "bic": "DEU*****237",
           "account_holder_name": "John Doe"
       }
   },
   "source_bank_data": {
       "iban": "DE712************40602",
       "bank_name": null,
       "bank_country_code": null,
       "bank_city": null,
       "bic": "DEU*****237",
       "account_holder_name": "ABC Inc."
   },
   "billing": {
       "address": {
           "city": "Frankfurt",
           "country": "DE",
           "line1": "Taunusanlage 12",
           "line2": null,
           "line3": null,
           "zip": "60325",
           "state": "HE",
           "first_name": "John",
           "last_name": "Doe",
           "origin_zip": null
       },
       "phone": null,
       "email": null
   },
   "auto_fulfill": true,
   "customer_id": "payout_customer121",
   "customer": {
       "id": "payout_customer121",
       "name": "John Doe",
       "email": "payout_customer@example.com",
       "phone": "999999999",
       "phone_country_code": "+49",
       "customer_document_details": null
   },
   "client_secret": "payout_payout_7AKlp8jmxZtCHm6iGuQX_secret_tFD6wiKLSsMdG8x2xnXR",
   "return_url": null,
   "business_country": null,
   "business_label": null,
   "description": "any-purpose",
   "billing_descriptor": null,
   "entity_type": "Individual",
   "recurring": false,
   "metadata": {
        "eligibility_reference_id": "29d8b78b-eb88-5f4b-b126-9bdf42b4b921"
   },
   "merchant_connector_id": "mca_l9oDd6NGEAsvyptN4H3C",
   "status": "not_permitted",
   "error_message": "The IBAN does not match the name of the payee. If you authorize the order anyway, this may result in the money being transferred to a payment account whose owner is not the payee you specified.",
   "error_code": "NMTC",
   "profile_id": "pro_jaLf7s7GauSiowdxrCEY",
   "created": "2026-08-06T22:41:41.695Z",
   "connector_transaction_id": null,
   "priority": null,
   "payout_link": null,
   "email": "payout_customer@example.com",
   "name": "John Doe",
   "phone": "999999999",
   "phone_country_code": "+49",
   "unified_code": null,
   "unified_message": null,
   "payout_method_id": null
}

Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
@hyperswitch-bot
hyperswitch-bot Bot requested a review from a team as a code owner August 6, 2026 23:31
// Only an eligible payee yields a payout id to act on; the VoP reference is
// reported separately so a refusal is still traceable, and so it survives the
// transfer overwriting `connector_payout_id`.
let connector_payout_id = is_eligible.then(|| vop_id.clone());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you confirm, that this will be the payout reference id for the transaction?

Comment on lines +420 to +421
pub error_code: Option<String>,
pub error_message: Option<String>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we putting error code and message here, shouldn't this be accompanied with error response?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants