[Tech Spec] Braintree — ZeroDollarAuth
- Dominant language
- Python
- Stars
- 4
- Forks
- 6
- PR merge metrics
- No merged PRs in 30d
Description
# Braintree — ZeroDollarAuth
**Complexity:** low
**Generated by:** Grace pipeline run `run-2026-05-13T15-18-07-758Z-b92d20`
## Summary
Implement ZeroDollarAuth payment method for Braintree connector. Technical Specification: Braintree — ZeroDollarAuth (SetupMandate)
## Scope
# Technical Specification: Braintree — ZeroDollarAuth (SetupMandate)
## 1. Connector Profile
- **Connector Name**: Braintree (a PayPal service)
- **Primary Flow Scope**: ZeroDollarAuth (SetupMandate) — card-on-file mandate establishment via zero-amount authorization with automatic vaulting; used as the Customer-Initiated Transaction (CIT) that seeds a `connector_mandate_id` for future Merchant-Initiated Transactions (MIT / RepeatPayment).
- **API Family**: GraphQL over HTTPS — single endpoint for all operations; all requests use HTTP POST with a JSON body containing `query` (GraphQL mutation/query string) and `variables`.
- **API Version**: `2019-01-01` (required via `Braintree-Version` request header).
- **Hosts**:
| Environment | Base URL |
|-------------|----------|
| Production | `https://payments.braintree-api.com/graphql` |
| Sandbox | `https://payments.sandbox.braintree-api.com/graphql` |
The URL is stored in the `base_url` connector config field and is environment-specific; there are no regional sub-endpoints.
---
## 2. Authentication
- **Scheme**: HTTP Basic Authentication (RFC 7617).
- **Credentials Required**:
| Field | Config Key | Notes |
|-------|-----------|-------|
| API Public Key | `public_key` | Used as Basic-auth username |
| API Private Key | `private_key` | Used as Basic-auth password |
| Merchant Account ID | `merchant_account_id` | Identifies the sub-account receiving funds; sent in `transaction.merchantAccountId` in every mutation |
| Merchant Config Currency | `merchant_config_currency` | Currency the merchant account is configured for; validated at request time against the payment currency |
- **Implementation Notes**:
1. Concatenate `{public_key}:{private_key}`, Base64-encode the result, and set `Authorization: Basic {encoded}` on every request.
2. Also set `Braintree-Version: 2019-01-01` and `Content-Type: application/json` on every request.
3. No token rotation, OAuth, or expiry — credentials are static
## Out of Scope
Not specified in techspec
## Technical Constraints
- Follow existing connector patterns in the codebase
---
## Full Tech Spec
# Technical Specification: Braintree — ZeroDollarAuth (SetupMandate)
## 1. Connector Profile
- **Connector Name**: Braintree (a PayPal service)
- **Primary Flow Scope**: ZeroDollarAuth (SetupMandate) — card-on-file mandate establishment via zero-amount authorization with automatic vaulting; used as the Customer-Initiated Transaction (CIT) that seeds a `connector_mandate_id` for future Merchant-Initiated Transactions (MIT / RepeatPayment).
- **API Family**: GraphQL over HTTPS — single endpoint for all operations; all requests use HTTP POST with a JSON body containing `query` (GraphQL mutation/query string) and `variables`.
- **API Version**: `2019-01-01` (required via `Braintree-Version` request header).
- **Hosts**:
| Environment | Base URL |
|-------------|----------|
| Production | `https://payments.braintree-api.com/graphql` |
| Sandbox | `https://payments.sandbox.braintree-api.com/graphql` |
The URL is stored in the `base_url` connector config field and is environment-specific; there are no regional sub-endpoints.
---
## 2. Authentication
- **Scheme**: HTTP Basic Authentication (RFC 7617).
- **Credentials Required**:
| Field | Config Key | Notes |
|-------|-----------|-------|
| API Public Key | `public_key` | Used as Basic-auth username |
| API Private Key | `private_key` | Used as Basic-auth password |
| Merchant Account ID | `merchant_account_id` | Identifies the sub-account receiving funds; sent in `transaction.merchantAccountId` in every mutation |
| Merchant Config Currency | `merchant_config_currency` | Currency the merchant account is configured for; validated at request time against the payment currency |
- **Implementation Notes**:
1. Concatenate `{public_key}:{private_key}`, Base64-encode the result, and set `Authorization: Basic {encoded}` on every request.
2. Also set `Braintree-Version: 2019-01-01` and `Content-Type: application/json` on every request.
3. No token rotation, OAuth, or expiry — credentials are static per environment.
4. The UCS implementation builds the auth header in `Braintree::get_auth_header` (`braintree.rs`) using `BASE64_ENGINE.encode(format!("{public_key}:{private_key}"))`.
---
## 3. Supported Flows
| Flow | HTTP | Path | Notes |
|------|------|------|-------|
| **ZeroDollarAuth (SetupMandate)** | POST | `/graphql` | `authorizeCreditCard` mutation with `amount: "0.00"` + `vaultPaymentMethodAfterTransacting: {when: ALWAYS}`; `transaction.paymentMethod.id` in response becomes `connector_mandate_id` |
| Authorize (Card) | POST | `/graphql` | `authorizeCreditCard` or `chargeCreditCard` mutation depending on `capture_method` |
| Authorize (Wallet) | POST | `/graphql` | `authorizePaymentMethod` / `chargePaymentMethod` for ApplePay, GooglePay, PayPal SDK tokens |
| Capture | POST | `/graphql` | `captureTransaction` mutation; requires `transactionId` + `amount` |
| Void | POST | `/graphql` | `reverseTransaction` mutation |
| Refund | POST | `/graphql` | `refundTransaction` mutation |
| PSync | POST | `/graphql` | `search { transactions(input: {id: {is: ...}}) }` query |
| RSync | POST | `/graphql` | `search { refunds(input: ..., first: 1) }` query |
| Tokenize (PaymentMethodToken) | POST | `/graphql` | `tokenizeCreditCard` mutation; prerequisite step to obtain `paymentMethodId` for ZeroDollarAuth |
| ClientAuthenticationToken | POST | `/graphql` | `createClientToken` mutation; seeds 3DS / Drop-in UI / SDK sessions |
| Webhooks (inbound) | POST | merchant endpoint | Braintree pushes signed events; verified via HMAC of `bt-payload` |
---
## 4. Request Schema Highlights
### 4a. Prerequisite — Tokenize Card (`tokenizeCreditCard`)
Before ZeroDollarAuth, the raw card must be tokenized to produce a `paymentMethodId`.
```json
{
"query": "mutation tokenizeCreditCard($input: TokenizeCreditCardInput!) { tokenizeCreditCard(input: $input) { clientMutationId paymentMethod { id } } }",
"variables": {
"input": {
"creditCard": {
"number": "4111111111111111",
"expirationYear": "2030",
"expirationMonth": "03",
"cvv": "737",
"cardholderName": "Jane Doe"
}
}
}
}
```
### 4b. ZeroDollarAuth — `authorizeCreditCard` with Vault
```json
{
"query": "mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }",
"variables": {
"input": {
"paymentMethodId": "",
"transaction": {
"amount": "0.00",
"merchantAccountId": "",
"channel": "HyperSwitchBT_Ecom",
"orderId": "",
"vaultPaymentMethodAfterTransacting": { "when": "ALWAYS" }
}
}
}
}
```
**Field Reference**:
| Field | Type | Required | Notes |
|-------|------|:--------:|-------|
| `query` | String | ✓ | GraphQL mutation string (hardcoded constant `AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION`) |
| `variables.input.paymentMethodId` | `Secret` | ✓ | Card token from prior `tokenizeCreditCard` call |
| `variables.input.transaction.amount` | `StringMajorUnit` | ✓ | `"0.00"` for zero-dollar verification; Braintree uses string decimal (NOT minor integer units) |
| `variables.input.transaction.merchantAccountId` | `Secret` | ✓ | From `BraintreeAuthType.merchant_account_id` |
| `variables.input.transaction.channel` | String | ✓ | Always `"HyperSwitchBT_Ecom"` (`constants::CHANNEL_CODE`) |
| `variables.input.transaction.orderId` | String | ✓ | `connector_request_reference_id` from `ResourceCommonData` |
| `variables.input.transaction.vaultPaymentMethodAfterTransacting` | Object | ✓ for mandate | `{"when": "ALWAYS"}` — causes Braintree to persist the card and return `paymentMethod.id` |
| `variables.input.transaction.customerDetails.email` | String | Optional | Billing email; binds vault entry to a customer record |
| `options.storeInVaultOnSuccess` | Boolean | Optional | Legacy REST flag; not used in GraphQL path |
**Idempotency**: No dedicated idempotency header in Braintree GraphQL. Use `orderId` for deduplication; a duplicate `orderId` within the same merchant account will surface a `DUPLICATE_TRANSACTION` gateway rejection.
**Currency Validation**: The UCS connector validates that `request.currency` matches `merchant_config_currency` from auth config before building the request (`validate_currency` call).
---
## 5. Response Schema Highlights
### 5a. Successful ZeroDollarAuth Response
```json
{
"data": {
"authorizeCreditCard": {
"transaction": {
"id": "dHJhbnNhY3Rpb246dHJhbnNhY3Rpb246QUJDREVGR0g=",
"status": "AUTHORIZED",
"createdAt": "2024-07-20T14:22:05.000Z",
"paymentMethod": {
"id": "cGF5bWVudG1ldGhvZDpwYXltZW50bWV0aG9kOlhZWlo="
}
}
}
}
}
```
| Field | Type | Notes |
|-------|------|-------|
| `data.authorizeCreditCard.transaction.id` | String | Connector transaction ID (opaque Base64 global ID); stored as `ConnectorTransactionId` |
| `data.authorizeCreditCard.transaction.status` | Enum | See §5b status table |
| `data.authorizeCreditCard.transaction.createdAt` | ISO 8601 datetime | Transaction creation timestamp |
| `data.authorizeCreditCard.transaction.paymentMethod.id` | `Secret` | **Critical** — returned only when `vaultPaymentMethodAfterTransacting.when = ALWAYS`; becomes `MandateReference.connector_mandate_id` for future MIT calls |
### 5b. Transaction Status Values
| Braintree Status | UCS `AttemptStatus` | Meaning |
|-----------------|---------------------|---------|
| `AUTHORIZED` | `Authorized` | Auth hold placed; card valid |
| `AUTHORIZING` | `Authorizing` | Authorization in progress |
| `AUTHORIZED_EXPIRED` | `AuthorizationFailed` | Auth hold expired (7–30 days depending on card network) |
| `SUBMITTED_FOR_SETTLEMENT` | `Charged` | Capture queued |
| `SETTLING` | `Charged` | Settlement in progress |
| `SETTLED` | `Charged` | Funds transferred |
| `SETTLEMENT_CONFIRMED` | `Charged` | Settlement confirmed |
| `SETTLEMENT_PENDING` | `Charged` | Settlement delayed (e.g., bank holiday) |
| `FAILED` | `Failure` | Generic failure |
| `PROCESSOR_DECLINED` | `Failure` | Issuer / processor declined |
| `GATEWAY_REJECTED` | `Failure` | Braintree risk/fraud rules triggered |
| `SETTLEMENT_DECLINED` | `Failure` | Settlement refused by processor |
| `VOIDED` | `Voided` | Transaction reversed before settlement |
### 5c. Mandate Reference Extraction
```rust
mandate_reference: transaction_data.payment_method.as_ref().map(|pm| {
Box::new(MandateReference {
connector_mandate_id: Some(pm.id.clone().expose()),
payment_method_id: None,
connector_mandate_request_reference_id: None,
})
})
```
The `paymentMethod.id` is the vaulted card token. Future MIT flows (`RepeatPayment`) pass this as `paymentMethodId` in the `chargeCreditCard` / `authorizeCreditCard` mutation.
---
## 6. Error Handling
Braintree returns HTTP 200 for most logical/validation errors; errors appear in the top-level `errors` GraphQL array. HTTP non-200 codes indicate infrastructure or authentication failures.
### GraphQL Error Response Shape
```json
{
"errors": [
{
"message": "Amount is required.",
"extensions": {
"legacyCode": "91507",
"errorClass": "VALIDATION",
"inputPath": ["input", "transaction", "amount"]
}
}
]
}
```
### Error Table
| HTTP | `extensions.legacyCode` | `errorClass` | Cause |
|------|------------------------|-------------|-------|
| 200 | `91507` | VALIDATION | Amount is required |
| 200 | `81501` | VALIDATION | Amount cannot be negative |
| 200 | `81528` | VALIDATION | Card number is invalid |
| 200 | `81703` | VALIDATION | Credit card type not accepted by this merchant account |
| 200 | `91564` | VALIDATION | Merchant account does not support zero-amount transactions |
| 200 | `91570` | VALIDATION | Merchant account currency mismatch |
| 200 | `81604` | VALIDATION | Card expiration month is invalid |
| 200 | `81605` | VALIDATION | Card expiration year is invalid |
| 200 | `91002` | AUTHORIZATION | Merchant account not found or not authorized |
| 200 | (none) | — | `PROCESSOR_DECLINED` status in transaction — issuer declined |
| 200 | (none) | — | `GATEWAY_REJECTED` status — Braintree fraud/AVS/CVV rules |
| 401 | — | — | Invalid API credentials (public_key/private_key) |
| 403 | — | — | Insufficient permissions for the requested operation |
| 422 | — | — | Malformed GraphQL query or missing required variable |
| 429 | — | — | Rate limit exceeded |
| 500 | — | — | Braintree internal server error |
**UCS Error Extraction Logic** (`build_error_response`):
- `error_code` ← `errors[0].extensions.legacyCode` (falls back to `NO_ERROR_CODE`)
- `error_message` ← `errors[0].message` (falls back to `NO_ERROR_MESSAGE`)
- `reason` ← concatenation of all error messages
---
## 7. Webhooks / Async Notifications
- **Subscription**: Configured in the Braintree Control Panel under Settings → Webhooks. Sandbox supports test pings only; production allows full event subscriptions.
- **Delivery Format**: HTTP POST to the merchant-configured URL with body `bt-signature={sig}&bt-payload={base64_xml_payload}` (URL-encoded form data).
- **Verification**: Parse `bt-payload` as Base64-decoded XML; verify `bt-signature` = HMAC-SHA1(`private_key`, `bt-payload`). Reject events that fail verification.
- **Event Types Relevant to ZeroDollarAuth / SetupMandate**:
| Event | Meaning |
|-------|--------|
| `transaction_settled` | Transaction moved to SETTLED |
| `transaction_settlement_declined` | Settlement was declined |
| `check` | Test delivery (Braintree health-check ping) |
- **Retry Policy**: Braintree retries on non-2xx responses with exponential backoff; up to ~50 attempts over 3 days. Idempotency key: `notification.kind` + `notification.timestamp` + merchant account ID.
- **Documented Gaps**:
- There is no dedicated `setup_mandate` or `verify_payment_method` webhook event type; the result of a ZeroDollarAuth is determined synchronously from the `authorizeCreditCard` response.
- In UCS field probe (`data/field_probe/braintree.json`), the `setup_recurring`, `proxy_setup_recurring`, and `token_setup_recurring` flows all report `not_implemented` — this spec targets their initial implementation.
- Braintree's GraphQL API does not support a standalone `verifyPaymentMethod` mutation for card verification without a transaction; the zero-dollar `authorizeCreditCard` with vault is the canonical approach.
---
## 8. References
- Braintree GraphQL API Reference: https://graphql.braintreepayments.com/reference/
- Braintree Transaction Statuses: https://developer.paypal.com/braintree/docs/reference/general/statuses
- Braintree Vault Guide: https://developer.paypal.com/braintree/docs/guides/payment-methods/
- Braintree Webhooks: https://developer.paypal.com/braintree/docs/guides/webhooks/overview
- Braintree Error Codes: https://developer.paypal.com/braintree/docs/reference/general/validation-errors/overview
- UCS Braintree connector source: `crates/integrations/connector-integration/src/connectors/braintree/`
- UCS Braintree transformers: `crates/integrations/connector-integration/src/connectors/braintree/transformers.rs`
- UCS SetupMandate pattern guide: `grace/rulesbook/codegen/guides/patterns/pattern_setup_mandate.md`
- UCS field probe data: `data/field_probe/braintree.json`
- UCS connector docs: `docs-generated/connectors/braintree.md`
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading the existing Braintree connector patterns in braintree.rs, including Braintree::get_auth_header, validate_currency, and constants::CHANNEL_CODE. Trace how ZeroDollarAuth requests and mandate references are represented, then implement the specified authorizeCreditCard flow and verify that the vaulted paymentMethod.id is returned for future MIT calls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- graphql, rust
- Domain
- api, payments
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100