juspay / juspay/grace

Connector integration for bank debit in cybversource

Open
#145 1 comment 0 reactions 0 assignees View on GitHub
connector-integration
Dominant language
Python
Stars
4
Forks
6
PR merge metrics
No merged PRs in 30d

Description

# Cybersource Technical Specification — BankDebit (ACH)

## Overview

This specification covers the integration of Cybersource's bank-debit payment rails through the Cybersource REST API (commonly referred to as "Payments REST", or `pts/v2`). Cybersource markets US bank-account debits as **eCheck** / **ACH** (Automated Clearing House) and exposes them as a payment type alongside cards on the same `/pts/v2/payments` family of resources. In Cybersource's model an ACH transaction is a single-step **debit** (Sale): there is no separate Authorize / Capture for eCheck — submitting `paymentInformation.paymentType.name = "check"` on the Process-a-Payment call initiates the ACH debit directly. This spec documents the supported flows (Authorize-as-Sale, Refund / stand-alone Credit, Void, PSync), the request and response schemas relevant to bank-account payments, the SEC-code authorization model, the HTTP-Signature authentication scheme, error reporting, and the webhook channel for asynchronous status updates. Where Cybersource exposes SEPA / BACS direct-debits, those rails live under a different product (`/docs/sepa/...` Direct Debits) with a mandate-based lifecycle and are summarised at the end; the primary contract for `BankDebit` in Hyperswitch / UCS is the US-ACH eCheck path on `pts/v2`.

## Authentication

Cybersource's REST API uses **HTTP Signature** authentication (HMAC-SHA256). Each request is signed using three credentials issued from the merchant's Business Center:

- **Merchant ID** — sent as the `v-c-merchant-id` header. Identifies the merchant account that owns the shared secret.
- **Key ID** (a.k.a. serial number) — sent inside the `Signature` header's `keyid="…"` parameter. Identifies which shared-secret key was used.
- **Shared Secret Key** — Base64-encoded HMAC key. Used to compute the signature value; never transmitted on the wire.

### Signed headers

For a `POST` request the signature MUST cover, in order:

1. `host` — `api.cybersource.com` (Production), `apitest.cybersource.com` (Sandbox), or `api.in.cybersource.com` (India).
2. `date` — RFC 7231 GMT date string (e.g. `Wed, 23 Jul 2025 11:30:42 GMT`).
3. `(request-target)` — pseudo-header of the form `post /pts/v2/payments`.
4. `digest` — `SHA-256=` of the JSON request body. Required for `POST`/`PUT`/`PATCH`. Omitted for `GET`.
5. `v-c-merchant-id` — duplicates the header value into the signature.

The fully assembled `Signature` header looks like:

```
Signature: keyid="08c94330-…", algorithm="HmacSHA256",
headers="host date (request-target) digest v-c-merchant-id",
signature=""
```

### JWT alternative

Cybersource also exposes **OAuth 2.0 / JWT** for select integrations (notably webhook subscriptions and the Token Management Service), backed by a P12 certificate uploaded in the Business Center. JWT is not the canonical auth path for `pts/v2/payments` and is not surfaced in the eCheck developer guide; HTTP Signature is the supported scheme for ACH transactions.

## Endpoints

All paths are appended to the environment host:

- Production: `https://api.cybersource.com`
- Sandbox: `https://apitest.cybersource.com`

| Flow | HTTP | Path | ACH Behaviour |
|-----------|------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| Authorize | POST | `/pts/v2/payments` | For `paymentType.name = "check"` this is a **Sale / debit** — funds are pulled in a single step. Returns `status = PENDING` while the ACH batch settles. |
| Capture | POST | `/pts/v2/payments/{id}/captures` | **Not applicable to ACH.** ACH eCheck is auto-captured at the moment the debit is submitted; there is no separate capture call. The endpoint exists for card flows on the same connector. |
| Refund | POST | `/pts/v2/payments/{id}/refunds` | Follow-on refund against a previously settled eCheck debit. Returns `status = PENDING`. |
| Credit | POST | `/pts/v2/credits` | **Stand-alone credit** (refund without a referenced original payment) — used when the original debit ID is not available. |
| Void | POST | `/pts/v2/payments/{id}/voids`
`/pts/v2/credits/{id}/voids`
`/pts/v2/captures/{id}/voids` | Cancels a payment/credit/capture **before settlement**. Once the ACH batch has been transmitted to the ODFI, the operation must be done as a refund instead. Returns `status = VOIDED`. |
| PSync | GET | `/tss/v2/transactions/{id}` | Transaction Details API — retrieves the latest decision/status for any payment, credit, refund or void. |
| RSync | GET | `/tss/v2/transactions/{id}` | Same endpoint as PSync; the refund/credit ID is supplied as `{id}`. |
| Webhooks | POST | `/notification-subscriptions/v2/webhooks` | Subscription management. Receivers handle `POST` deliveries from Cybersource at the merchant-supplied URL. |

