Before you can use the features described in this documentation to integrate with Meta Pay, you must request access from Meta. If you satisfy the prerequisites, email Meta-Pay-Partnerships@meta.com to request access.

Payment Processing in Meta Pay

The documentation in this section describes how to process payments with Meta Pay.

After a user successfully completes checkout using Meta Pay JS SDK a container will be returned to the integrating merchant. A container in Meta Pay represents a payment instrument such as a credit card or other payment method. Merchants are not responsible for processing the container and should reach out to their payment partner for details on how to pass FBPaymentContainer for processing.

The information below is intended for payment partners.

Container Structure

For details of the Meta Pay container structure, see FBPaymentContainer. The following is an example of a Meta Pay container:

{
  mode: "TEST",
  amount:
  { 
    currency: "USD",
    value: 757
  }
  container_id: 'cGF5bWVudF9jb250YWl...',
  container_data: 'eyJ4NWMiO ...'
}

Container Data

Container data contains the payment instrument that can be used to process the user's payment. The contents of this field will be encrypted using the public key of the payment partner for whom it is intended. See container encryption section below for more information about encryption. After decryption container data is a JSON that is guaranteed to contain a container_type field. This field will be used to identify the type of container that was returned. The other fields in container data may differ depending on the container type.

{
  container_type: 'basic-card-v1',
  ...
}

Container types

Meta Pay uses many factors to determine which container type to return for a given payment. The returned type is guaranteed to come from the list of supported container types specified in the Meta Pay JS SDK FBPaymentConfiguration. See FBPayContainerType for a list of all supported container types.

See the following for details about processing each container type:

Container Context

Container context is an opaque ID that uniquely identifies the context that the container is intended for. Create a container context for each payment transaction when you initiate a payment request.

Verify the container context before you process each payment to prevent misuse of containers. For example, if the container context value is a customer ID, verify that the payment request for the same customer ID.

Container Encryption

To ensure security in transit, Meta encrypts container data by using the partner's public key before sending it to the merchant. You exchange keys with Meta as part of the onboarding process.

