> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clinkbill.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Decryption

> Use the decryption example to read payment credentials and fill the merchant card form.

Use this page when `get_credential` returns `credential.key` and `credential.payload`. Contact Clink offline to configure your platform's public key first, as described in [platform integration](/guides/agent/platform_integration), then decrypt with the matching private key.

If the response already contains `credential.card`, continue directly with [Browser Pay](/guides/agent/browser_pay). No decryption is needed.

## Decrypt the credential

Copy the function from the expandable example below. Pass your private key and the unchanged `key` and `payload` values from the same response. The returned `body` contains payment information and identifiers for result reporting.

```python theme={null}
body = decrypt_credential(
    private_key_pem,
    credential["key"],
    credential["payload"],
)
```

<Accordion title="Complete Python decryption example">
  Use Python 3.10+ and install the dependency:

  ```bash theme={null}
  python -m pip install cryptography
  ```

  `private_key_pem` accepts unencrypted RSA PEM private-key bytes. Adapt key loading to your existing platform implementation. The function raises an error without returning payment data if decryption fails.

  ```python theme={null}
  import base64
  import binascii
  import json
  from typing import Any

  from cryptography.exceptions import InvalidTag, UnsupportedAlgorithm
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM


  def _decode_base64(value: str) -> bytes:
      if not isinstance(value, str):
          raise TypeError("Expected a Base64 string")
      return base64.b64decode("".join(value.split()), validate=True)


  def decrypt_credential(
      private_key_pem: bytes, key: str, payload: str
  ) -> dict[str, Any]:
      try:
          private_key = serialization.load_pem_private_key(
              private_key_pem, password=None
          )
          if not isinstance(private_key, rsa.RSAPrivateKey):
              raise ValueError("Expected an RSA private key")

          aes_key_text = private_key.decrypt(
              _decode_base64(key),
              padding.OAEP(
                  mgf=padding.MGF1(algorithm=hashes.SHA1()),
                  algorithm=hashes.SHA256(),
                  label=b"",
              ),
          )
          aes_key = _decode_base64(aes_key_text.decode("ascii"))
          if len(aes_key) != 32:
              raise ValueError("Expected a 32-byte AES key")

          envelope = _decode_base64(payload)
          if len(envelope) <= 12 + 16:
              raise ValueError("Invalid payload length")
          iv = envelope[:12]
          ciphertext_and_tag = envelope[12:]
          plaintext = AESGCM(aes_key).decrypt(iv, ciphertext_and_tag, None)
          body = json.loads(plaintext.decode("utf-8"))
          if not isinstance(body, dict):
              raise ValueError("Expected a JSON object")
          return body
      except (
          ValueError,
          TypeError,
          binascii.Error,
          InvalidTag,
          UnsupportedAlgorithm,
      ):
          raise ValueError("Credential decryption failed") from None
  ```
</Accordion>

## Read the payment information

Use the fields actually present in `body`:

| Field | Usage |
| :- | :- |
| `orderId` | Pass to `confirm_result.orderId`. |
| `paymentExecutionDetailId` | Pass to `confirm_result.paymentExecutionDetailId`. |
| `amount`, `currencyCode` | Check the amount and currency for this checkout. |
| `cardholderName`, `billingAddress` | When provided, use for the corresponding merchant fields. `billingAddress` may be a JSON string that needs parsing first. |
| `status` | Credential issuance status; it does not establish merchant payment success. |

Also keep the outer `operation_id` from the `get_credential` response for `confirm_result.operationId`.

<Note>
  The decrypted `amount` uses major units, such as USD 12.50. `get_credential.amount` and `confirm_result.paidAmount` use minor units, which would be `1250`. Convert the actual paid amount using the currency's precision; do not copy the decrypted amount directly.
</Note>

## Fill the card fields

When the decrypted `body` contains `signedPayload`, use the helper below to read its card fields. Copy the function from the expandable example, then call it:

```python theme={null}
dynamic = read_dynamic_data(body["signedPayload"])
```

<Accordion title="Card field-reading example">
  Uses the imports from the decryption example above. This function only reads fields from `signedPayload`; it does not verify the JWT signature.

  ```python theme={null}
  def read_dynamic_data(signed_payload: str) -> dict[str, Any]:
      # Decodes JWT claims; does not verify the JWT signature.
      try:
          parts = signed_payload.split(".")
          if len(parts) != 3:
              raise ValueError("Expected a JWT")
          claims_bytes = base64.b64decode(
              parts[1] + "=" * (-len(parts[1]) % 4),
              altchars=b"-_",
              validate=True,
          )
          claims = json.loads(claims_bytes.decode("utf-8"))
          dynamic_data = claims["dynamicData"][0]
          if not isinstance(dynamic_data, dict):
              raise ValueError("Expected dynamicData")
          return dynamic_data
      except (ValueError, TypeError, KeyError, IndexError, binascii.Error):
          raise ValueError("Invalid credential claims") from None
  ```
</Accordion>

| Field inside `dynamic` | Merchant checkout usage |
| :- | :- |
| `token.paymentToken` | Card number |
| `token.tokenExpirationMonth` | Expiry month, padded to two digits |
| `token.tokenExpirationYear` | Expiry year; use the last two digits if the form expects `MM/YY` |
| `dynamicDataValue` | Security code |
| `dynamicDataExpiration` | Credential expiry time in Unix seconds |

Check that the card number, security code, and expiry fields are present before submitting, and use the credential before it expires. Continue with [Browser Pay](/guides/agent/browser_pay) to submit the payment and report the result.

## Handle decryption failures

If decryption fails, check that the private key matches the public key configured for this platform and environment, and that `key` and `payload` come from the same complete response. If it still fails or payment fields are missing, stop before submitting and contact Clink to investigate.

If a credential was issued but the payment was never submitted, call `confirm_result` with the available order and operation identifiers and report `NOT_SUBMITTED`. Use `OTHER` if supplying a `failureReason`. If identifiers are unavailable or you need another credential, follow [Browser Pay recovery](/guides/agent/browser_pay).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.