> Note: HTTP 404 on `/tss/v2/transactions/{id}` is expected immediately after submission. The developer guide instructs clients to retry the lookup after ~5 minutes, up to 5 times, because Transaction Search is populated asynchronously.

## Request Schemas

### Authorize (eCheck Debit) — `POST /pts/v2/payments`

The minimum payload for an ACH Sale:

```json
{
"clientReferenceInformation": {
"code": "TC123"
},
"processingInformation": {
"commerceIndicator": "internet",
"bankTransferOptions": {
"secCode": "WEB"
}
},
"orderInformation": {
"amountDetails": {
"currency": "USD",
"totalAmount": "12.00"
},
"billTo": {
"firstName": "Jane",
"lastName": "Doe",
"address1": "112 12 Ave",
"locality": "Bellevue",
"administrativeArea": "WA",
"postalCode": "98004",
"country": "US",
"email": "test@example.com",
"phoneNumber":"2053040804"
}
},
"paymentInformation": {
"paymentType": { "name": "check" },
"bank": {
"routingNumber": "071923284",
"account": {
"number": "12345678901234567",
"type": "C"
}
}
}
}
```

### Key fields — `paymentInformation.bank`

| Field | Type | Length / Format | Description |
|---------------------------------------------|--------|--------------------------------|------------------------------------------------------------------------------------------------------|
| `paymentInformation.paymentType.name` | string | constant `"check"` | Selects the eCheck/ACH rail. Without this the request is treated as a card transaction. |
| `paymentInformation.bank.routingNumber` | string | 9 digits (ABA) | US bank routing/transit number. Validated against a checksum. |
| `paymentInformation.bank.account.number` | string | 1–17 numeric | Customer's bank account number. PCI-equivalent; must be transmitted only over the signed REST call. |
| `paymentInformation.bank.account.type` | enum | `"C"`,`"S"`,`"X"` | `C` = consumer checking, `S` = savings, `X` = corporate / business checking. Maps to ACH SEC code. |
| `paymentInformation.bank.account.checkNumber` | string | up to 8 | Optional check serial — used for source-document audit on PPD/CCD. |

### Key fields — `processingInformation`

| Field | Type | Allowed values | Notes |
|--------------------------------------------------------|--------|---------------------------------------------|----------------------------------------------------------------------------------------------------|
| `processingInformation.commerceIndicator` | enum | `internet` (default), `moto`, `recurring` | Indicates channel; influences default SEC code and risk treatment. |
| `processingInformation.bankTransferOptions.secCode` | enum | `PPD`,`CCD`,`WEB`,`TEL` | NACHA Standard Entry Class — see SEC-code section below. Lower-case strings are also accepted. |
| `processingInformation.bankTransferOptions.fraudScreeningLevel` | enum | `1`,`2` | Optional eCheck fraud screening tier (TeleCheck verification). |
| `processingInformation.bankTransferOptions.partialPaymentId` | string | up to 25 | Used when chaining multiple debits to the same authorization (recurring). |

#### SEC codes

NACHA classifies every ACH entry by a 3-letter Standard Entry Class. Cybersource forwards whichever code the merchant supplies in `bankTransferOptions.secCode`:

- **PPD** — Prepaid Personal — written / signed authorization (paper or scanned). Used for recurring consumer debits.
- **CCD** — Corporate Credit / Debit — used when the customer is a business entity (corporate checking, account type `X`).
- **WEB** — Internet-Initiated — the most common code for Hyperswitch's online checkout flow. Requires authentication of the customer at the time of authorization (NACHA WEB rule).
- **TEL** — Telephone-Initiated — verbal authorization recorded or repeated back during a phone interaction.

The eCheck developer guide is explicit that "identity authentication and payment authorization must occur simultaneously" — the WEB SEC code in particular requires the merchant to capture both a clear authorization statement (amount, frequency, revocation) and identity proof (login alone is not sufficient).

### Refund — `POST /pts/v2/payments/{id}/refunds`

Body mirrors the Authorize request but only requires `clientReferenceInformation.code` and `orderInformation.amountDetails.totalAmount`. Bank details are inherited from the original `{id}`.

### Stand-Alone Credit — `POST /pts/v2/credits`

Used when the original payment is unknown or settled outside Cybersource. Must include the full `paymentInformation.bank` block again, plus `orderInformation.billTo` and `orderInformation.amountDetails`.