The following are the encryption steps:

  1. The container data is serialized into a JSON string.
  2. The JSON string is wrapped into a JWE (RFC7516) object by using the ECDH-ES algorithm (against the partner's public key, identified by the RFC7638 thumbprint in the kid field) and the A256GCM (aes256-gcm) encryption algorithm, using compact serialization.
  3. The resulting compact serialized JWE is wrapped into a JWS (RFC7515), signed with ES256 (ecdsa-sha256) using a certificate which chains up (RFC5280) to Meta's trust root, using compact serialization.

Example

The following is an example of an encrypted and signed container data payload:

eyJ4NWMiOlsiTUlJQ1JqQ0NBUzZnQXdJQkFnSUJBVEFOQmdrcWhraUc5dzBCQVFzRkFEQWZNUjB3R3dZRFZRUUREQlJHUWxCaGVTQjBaWE4wSUcxdlpHVWdjbTl2ZERBZUZ3MHlNREEzTURjeE9EQTFOVE5hRncweU5EQXpNRFV4T0RBMU5UTmFNQ2N4SlRBakJnTlZCQU1NSEVaQ1VHRjVJSFJsYzNRZ2JXOWtaU0JwYm5SbGNtMWxaR2xoZEdVd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTMVpxM0swclh6aWFacDEwRmE3MDAvZWNEb3RQRHd3N3V1ajhFZnZFSDdIdHRrQmtNQU1ycUJJMjFFUERxTmxlK2xpaFNHSWw1ZkZxNGZRYWIrTGdpb28xQXdUakFkQmdOVkhRNEVGZ1FVdnBaNmtNSldTZmdhcXlIaEY3aVBTelRESzV3d0h3WURWUjBqQkJnd0ZvQVVLbHl0R2VwYWNwZ011MFdmcjNhbE53ZWJ0dzB3REFZRFZSMFRCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFCWDNmYTBycXFlNmNFeUoxeXVGS2RPZFcvSnpFU09JSWo3WDkzRkFiNEZUUWsrQlcwQVVCdnJwei9idk9WKzhBVjFBUEllRDNFWld0czlaMGNNdjdZdXF0SktUaFd2R0JPc21QQnRIZURlNjZBeFlVOUkzcWhJdUNFcmU3Y2ZWeDVuSm9DcVVyY1B2aGJFbDd5K1BDaGFZdTBSV0VyVnV5OVhPT2NjN1JDZjdLcWFwTmdBQzhSNmdmOVNsUms4OEozRVFPaEowenRBM0ZuSC9ZRzFlM0RVa1JLa2pvMkZHZ3EyakRZTkdOc3BWenRpQXl2QVVqMnk3UUxJMytDclZGL3VlR0J0LzJnSG12ZThMS2s2OXlEKzN3ZGVEQi9YVG1xQVlxaWljQVlSTjAxMURObEgyMHg1aExGYXJhTkZEZ0ZvZlhlRDdHZWtwZk95ZGlaSHhjc0E9PSJdLCJhbGciOiJFUzI1NiJ9.eyJjb250YWluZXJfaWQiOiJjR0Y1YldWdWRGOWpiMjUwWVdsdVpBWEk2TVRJek5EVTJOemhmWDAxRlVrTklRVTVVWDFSRlUxUmZSVEpGWDE5UVUxQmZWRVZUVkY4eCIsIm1vZGUiOiJURVNUIiwiYW1vdW50Ijp7InZhbHVlIjozMjIzLCJjdXJyZW5jeSI6IlVTRCJ9LCJjb250YWluZXJfZGF0YSI6ImV5SmxibU1pT2lKQk1qVTJSME5OSWl3aVlXeG5Jam9pUlVORVNDMUZVeUlzSW10cFpDSTZJa0l4VWw5aFkwVmhhVVZaY3pWTE1IcHRZWGRNVTJoWlVtdGpkSEJKY0ZWVldGRTFkRE5EYVZBMlREQWlMQ0psY0dzaU9uc2lhM1I1SWpvaVJVTWlMQ0pqY25ZaU9pSlFMVEkxTmlJc0luZ2lPaUpHY0RNeE9YVTFOMjFRVVVkbWMyWmFTbFJQVnpoeVoyTklWVzlVZEd3NExUaE5jbFpzWVVnelYyZzRJaXdpZVNJNkltUlJhbmRVYmpWMGJHdEZZMjFPWjFkSFdEVmpSbEZzWkdob2VsaGpNM2xFZURsUGIwSnVlRGcwVm1zaWZYMC4uUGlEaThTbVhPVjNaRXNVUy5oQ0pLcnYwOUFGZEpRRE9lX0hNeEpCZXNuSHV2Ny00MXFGaE8ySTU3a1prVXhsSVJxV0Fia2ZOT2d0SlIxNEMyZ01qNTBERFMyYnFoV3dmV3hTa1RlS3c4bUhIcWtmVzRLSUQ1QlhfcjFSSkstWlBwcnF6OTlBY3dySTJYbFJ5aG1VaHd3Yno3cktDbWVxUUZnUVVuLXlHdGl4cGRiTGl5UmM3czVBSUtPeG1PNkwwTXhXdUVzMjFKZ2xBZGtXSzdWanJ1M3FYYVVFdjFRUzdlWV9OWWxYUnJiUDhNM2JWQ2ZlM2lFZTJwa05IcEtTckRxU1VJY3JnMjM3Z2VPOTJPbjJKRHhUUGkybkhublNZcjI4aVZFQkhBa0ZDRjAxX2VnTVlmeVhKMVFLZ0NBVmVrM2lTdjBNU3ZVeHJRWnVoWm5jQ0s5MEgtdF9kWVdrZl9SLXctSjVWOWc0SVRSMm1NYnZCQWZkcUl5aXlJb1FUak1nWTdGQjdaOGVFZTBSeDZIWkVwNnN0aWlSRGZEQVk0MG1seFdEeWJ3LUswZUtnMGtRODB6eUpKQzJoakpXZTAwSUF4WlI4Rk1QOU4yOG5lc29pR25QbUtqNFRFQkhEcUtYb1NlTWV6cUVjSHpuTFZhdVhpODBzdlotS0Fjc1oxNVQwSmpBcXp6ZnE0V09idGpQM3FnOVNkdXN3TWIyU2FBbWZzVncxOXZiTWtVcUtZRXc5bGlOUi00Q2w5UVR3Y2tnSDFuMlRUTEdNejA0XzNuWVAtYktZLkE2dmdxdGZhVUMwSU9pNXZUSy1LVHcifQ.Jj4v0XNdwdsZ42E_zA2qEdZix2DWyoBZpZT2ehUoLvGRV5NGDFUg7OU_9y91fCGQNjH5MKFiym1qoTC_Rcz1hw

The following is an example of the container data payload Base64 and JSON decoded for clarity, presented in JavaScript syntax:

base64url(json({
 x5c: [
   'MIICRjCCAS6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRGQlBheSB0ZXN0IG1vZGUgcm9vdDAeFw0yMDA3MDcxODA1NTNaFw0yNDAzMDUxODA1NTNaMCcxJTAjBgNVBAMMHEZCUGF5IHRlc3QgbW9kZSBpbnRlcm1lZGlhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS1Zq3K0rXziaZp10Fa700/ecDotPDww7uuj8EfvEH7HttkBkMAMrqBI21EPDqNle+lihSGIl5fFq4fQab+Lgioo1AwTjAdBgNVHQ4EFgQUvpZ6kMJWSfgaqyHhF7iPSzTDK5wwHwYDVR0jBBgwFoAUKlytGepacpgMu0Wfr3alNwebtw0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABX3fa0rqqe6cEyJ1yuFKdOdW/JzESOIIj7X93FAb4FTQk+BW0AUBvrpz/bvOV+8AV1APIeD3EZWts9Z0cMv7YuqtJKThWvGBOsmPBtHeDe66AxYU9I3qhIuCEre7cfVx5nJoCqUrcPvhbEl7y+PChaYu0RWErVuy9XOOcc7RCf7KqapNgAC8R6gf9SlRk88J3EQOhJ0ztA3FnH/YG1e3DUkRKkjo2FGgq2jDYNGNspVztiAyvAUj2y7QLI3+CrVF/ueGBt/2gHmve8LKk69yD+3wdeDB/XTmqAYqiicAYRN011DNlH20x5hLFaraNFDgFofXeD7GekpfOydiZHxcsA==',
 ],
 alg: 'ES256',
})) + // Protected Header
'.' + base64url(json({
 container_id: 'cGF5bWVudF9jb250YWluZAXI6MTIzNDU2NzhfX01FUkNIQU5UX1RFU1RfRTJFX19QU1BfVEVTVF8x',
 mode: 'TEST',
 amount: {
   value: 3223,
   currency: 'USD',
 },
 container_data: base64url(json({
     enc: 'A256GCM',
     alg: 'ECDH-ES',
     kid: 'B1R_acEaiEYs5K0zmawLShYRkctpIpUUXQ5t3CiP6L0',
     epk: {
       kty: 'EC',
       crv: 'P-256',
       x: 'Fp319u57mPQGfsfZJTOW8rgcHUoTtl8-8MrVlaH3Wh8',
       y: 'dQjwTn5tlkEcmNgWGX5cFQldhhzXc3yDx9OoBnx84Vk',
     },
   })) + // Protected Header
   '.' + '' + // Encrypted Key is empty when using ECDH-ES
   '.' + 'PiDi8SmXOV3ZEsUS' + // Initialization Vector
   '.' + 'hCJKrv09AFdJQDOe_HMxJBesnHuv7-41qFhO2I57kZkUxlIRqWAbkfNOgtJR14C2gMj50DDS2bqhWwfWxSkTeKw8mHHqkfW4KID5BX_r1RJK-ZPprqz99AcwrI2XlRyhmUhwwbz7rKCmeqQFgQUn-yGtixpdbLiyRc7s5AIKOxmO6L0MxWuEs21JglAdkWK7Vjru3qXaUEv1QS7eY_NYlXRrbP8M3bVCfe3iEe2pkNHpKSrDqSUIcrg237geO92On2JDxTPi2nHnnSYr28iVEBHAkFCF01_egMYfyXJ1QKgCAVek3iSv0MSvUxrQZuhZncCK90H-t_dYWkf_R-w-J5V9g4ITR2mMbvBAfdqIyiyIoQTjMgY7FB7Z8eEe0Rx6HZEp6stiiRDfDAY40mlxWDybw-K0eKg0kQ80zyJJC2hjJWe00IAxZR8FMP9N28nesoiGnPmKj4TEBHDqKXoSeMezqEcHznLVauXi80svZ-KAcsZ15T0JjAqzzfq4WObtjP3qg9SduswMb2SaAmfsVw19vbMkUqKYEw9liNR-4Cl9QTwckgH1n2TTLGMz04_3nYP-bKY' + // Ciphertext
   '.' + 'A6vgqtfaUC0IOi5vTK-KTw' // Authentication Tag
})) + // Payload
'.' + 'Jj4v0XNdwdsZ42E_zA2qEdZix2DWyoBZpZT2ehUoLvGRV5NGDFUg7OU_9y91fCGQNjH5MKFiym1qoTC_Rcz1hw' // Signature

Container Decryption

The payload in the above example can be decrypted with the following private key which corresponds to kid B1R_acEaiEYs5K0zmawLShYRkctpIpUUXQ5t3CiP6L0:

-----BEGIN EC PRIVATE KEY-----
MGICAQEEC++break+things+oAoGCCqGSM49AwEHoUQDQgAEt9vlzj8MootxUFNb
fBL2eRU9YPMJNlgjLS2byDWzIHSr/g5At16XE+9oPndHYVLoYvlMKrXfTyyAnIRC
+u9C4w==
-----END EC PRIVATE KEY-----

Below is sample code demonstrating how to verify and decrypt an encrypted Meta Pay container (in JavaScript/TypeScript/Node and leveraging node-jose). partnerEncryptionKeyPem is your EC keypair. fbpayTestRootCertPem is the Meta Pay Test Root Certificate used to sign the public key in JWS x5c header of the container. Please note that LIVE containers use a separate root certificate.

import {JWK, JWE, JWS} from 'node-jose';
import partnerEncryptionKeyPem from './test_keys/encryption.key';
import fbpayTestRootCertPem from './test_keys/test_mode_root.crt';

import {X509Certificate} from 'crypto';


export async function decryptTestContainer(container_jws: string) {
  const partnerEncryptionKey = await JWK.asKey(partnerEncryptionKeyPem, 'pem');
  const unverifiedPayload = JSON.parse(base64decode(container_jws.split('.')[1]));
  const jweHeader = JSON.parse(base64decode(unverifiedPayload.container_data.split('.')[0]));

  if (jweHeader.kid === partnerEncryptionKey.kid) {
    const verifiedPayload = await verifyTestContainer(container_jws);
    const payload = JSON.parse(verifiedPayload);
    const decryptResult = await JWE.createDecrypt(partnerEncryptionKey, {
      algorithms: ['ECDH-ES', 'A256GCM'],
    }).decrypt(payload.container_data);

    return {
      ...payload,
      container_data: JSON.parse(decryptResult.plaintext.toString()),
    };
  }

  throw new Error(`Decryption not available for key id ${jweHeader.kid}`);
}

export async function verifyTestContainer(container_jws: string) {
    // verify container signature using x5c leaf key
    const verifySignatureResult = await JWS.createVerify(undefined, {
      algorithms: ['ES256'],
    }).verify(container_jws, {allowEmbeddedKey: true});
  
  
    if(typeof X509Certificate != 'undefined') {
      const rootX509 = new X509Certificate(fbpayTestRootCertPem);
      const x5c = (verifySignatureResult.header as any).x5c;

      if(x5c.length !== 1) {
        throw new Error("This util only supports single certificate in x5c header. Found " + x5c.length);
      }
      // x5c to PEM
      const embeddedX509 = new X509Certificate(Buffer.from(x5c[0], 'base64'));

      // verify x5c chains to root cert
      let isChainVerified = embeddedX509.verify(rootX509.publicKey);

      if(!isChainVerified) {
        console.error("Certificate chain is invalid. x5c = \n", x5c, "\nroot = \n", rootX509.toString);
        throw new Error("Certificate chain is invalid");
      }
      // TODO: CRL verification as defined in RFC5280
    } else {
      throw new Error("Crypto module does not support X509Certificate in browser.");
    }

    return verifySignatureResult.payload.toString();
}

function base64decode(encoded: string): string {
  return Buffer.from(encoded, 'base64').toString();
}

Code samples in other languages demonstrating how to decrypt an encrypted container are available on Meta's GitHub repo.

Container signature

The outermost format of the container is JWS (RFC7515). The signature of the payload must be verified using the certificate provided in JWS header x5c parameter as specified in RFC7515. Additionally it must be verified that the certificate specified in JWS header x5c parameter chains up (RFC5280) to the root certificate specified below.

Meta Pay Signature Root Certificate

For Test mode containers

-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRGQlBh
eSB0ZXN0IG1vZGUgcm9vdDAeFw0yMDA3MDcxODA1NTNaFw0yNDAzMDUxODA1NTNa
MB8xHTAbBgNVBAMMFEZCUGF5IHRlc3QgbW9kZSByb290MIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA19GMMJH28jIQYE946FmhklmoQAlfaKbV56+f61gW
3tTxAn0OI9cY/ZLJUWS/hiaJJmfrCbRBb1BsoOd2xvgOcmqwLlvZUIn7rWwTbd4k
SHojYw01T17h9zcxGcyPsvEhtDxWkP0LpJZhZQDZkJHG2uEXnZ3RaQTmKxY40j5I
/wOnOO/zwRddYnDPnLAAExlKKLPnlMNReTGTXske4lX8HSrYMJBK7zydJN87CUOl
pyWqSvuMdTuhownNTtyhmkwm2TnsGmchxmHlL5dg6mKm469LVuQlFhdtULvHippQ
a5KTZYjOZtrKgQGds3ygR8IkmCHf/gUJTRJhp0Ny8eMThQIDAQABo1MwUTAdBgNV
HQ4EFgQUKlytGepacpgMu0Wfr3alNwebtw0wHwYDVR0jBBgwFoAUKlytGepacpgM
u0Wfr3alNwebtw0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
YpUYvYji97213msiXt47TCSdJC3BXtCoUg2o7wIEu0o9mD/Fo1yN8/7gvCMcXiEK
rp+DVzkJQHx8IRFcFaMnLKyysK4gATQz6J39z12E0Ld3yqj9vfryzgL0ALHnq5Ei
0oMV0Dgk1zU9gH3Ce2FH54n7bXZooCKEJF1S7qsiRiG77m1D58iDqGd6kuyDh1cr
fiZ29q3n1VyYBNihZF45Kxx3so+WyAXGeTq9nRKNCwhePVBAxaMTNEmINFE9NnNj
f2xRXuHrOIr6wxkZkqoN23v/OS9r+kiLz8GbXmll8upCDM4d/pw3yhOSMO8zo41g
C2AVRQVkwIfTaWGmgjI0DA==
-----END CERTIFICATE-----

For Live mode containers

-----BEGIN CERTIFICATE-----
MIIC8DCCApagAwIBAgIUbAy5P226baZmwNysloMirbU9D7owCgYIKoZIzj0EAwIw
eDEnMCUGA1UEAwweRmFjZWJvb2sgUGF5IENvbnRhaW5lciBSb290IENBMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTETMBEGA1UEBwwKTWVubG8gUGFy
azEWMBQGA1UECgwNRmFjZWJvb2sgSW5jLjAgFw0yMDEyMTEwMDA5NDNaGA8yMDUw
MTIxMTAwMDk0M1oweDEnMCUGA1UEAwweRmFjZWJvb2sgUGF5IENvbnRhaW5lciBS
b290IENBMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTETMBEGA1UE
BwwKTWVubG8gUGFyazEWMBQGA1UECgwNRmFjZWJvb2sgSW5jLjBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABDiGbVM1PQ2mbzXMbGvQQ/Bce/4XDiGnMZS2WGgW4RwT
UgWKvFoXOLl22Oeoc07Bl0jriqD8P29cP8gGn7QFThKjgfswgfgwDwYDVR0TAQH/
BAUwAwEB/zAdBgNVHQ4EFgQUldu9FgG+FMaFMAffgrHDwKw9p7swgbUGA1UdIwSB
rTCBqoAUldu9FgG+FMaFMAffgrHDwKw9p7uhfKR6MHgxJzAlBgNVBAMMHkZhY2Vi
b29rIFBheSBDb250YWluZXIgUm9vdCBDQTELMAkGA1UEBhMCVVMxEzARBgNVBAgM
CkNhbGlmb3JuaWExEzARBgNVBAcMCk1lbmxvIFBhcmsxFjAUBgNVBAoMDUZhY2Vi
b29rIEluYy6CFGwMuT9tum2mZsDcrJaDIq21PQ+6MA4GA1UdDwEB/wQEAwIBhjAK
BggqhkjOPQQDAgNIADBFAiEAwULK+UPNPaa60FEYz9Av5MqVPl5QkyqqmSI1W6x7
DI8CIHFexZ8WaT4utSnqvgekEZ5DQM6lL2DwlynfJo6ZUSol
-----END CERTIFICATE-----