Create the clear-text card object and perform encryption only inside an approved PCI-controlled component. Never write PAN, security code, the unencrypted concatenated block, or the private key to application logs, browser analytics, review comments, or source control.
Straight-Through Disbursement
Straight-through disbursement submits a payout to a recipient without creating and activating a Commerce Hub party for that recipient. The caller identifies the recipient with its own merchantPartyId and sends the recipient's owner details and an encrypted payment card in the same request.
Use it when your platform manages recipient identity itself and needs to pay that recipient without the full onboarding flow described in Onboard Participant.
Key Rules
transactionDetails.operationTypeisPAYOUT.payee.typeisPERSONAL.payee.partyRoleisRECIPIENT.payee.platform.merchantPartyIdis the caller-managed unique identifier for the recipient.payee.ownersis required for the recipient.merchantDetailsare typically supplied for gateway validation of the client.merchantTransactionIdmust be unique for every payout request.amount.totalis variable and supports up to 2 decimal places.
Encrypt the Payout Card
The payout destination is a PaymentCard sent as target.encryptionData. Commerce Hub Multi-Use Public Key (MUPK) encryption uses an RSA public key to protect payment-instrument data before it is stored or sent. For the full encryption contract, see Multi-Use Public Key Encryption.
- Obtain a MUPK through the approved Commerce Hub generate-key operation (step 3).
- Keep the returned
keyIdwith the corresponding Base64-encoded public key. - Confirm the key is active for the selected environment and merchant configuration.
- Confirm the required card fields and
encryptionTargetwith the owning Commerce Hub contract.
Do not reuse a key from QA in certification or production.
Generate unencrypted encryptionBlock
The encryptionBlock is passed through the PaymentCard request to encrypt the data. It is a concatenated string of the payment instrument's unencrypted data.
const cardData = {
"cardData": "4005550000000019",
"nameOnCard": "John Doe",
"expirationMonth": "01",
"expirationYear": "2034",
"securityCode": "123"
};
const encryptionBlock = await asymmetricallyEncrypt(rsaAsymmetricPublicKey, Object.values(cardData).join(""));For this ordered object, the clear-text value is conceptually 4005550000000019John Doe012034123. It is shown only to explain ordering. Do not persist or log it.
Generate encryptionBlockFields
encryptionBlockFields lists each data field with its byte length. The order must match the order used to build the encryptionBlock in step 1.
const encoder = new TextEncoder();
const encryptionBlockFields = Object.keys(cardData)
.map(key => `card.${key}:${encoder.encode(cardData[key]).length}`)
.join(',');card.cardData:16,card.nameOnCard:8,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3Generate key
Key generation is a one-time step for a given environment and merchant.
Commerce Hub lets the merchant provision a new encryption key for payment data that will be stored and forwarded to Commerce Hub later. The response contains a valid Commerce Hub-generated merchant public key for card encryption. The example below contains the minimum parameters for a successful request.
API: POST /security/v1/keys/generate
{
"merchantDetails": {
"merchantId": "100000000000001",
"terminalId": "10000001"
}
}{
"gatewayResponse": {
"transactionProcessingDetails": {
"transactionTimestamp": "2025-10-08T15:36:36.173418042Z",
"apiTraceId": "a515111866f540fea8abf33b4c6036c5",
"clientRequestId": "5066690",
"transactionId": "a515111866f540fea8abf33b4c6036c5",
"apiKey": "<masked-api-key>"
}
},
"asymmetricKeyDetails": {
"keyId": "2217da6db37189a618852320bcfe7308",
"encryptionType": "RSA",
"modulus": 2048,
"encodedPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqRvQjy3u7Ge3yJAVSSEdCb69+p921gLnd5qwYxtCwUZz.....",
"validFrom": "2025-10-08T15:36:36.231026246Z",
"validTo": "2026-01-06T15:36:36.231026246Z",
"status": "ACTIVE",
"source": "FutureX"
},
"symmetricKeyDetails": {
"encryptionType": "AES-GCM",
"modulus": 256
}
}Perform RSA encryption
Use the Base64-encoded public key from step 3 to encrypt the encryptionBlock created in step 1.
const asymmetricallyEncrypt = async (base64PubKey, sourceString) => {
const keyBuf = toArrayBuffer(window.atob(base64PubKey));
const pubKeyDer = await window.crypto.subtle.importKey(
"spki",
keyBuf,
{ name: "RSA-OAEP", hash: "SHA-256" },
true,
["encrypt"]
);
const encryptedBlock = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
pubKeyDer,
new TextEncoder().encode(sourceString)
);
return toBase64Encode(encryptedBlock);
};
const toBase64Encode = (arrayBuffer) =>
window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));Form encryptionData
{
"encryptionData": {
"keyId": "79cd0553-9db5-4676-989b-f29edfbb6a51",
"encryptionType": "RSA",
"encryptionBlock": "cyz8/XQHosFcIVfWcRs0KL....",
"encryptionBlockFields": "card.cardData:16,card.nameOnCard:8,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3",
"encryptionTarget": "MANUAL"
}
}| Field | Type | Maximum length | Description |
|---|---|---|---|
keyId | String | 40 | Identifier returned with the public key and used by Commerce Hub to select the decryption key. |
encryptionType | String | 256 | Use RSA for this MUPK flow. |
encryptionBlock | String | 2000 | Base64-encoded RSA ciphertext. |
encryptionBlockFields | String | 256 | Ordered object.field:byte_count descriptors used during decryption. |
encryptionTarget | String | 256 | Identifies how the payment data was entered. MANUAL is used by this manually entered card example. |
deviceType | String | 256 | Original device type; required only when the owning in-person integration contract requires it. |
Build the payment source
Place the encryptionData under target with sourceType set to PaymentCard.
{
"target": {
"sourceType": "PaymentCard",
"encryptionData": {
"keyId": "<key-id-returned-by-generate-key>",
"encryptionType": "RSA",
"encryptionBlock": "<base64-rsa-ciphertext>",
"encryptionBlockFields": "card.cardData:16,card.nameOnCard:8,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3",
"encryptionTarget": "MANUAL"
}
}
}const toArrayBuffer = (str) => {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
};
const toBase64Encode = (arrayBuffer) => window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
// RSA Algorithm
const asymmetricallyEncrypt = async (base64PubKey, sourceString) => {
const keyBuf = toArrayBuffer(window.atob(base64PubKey));
const pubKeyDer = await window.crypto.subtle.importKey(
"spki",
keyBuf,
{ name: "RSA-OAEP", hash: "SHA-256" },
true,
["encrypt"]
);
const encryptedBlock = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
pubKeyDer,
new TextEncoder().encode(sourceString)
);
return toBase64Encode(encryptedBlock);
};
(async () => {
const rsaAsymmetricPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3bOOfW6F6rMSmSy2/.....";
const cardData = {
"cardData": "4005550000000019",
"nameOnCard": "John Doe",
"expirationMonth": "01",
"expirationYear": "2034",
"securityCode": "123"
};
const encryptionBlock = await asymmetricallyEncrypt(rsaAsymmetricPublicKey, Object.values(cardData).join(""));
const encoder = new TextEncoder();
const encryptionBlockFields = Object.keys(cardData)
.map(key => `card.${key}:${encoder.encode(cardData[key]).length}`)
.join(',');
const target = {
sourceType: "PaymentCard",
encryptionData: {
keyId: "79cd0553-9db5-4676-989b-f29edfbb6a51",
encryptionType: "RSA",
encryptionBlock: encryptionBlock,
encryptionBlockFields: encryptionBlockFields,
encryptionTarget: "MANUAL"
}
};
console.log(JSON.stringify({ target }, null, 2));
})();Use approved test card values only in non-production examples. Never use real cardholder data in local testing or documentation.
Submit the payout without onboarding
API: POST /v1/funds/transact
{
"target": {
"sourceType": "PaymentCard",
"encryptionData": {
"keyId": "79cd0553-9db5-4676-989b-f29edfbb6a51",
"encryptionType": "RSA",
"encryptionBlock": "{{encryptionBlock}}",
"encryptionBlockFields": "{{encryptionBlockFields}}",
"encryptionTarget": "MANUAL"
}
},
"payee": {
"type": "PERSONAL",
"partyRole": "RECIPIENT",
"platform": {
"merchantPartyId": "merch-party-id-1234567890"
},
"owners": [
{
"individual": {
"firstName": "MK",
"lastName": "TEST",
"email": "MK@test.com",
"dateOfBirth": "1990-03-22",
"phone": {
"countryCode": "1",
"phoneNumber": "404-555-5678",
"type": "MOBILE"
}
},
"address": {
"street": "123 main st",
"city": "Marietta",
"stateOrProvince": "GA",
"postalCode": "30062",
"country": "USA"
}
}
],
"email": "recipient.create1@test.com"
},
"merchantDetails": {
"terminalId": "10000001",
"merchantId": "100000000000001"
},
"transactionDetails": {
"operationType": "PAYOUT",
"merchantTransactionId": "3ce49dkbf3da499k03226da1827d1979"
},
"amount": {
"total": 111.99,
"currency": "USD"
}
}{
"gatewayResponse": {
"transactionType": "CREDIT",
"transactionState": "CAPTURED",
"transactionOrigin": "ECOM",
"transactionProcessingDetails": {
"transactionTimestamp": "2026-09-25T17:28:55.672572Z",
"apiTraceId": "b227364bab7b4f20960e2425093d79a7",
"clientRequestId": "7094988",
"merchantTransactionId": "3ce49dkbf3da499k03226da1827d1979",
"transactionId": "0200b227364bab7b4f20960e2425093d79a7",
"apiKey": "<masked-api-key>"
}
},
"paymentReceipt": {
"approvedAmount": {
"total": 111.99,
"currency": "USD"
},
"processorResponseDetails": {
"approvalStatus": "APPROVED",
"referenceNumber": "b227364bab7b4f20960e2425093d79a7",
"processor": "COMMERCEHUB_NATIVE",
"responseCode": "000",
"responseMessage": "Approved",
"localTimestamp": "2026-09-25T17:28:57.136113072Z"
}
},
"transactionDetails": {
"retrievalReferenceNumber": "b227364bab7b4f20960e2425093d79a7",
"operationType": "CREDIT"
},
"merchantDetails": {
"terminalId": "10000001",
"merchantId": "100000000000001"
}
}Receive the payout notification
Once the payout is submitted, configure the downstream notification flow for payout status changes. See Payout Events.
The two webhooks below are alternative terminal outcomes for the same payout request, not two notifications to expect for one successful payout. Both use the request's merchant ID (100000000000001), recipient role (RECIPIENT), caller-owned recipient reference (merch-party-id-1234567890), and the apiTraceId/transactionId returned in its synchronous response. The submitted merchantTransactionId is 3ce49dkbf3da499k03226da1827d1979 in either case.
{
"gatewayResponse": {
"transactionType": "PAYOUT_COMPLETED",
"transactionState": "SUCCESS",
"transactionProcessingDetails": {
"transactionTimestamp": "2026-09-25T20:30:48Z",
"apiTraceId": "b227364bab7b4f20960e2425093d79a7",
"transactionId": "0200b227364bab7b4f20960e2425093d79a7"
}
},
"party": {
"partyRole": "RECIPIENT",
"platform": {
"merchantPartyId": "merch-party-id-1234567890"
}
},
"merchantDetails": {
"merchantId": "100000000000001",
"terminalId": "10000001"
}
}{
"gatewayResponse": {
"transactionType": "PAYOUT_FAILED",
"transactionState": "FAILED",
"transactionProcessingDetails": {
"transactionTimestamp": "2026-09-25T20:28:11Z",
"apiTraceId": "b227364bab7b4f20960e2425093d79a7",
"transactionId": "0200b227364bab7b4f20960e2425093d79a7"
}
},
"party": {
"partyRole": "RECIPIENT",
"platform": {
"merchantPartyId": "merch-party-id-1234567890"
}
},
"merchantDetails": {
"merchantId": "100000000000001",
"terminalId": "10000001"
}
}On receipt, authenticate the notification using the approved webhook contract, then use gatewayResponse.transactionProcessingDetails.apiTraceId or transactionId to locate the saved payout response and its request merchantTransactionId.
Encryption Validation Checklist
-
keyIdand public key came from the same generate-key response. - Public key is imported as SPKI with RSA-OAEP SHA-256.
- Concatenation order exactly matches descriptor order.
- Ciphertext is Base64 encoded and within the API size limit.
-
encryptionTargetmatches the card-entry method. - No raw PAN or security code is logged or persisted.
- The target environment and merchant are entitled to the key and payout operation.
Field Reference
Top-Level Fields
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
target | Yes | Object | { "sourceType": "PaymentCard", "encryptionData": {...} } | Destination funding method for the payout. |
payee | Yes | Object | { "type": "PERSONAL", "partyRole": "RECIPIENT", ... } | Recipient data for the disbursement request. |
merchantDetails | Yes | Object | { "terminalId": "10000001", "merchantId": "100000000000001" } | Usually provided by the caller or test harness. |
transactionDetails | Yes | Object | { "operationType": "PAYOUT", "merchantTransactionId": "3ce49dkbf3da499k03226da1827d1979" } | Contains payout operation metadata. |
amount | Yes | Object | { "total": 111.99, "currency": "USD" } | Variable amount. Up to 2 decimal places. |
target Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
target.sourceType | Yes | String | PaymentCard | Identifies the destination rail / source type used for the payout target. |
target.encryptionData | Yes | Object | { "keyId": "79cd0553-9db5-4676-989b-f29edfbb6a51", "encryptionType": "RSA", ... } | Required for encrypted card payloads. |
target.encryptionData.keyId | Yes | String | 79cd0553-9db5-4676-989b-f29edfbb6a51 | Encryption key identifier provided by Commerce Hub or the gateway environment. |
target.encryptionData.encryptionType | Yes | String | RSA | Encryption algorithm or type used for the payload. |
target.encryptionData.encryptionBlock | Yes | String | {{encryptionBlock}} | Encrypted payload block generated by the caller. |
target.encryptionData.encryptionBlockFields | Yes | String | {{encryptionBlockFields}} | Field mapping used to build the encrypted block. |
target.encryptionData.encryptionTarget | Yes | String | MANUAL | Indicates the encryption target flow. |
payee Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
payee.type | Yes | String | PERSONAL | Fixed for this payload. Do not change. |
payee.partyRole | Yes | String | RECIPIENT | Fixed for this payload. Do not change. |
payee.platform.merchantPartyId | Yes | String | merch-party-id-1234567890 | Caller-configured unique identifier for the recipient. |
payee.owners | Yes | Array | [{ "individual": { ... }, "address": { ... } }] | Owner details are required. Include at least one owner record. |
payee.email | Recommended | String | recipient.create1@test.com | Useful for recipient communication and reconciliation. |
payee.owners[] Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
payee.owners[].individual.firstName | Yes | String | MK | Owner first name. |
payee.owners[].individual.lastName | Yes | String | TEST | Owner last name. |
payee.owners[].individual.email | Yes | String | MK@test.com | Owner email address. |
payee.owners[].individual.dateOfBirth | Yes | Date | 1990-03-22 | Use ISO date format. |
payee.owners[].individual.phone.countryCode | Yes | String | 1 | Country dialing code. |
payee.owners[].individual.phone.phoneNumber | Yes | String | 404-555-5678 | Owner phone number. |
payee.owners[].individual.phone.type | Yes | String | MOBILE | Phone type. |
payee.owners[].address.street | Yes | String | 123 Main St | Street address line. |
payee.owners[].address.city | Yes | String | Marietta | City. |
payee.owners[].address.stateOrProvince | Yes | String | GA | State or province. |
payee.owners[].address.postalCode | Yes | String | 30062 | Postal or ZIP code. |
payee.owners[].address.country | Yes | String | USA | Country code or country name, depending on environment conventions. |
merchantDetails Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
merchantDetails.merchantId | Yes | String | 100000000000001 | Merchant identifier used for payout submission. |
merchantDetails.terminalId | Yes | String | 10000001 | Terminal or origin identifier. |
transactionDetails Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
transactionDetails.operationType | Yes | String | PAYOUT | Fixed for this flow. Do not change. |
transactionDetails.merchantTransactionId | Yes | String | 3ce49dkbf3da499k03226da1827d1979 | Unique identifier for this transaction. Reuse on retry only if the intent is the same. |
amount Object
| Field Path | Required | Type | Example | Developer Notes |
|---|---|---|---|---|
amount.total | Yes | Number | 111.99 | Variable payout amount. Up to 2 decimals. |
amount.currency | Yes | String | USD | ISO currency code. |
What to Configure as the Caller
The caller is responsible for maintaining a stable mapping between the business recipient and the request payload.
| Caller-Owned Value | Purpose | Example |
|---|---|---|
merchantPartyId | Unique business identifier for the recipient | merch-party-id-1234567890 |
merchantTransactionId | Unique payout transaction identifier | 3ce49dkbf3da499k03226da1827d1979 |
merchantId / terminalId | Merchant context for testing and routing | 100000000000001 / 10000001 |
Important Implementation Notes
- Send
merchantPartyIdin this request. The recipient is tracked usingpayee.platform.merchantPartyId. - Use a unique
merchantTransactionIdper payout intent. Every payout request must have its own transaction identifier. If you retry the same payout after a timeout, reuse the same ID so the platform can deduplicate it. - Validate amount precision. The amount may vary, but it should always be formatted to 2 decimal places, for example
111.99,240.75, or12.30. - Require owner data. The
ownersarray is mandatory. Include complete personal and address details for the recipient owner. - Use the documented encryption contract for card payloads. Populate
encryptionDatainstead of sending raw card details, keep encryption values aligned with the gateway environment configuration, and make sureencryptionBlockFieldsmatches the exact fields used to createencryptionBlock.
Summary
Straight-through disbursement is designed for payout flows where the caller manages the recipient using merchantPartyId. To implement it correctly:
- Keep
payee.typeandpayee.partyRolefixed. - Provide owner information and merchant context.
- Use a unique
merchantTransactionId. - Submit
operationType=PAYOUT.
For other ways to pay out funds, see Payout Models and Payout Instructions.