### Void — `POST /pts/v2/payments/{id}/voids` (or `/credits/{id}/voids`, `/captures/{id}/voids`)

Body is essentially empty:

```json
{ "clientReferenceInformation": { "code": "TC123-void" } }
```

### PSync / RSync — `GET /tss/v2/transactions/{id}`

No body. The `{id}` is the `id` returned by the original `/payments`, `/credits`, `/refunds` or `/voids` response.

## Response Schemas

### Authorize / Sale response

```json
{
"id": "16681201582270123456789",
"reconciliationId": "9PK9Y6A8A75018I",
"submitTimeUtc": "2026-04-29T10:42:38Z",
"status": "PENDING",
"clientReferenceInformation": { "code": "TC123" },
"orderInformation": {
"amountDetails": { "currency": "USD", "totalAmount": "12.00" }
},
"_links": {
"self": { "method": "GET", "href": "/pts/v2/payments/16681201582270123456789" },
"void": { "method": "POST", "href": "/pts/v2/payments/16681201582270123456789/voids" },
"refund": { "method": "POST", "href": "/pts/v2/payments/16681201582270123456789/refunds" }
}
}
```

### Status values

ACH responses use a small status vocabulary distinct from cards:

| Status | Meaning |
|---------------------|------------------------------------------------------------------------------------------------------------|
| `PENDING` | Accepted by Cybersource, queued for the next ACH batch (or in the holding period before settlement). |
| `AUTHORIZED` | (Cards only.) Not produced by eCheck. |
| `SETTLED` | Funds have cleared the ACH network and been deposited. |
| `VOIDED` | Cancelled before settlement. |
| `DECLINED` | Synchronous reject (e.g. routing-number checksum failure, fraud-screening reject). |
| `INVALID_REQUEST` | Field-level validation error. |
| `REVERSED` | Used when an ACH return is processed against a previously settled debit. |

### Refund / Credit / Void responses

Same envelope shape — `id`, `status`, `submitTimeUtc`, `clientReferenceInformation`, `_links.self`, `_links.void` (where applicable), plus `voidAmountDetails` on void responses and `refundAmountDetails` on refund responses.

### PSync (Transaction Details API) response

`GET /tss/v2/transactions/{id}` returns the persisted record:

```json
{
"id": "16681201582270123456789",
"applicationInformation": { "reasonCode": "100", "status": "PENDING" },
"paymentInformation": {
"bank": { "account": { "type": "C" }, "routingNumber": "07192****" },
"paymentType": { "type": "Bank Transfer", "method": "ECP" }
},
"processingInformation": {
"bankTransferOptions": { "secCode": "WEB" }
},
"orderInformation": {
"amountDetails": { "totalAmount": "12.00", "currency": "USD" }
},
"submitTimeUtc": "2026-04-29T10:42:38Z"
}
```

HTTP `200` is the success path; `404` means the record has not been indexed yet (retry after 5 minutes, up to 5 attempts); `500` is an unexpected server error.

## Error Handling

Cybersource reports errors via HTTP status code plus a structured JSON body.

| HTTP | `status` field | Typical cause |
|------|-------------------------|----------------------------------------------------------------------------|
| 400 | `INVALID_REQUEST` | Malformed JSON, missing required field, invalid SEC code, bad routing #. |
| 401 | (none, plain body) | HTTP-Signature mismatch or expired key. |
| 403 | `DECLINED` | Merchant not enrolled for eCheck or SEC code not permitted. |
| 404 | (transaction lookup) | `id` not yet indexed (PSync) or unknown. |
| 409 | `DUPLICATE_REQUEST` | Same `clientReferenceInformation.code` resubmitted within idempotency window. |
| 422 | `DECISION_DECLINED` | Risk / fraud-screening rejected the debit. |
| 502/503 | `SERVER_ERROR` | Upstream gateway issue; safe to retry idempotently. |

The error body follows this shape:

```json
{
"submitTimeUtc": "2026-04-29T10:42:38Z",
"status": "INVALID_REQUEST",
"reason": "MISSING_FIELD",
"message": "Declined - The request is missing one or more fields",
"details": [
{ "field": "paymentInformation.bank.routingNumber", "reason": "MISSING_FIELD" }
]
}
```

The full reason-code catalogue lives at `developer.cybersource.com/api/reference/response-codes.html`. For ACH specifically, three-digit `reasonCode` values include `100` (success / accepted), `101` (missing field), `102` (invalid field), `230` (fraud screening declined), `476` (customer authentication failed), and `481` (decision-manager review).

## Webhooks / Notifications

