| Endpoints | GQL making_api_calls, Endpoints: https://payments.sandbox.braintree-api.com/graphql, https://payments.braintree-api.com/graphql; "You SHOULD use the POST method" | Config::TEST, Config::LIVE; every request is a POST |
| Authorization | GQL making_api_calls, API Keys: "you MUST Base64-encode it", Authorization: Basic base64(public_key:private_key); "vaulting and charging payment methods, require API Key authentication" | Api::call() |
| Braintree-Version | GQL making_api_calls: "You MUST provide a Braintree-Version header with a date in the format YYYY-MM-DD. We recommend using the date on which you begin integrating" | Config::API_VERSION = '2026-11-07' |
| Content-Type, body | GQL making_api_calls: "MUST be application/json", body {query, variables} | Api::call() |
| Responses and errors | GQL making_api_calls, Understanding Responses: always HTTP 200; errors[].extensions.errorClass one of AUTHENTICATION, AUTHORIZATION, INTERNAL, UNSUPPORTED_CLIENT, NOT_FOUND, NOT_IMPLEMENTED, RESOURCE_LIMIT, SERVICE_AVAILABILITY, VALIDATION; legacyCode; partial success possible; extensions.requestId | INTERNAL / SERVICE_AVAILABILITY / RESOURCE_LIMIT and transport errors are retryable (unclear); VALIDATION, NOT_FOUND are final; AUTHENTICATION / AUTHORIZATION = key message. A field with data counts even if errors is present for another field |
| Timeout | GQL making_api_calls, Timeouts: "We set a recommend setting a timeout of 60 seconds ... If ... less than 60 seconds ... the customer will be charged, but you will not receive any notification ... You will need to search for the transaction" | cURL timeout 60 s; on any unclear charge the transaction is searched by orderId |
| Output encoding | GQL making_api_calls, Important Security Requirement: "The Braintree API returns data exactly as it is provided ... You are responsible for properly encoding" | Every Braintree value shown in the admin is htmlspecialchars-escaped; history notes use Core::enc() |
query { ping } | GQL making_api_calls, Your First Request: answer {"data":{"ping":"pong"}} | Test connection |
| Merchant accounts | schema Viewer.merchant: Merchant, Merchant.merchantAccounts(input, first, after): MerchantAccountConnection, MerchantAccount { id currencyCode isDefault status } | Test connection stores accounts_<mode>; Config::accountFor() |
createClientToken | GQL collecting_payment_information, Requesting a Client Token; schema CreateClientTokenInput.clientToken: ClientTokenInput { merchantAccountId }; Docs 3d-secure step-by-step: "The merchant account ID used to create the client token must match the merchant account ID used to create the subsequent transaction or verification" | .prepare creates one per checkout with the merchant account of the charge |
| Nonce = payment method id | GQL collecting_payment_information: the nonce "is the ID of a single-use PaymentMethod ... Send the string to your payment integration with our GraphQL API in order to charge" | paymentMethodId of charge / authorize |
| Nonce read before the charge | schema node(id), PaymentMethod.details: PaymentMethodDetails, CreditCardDetails { brandCode last4 threeDSecure { authentication { liabilityShifted liabilityShiftPossible } } } | Api::paymentMethod(); "Require liability shift" refuses liabilityShiftPossible = true without liabilityShifted = true, or no 3DS data |
| 3DS required | Docs applying-3ds (php): "When you create a transaction with a 3D Secure enriched single-use-token, the options.threeDSecure.required parameter defaults to true ... We strongly recommend you allow this parameter to default to true" | The extension never sends required: false; Braintree's own default stays on, plus the server check above |
chargePaymentMethod | GQL transactions, Charging a Payment Method; schema ChargePaymentMethodInput { apiRequestKey paymentMethodId transaction: TransactionInput }, "A successful charge results in a transaction with the SUBMITTED_FOR_SETTLEMENT or SETTLING status"; schema: "Charge any payment method" | Payment action "Charge at once" (the GraphQL equivalent of submitForSettlement: true); used for cards and PayPal nonces |
authorizePaymentMethod | GQL transactions, Using Separate Authorization and Capture: result AUTHORIZED | Payment action "Authorize only" |
TransactionInput | schema: amount: Amount!, merchantAccountId, orderId, riskData: RiskDataInput { deviceData customerIp }, customerDetails: TransactionCustomerDetailsInput { email firstName lastName } | Amount from the order (server side), the account of the order currency, our orderId, Data Collector device data, customer IP and contact |
apiRequestKey | schema (on Charge/Authorize/Capture/Void/Refund inputs): "If subsequent requests are made with the same apiRequestKey, and the first request resulted in a transaction being created (in any status), then the same transaction in its current status will be returned ... must have the same input values ... before 30 days" | 32-hex key stored BEFORE the call (txn.request_key, op.request_key); a retry re-sends the identical input with the same key; resends stop after 24 h (RESEND_WINDOW) |
| Lookup after a timeout | schema Query.transactions(input: TransactionSearchInput!, first, after), TransactionSearchInput.orderId: SearchTextInput { is } (search is deprecated: "Use the individual top-level search queries") | Api::findByOrderRef(); our orderId is unique per attempt (Money::reference()) |
| Read-back | GQL node_query / transactions, Node Query; schema Transaction { id legacyId status orderId merchantAccountId amount { value currencyCode } paymentMethodSnapshot refunds statusHistory processorAuthorizationResponse } | Api::transaction() |
| Statuses | schema enum PaymentStatus (AUTHORIZATION_EXPIRED, AUTHORIZED, AUTHORIZING, FAILED, GATEWAY_REJECTED, PROCESSOR_DECLINED, SETTLED, SETTLEMENT_CONFIRMED, SETTLEMENT_DECLINED, SETTLEMENT_PENDING, SETTLING, SUBMITTED_FOR_SETTLEMENT, VOIDED); Docs statuses | Payments::derive(): AUTHORIZING and any unknown value stay pending (never cancelled); declines only from PROCESSOR_DECLINED / GATEWAY_REJECTED / FAILED |
| Decline reason | schema TransactionAuthorizationProcessorResponse { legacyCode message }, GatewayRejectedEvent.gatewayRejectionReason | Stored and shown to the admin; the customer sees a neutral text |
captureTransaction | GQL transactions; schema CaptureTransactionInput { apiRequestKey transactionId transaction: CaptureTransactionOptionsInput { amount } }; "You have a limited number of days to capture a transaction once it has been authorized" | Capture once, full or a lower amount; cron warns after 5 days |
| Partial captures | GQL transactions, Using Multiple Partial Capture: "This mutation is available for PayPal transactions" | Not used in 1.0 (one capture per authorization) |
voidTransaction | GQL transactions, Voiding a Transaction: "If a payment has been authorized or submitted for settlement but has not yet completed settlement, you can void it"; schema VoidTransactionPayload { clientMutationId transaction } | Void of AUTHORIZED / SUBMITTED_FOR_SETTLEMENT. The guide's sample selects an errors field that the schema does not have; we follow the schema |
refundTransaction | GQL transactions, Refunding a Transaction: refundable "in either the SETTLING or SETTLED status", "has not already been fully refunded"; "pass in an amount less than the original amount in order to partially refund"; "optionally specify an orderId to give the refund its own unique order ID"; schema RefundInput { amount orderId }, RefundTransactionPayload { refund } | Full and partial refunds of SETTLING / SETTLED (and SETTLEMENT_CONFIRMED) payments; each refund gets the orderId <txn orderId>-r<op id> so a lost answer is matched in Transaction.refunds |
| Refunded amount | schema Transaction.refunds: [Refund!], Refund { status amount orderId } | Sum of refunds not in FAILED / GATEWAY_REJECTED / PROCESSOR_DECLINED / VOIDED / SETTLEMENT_DECLINED |
| Amounts and currencies | schema scalar Amount (decimal string), MonetaryAmount { value currencyCode }; Docs currencies: supported list, zero-decimal currencies marked with an asterisk | Money: ISO 4217 List One exponents (tooling/release-templates/cscart/iso4217), Braintree list in Money::CURRENCIES; LAK left out (Braintree zero-decimal vs ISO 2); parse of returned amounts is exact (no float) |
| Client SDK loading | Docs client-sdk setup: https://js.braintreegateway.com/web/3.147.0/js/client.min.js and components | Config::jsBase() |
| Hosted Fields | Docs hosted-fields setup: braintree.hostedFields.create({client, styles, fields: {number, cvv, expirationDate}}), tokenize() -> payload.nonce, payload.details.bin | catalog/view/template/payment/kv_braintree.twig |
| 3D Secure 2 | Docs 3d-secure step-by-step (Hosted Fields): load three-d-secure; threeDSecure.create({version: 2, client}) ("Version 1 is deprecated"); verifyCard({amount, nonce, bin, email, billingAddress, collectDeviceData: true, onLookupComplete(data, next)}) ("It is required to implement the callback onLookupComplete"); names "ASCII-printable characters required" | Amount = the charge amount in the merchant account currency; billing fields sent only when ASCII |
| Device data | Docs premium fraud management tools (client side): braintree.dataCollector.create({client}) -> deviceData, passed with the transaction | TransactionInput.riskData.deviceData |
| PayPal | Docs paypal checkout-with-paypal (JS v3): paypalCheckout.create({client}), loadPayPalSDK({currency, intent}), paypal.Buttons({fundingSource: paypal.FUNDING.PAYPAL, createOrder: createPayment({flow: 'checkout', amount, currency, intent}), onApprove: tokenizePayment(data)}); "If intent is used, the value must match in both loadPayPalSDK and createPayment"; JS ref PayPalCheckout intents authorize, order, capture | intent capture for sale, authorize for authorize only; the nonce is charged with the same mutations as cards |
| Privacy notice | Docs hosted-fields overview, Notice of Collection: the merchant "must include the below languages in your own privacy notice, which must be presented to the buyers at or before the point of collection" | The text is shown under the payment form (text_privacy) |
| Webhook creation | Docs webhooks create: Control Panel > gear > API > Webhooks > Create New Webhook; "a valid HTTPS path ... publicly accessible" | Destination URL shown on the settings page; Config::webhookUrlOk() |
| Webhook POST | Docs webhooks parse: "The post body contains two x-www-form-urlencoded parameters: bt_signature, bt_payload"; kinds; retries "every hour for up to 3 hours in sandbox, or up to 24 hours in production, until the webhook responds with a successful HTTPS response code (i.e. '2xx') within 30 seconds" | .webhook reads the raw $_POST fields (OpenCart's request object HTML-encodes them); 200 when handled or not ours, 403 bad signature, 400 unreadable, 503 when the read-back failed |
| Webhook signature | SDK WebhookNotificationGateway::parse(): payload must not match /[^A-Za-z0-9+=\/\n]/; signature pairs split on &, then ` | , pair of our public key; valid if Digest::hexDigestSha1(privateKey, payload) or of payload . "\n" matches; Digest::_builtInHmacSha1 = hash_hmac('sha1', message, sha1(key, true))`; secure compare | Webhook::verify() (hash_equals); tried with the keys of both modes |
| Webhook payload | SDK: base64_decode(payload) -> XML <notification><timestamp/><kind/><subject>; WebhookTestingGateway sample XML for transaction_settled, transaction_settlement_declined, dispute_*, check | Webhook::parse() with LIBXML_NONET, no DOCTYPE / ENTITY |
| Kinds | Docs reference webhooks transaction: transaction_settled, transaction_settlement_declined ("Transaction webhooks are available for ACH and SEPA Direct Debit"); dispute: dispute_accepted, dispute_auto_accepted, dispute_disputed, dispute_expired, dispute_lost, dispute_opened, dispute_under_review, dispute_won; check (Control Panel "Check URL") | Disputes: history note + optional status; transaction kinds: read-back; card settlement is read back by the cron because card transactions get no settlement webhook |
| Test data | GQL testing; Docs drop-in overview ("test card numbers (e.g. 4111111111111111)") | docs/installation.md |