Webhook subscriptions are managed via `POST /notification-subscriptions/v2/webhooks`. The subscriber registers a delivery URL, optionally a health-check URL, and chooses `securityType: "KEY"` (mutual-TLS + signed payload) or `securityType: "oAuth"` (with a P12 certificate for JWT).

### Delivery payload

Every notification is delivered as `POST {merchant-url}` with:

- Header `v-c-signature: t=;keyId=;sig=`
- Header `v-c-correlation-id` for tracing
- JSON body with `eventType`, `eventDate`, `payload` (event-specific)

Validation: concatenate `t` + `.` + raw body, HMAC-SHA256 with the digital-signature key fetched from `/kms/egress/v2/keys-sym/{keyId}`, compare Base64 to `sig`. Replay protection is the responsibility of the receiver via the `t` timestamp.

### Event types relevant to ACH

Cybersource exposes a generic Payments-product event family that covers eCheck:

- `payments.payments.updated` — a payment record changed status (`PENDING` -> `SETTLED` etc.)
- `payments.refund.status.updated` — a refund moved status.
- `payments.capture.status.accepted` — fired only for card captures, not ACH.
- `payments.authorization.status.accepted` — fired only for card authorizations.

> **Important caveat documented by Cybersource:** eCheck **returns**, **chargebacks** and standalone **refunds** are not currently surfaced as webhook events — receivers must poll the Transaction Details API or rely on the daily reconciliation report to detect ACH returns (NSF, account-closed, etc.). This is a known gap in the eCheck product.

### Retry policy

Default delivery retries: 3 attempts, first retry 1 minute after failure, fixed 1-minute interval (arithmetic progression). Configurable per subscription via `numberOfRetries`, `firstRetry`, `interval`, `repeatSequenceCount`, `repeatSequenceWaitTime`, and `deactivateFlag`.

### Encryption

Payment events are subject to mandatory **message-level encryption (MLE)** using JSON Web Encryption (AES-GCM 256-bit symmetric or RSA-OAEP 2048-bit asymmetric). The merchant uploads an X.509 certificate via the KMS endpoint and decrypts incoming `payload` blocks with the matching private key.

## SEPA / BACS — Adjacent rails (not part of `pts/v2`)

Cybersource also offers SEPA Direct Debit and BACS Direct Debit, but these are **not** exposed through the `paymentInformation.bank` block on `/pts/v2/payments`. They live in the Direct-Debits product family (`developer.cybersource.com/docs/sepa/...`) and require a **mandate-first** lifecycle:

1. Create / import a mandate (the customer's signed authorisation), receive a `mandateId`.
2. Verify the mandate is in `Active` state.
3. Submit `Process a Direct Debit Sale` referencing the `mandateId`.
4. Status transitions are batched (BACS uses the 3-day Bacs cycle; SEPA uses SEPA Core / B2B settlement).
5. Reversals, refunds and chargebacks each have dedicated endpoints.

The Direct-Debits product is documented under the Simple Order API in the public references; a REST mapping exists for newer onboardings but is not surfaced in the same `pts/v2` shape as eCheck. For UCS `BankDebit` flows, ACH (US) is the primary path; SEPA / BACS would require a second adapter against the Direct-Debits product.

## References

- https://developer.cybersource.com/docs/cybs/en-us/echeck/user/all/rest/echeck-user-guide/echeck-txnprocess-overview.html
- https://developer.cybersource.com/docs/cybs/en-us/echeck/user/all/rest/echeck-user-guide/echeck-api-intro/echeck-api-debit-intro.html
- https://developer.cybersource.com/docs/cybs/en-us/echeck/user/all/rest/echeck-user-guide/echeck-txnprocess-overview/echeck-txnprocess-pmtauth.html
- https://developer.cybersource.com/library/documentation/dev_guides/REST_API/Getting_Started/html/REST_GS/ch_authentication.5.3.htm
- https://developer.cybersource.com/docs/cybs/en-us/payments/developer/cybsach/rest/payments/payments-processing-basic-intro/payments-processing-basic-credit-intro/payments-processing-basic-credit-ex-rest.html
- https://developer.cybersource.com/docs/cybs/en-us/payments/developer/ctv/rest/payments/payments-processing-basic-intro/payments-processing-basic-void-intro.html
- https://developer.cybersource.com/docs/cybs/en-us/webhooks/implementation/all/rest/webhooks.html
- https://developer.cybersource.com/api-reference-assets/index.html

Contributor guide

No contributing guide indexed for this repository

Research direction

No implementation files, tests, or repository entry points are named. Start by locating the existing payment connector entry points in the Python repository, then compare their flows with the Cybersource /pts/v2/payments, refunds, credits, voids, and /tss/v2/transactions endpoints described here; done means the BankDebit integration supports the specified flows.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, payments
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.