> For the complete documentation index, see [llms.txt](https://developer.branta.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.branta.pro/setup/platforms/custom-integration.md).

# Custom Integration

If none of the [no-code gateway options](/setup/platforms/payment-gateway-options.md) fit — you run your own checkout, a custom processor, or just want full control over the request — wire Branta in directly. There are two equivalent paths: the SDK (recommended) or a raw HTTP `POST`.

### Prerequisites

* You've completed [Platform onboarding](/setup/platforms.md) — account, approved platform, and an API key.
* You know which [environment](/tech/environments.md) you're hitting (`staging` while testing, `production` for live traffic).
* Your code can hold an API key securely — server-side only, never in a browser bundle or mobile binary.

### What "integrating" means

Every time your platform issues a payment destination (on-chain address, Lightning invoice, Lightning address, etc.) to a sender, `POST` it to Branta. From that moment, any wallet or scanner that looks up that destination will see your name and logo. The full request/response shape is documented under [Adding Payments](/tech/api/v2/adding-payments.md).

Two privacy postures:

* **Plain** — you send the destination as-is. Branta stores it and serves it back on lookup. Simplest, works for any [destination type](/tech/api/v2/adding-payments.md).
* **Zero-Knowledge** — your code encrypts the destination with a per-payment secret before posting. Branta only ever sees ciphertext; only senders with the secret (delivered via the QR code) can decrypt. See [Zero Knowledge](/tech/api/v2/adding-payments.md#zero-knowledge) for the algorithm. The SDKs handle this transparently when `isZk: true` is set on a destination.

### Option 1: Use the SDK (recommended)

The SDKs handle ZK encryption, secret generation, verify-URL building, and request shaping for you.

{% tabs %}
{% tab title="JavaScript" %}
SDK: [`@branta-ops/branta`](https://www.npmjs.com/package/@branta-ops/branta) ([source](https://github.com/BrantaOps/branta-js))

```ts
import { BrantaServerBaseUrl } from "@branta-ops/branta";
import { BrantaService } from "@branta-ops/branta/v2";
import { DestinationType } from "@branta-ops/branta";
import { PrivacyMode } from "@branta-ops/branta";

const service = new BrantaService({
  baseUrl: BrantaServerBaseUrl.Production,
  defaultApiKey: process.env.BRANTA_API_KEY!,
  privacy: PrivacyMode.Loose,
});

// Called whenever your platform issues a destination to a sender.
async function publishDestination(address: string) {
  const { payment, secret, verifyUrl } = await service.addPayment({
    description: "Invoice #12345",
    destinations: [
      {
        value: address,
        type: DestinationType.BitcoinAddress,
        isPrimary: true,
        isZk: true, // set false to publish in plain
      },
    ],
    ttl: 3600, // optional — seconds until Branta forgets this destination
    metadata: "order-12345", // optional — your own correlation id
  });

  // For ZK: persist `secret` alongside your order — you need it to build
  // the `branta_secret` query param when rendering the payment QR / link.
  // `verifyUrl` is the public Branta verify page for this payment.
  return { payment, secret, verifyUrl };
}
```

Privacy mode interacts with `isZk` on `addPayment`: in `strict` mode (the wallet default) all destinations must be ZK or the call throws. Strict is also the SDK default — platforms typically set `privacy` explicitly to `loose` and choose per-destination.
{% endtab %}

{% tab title=".NET" %}
SDK: [`Branta`](https://www.nuget.org/packages/Branta) ([source](https://github.com/BrantaOps/branta-dotnet))

```cs
using Branta.Classes;
using Branta.Enums;
using Branta.V2.Extensions;
using Branta.V2.Interfaces;
using Branta.V2.Models;

services.ConfigureBrantaServices(new BrantaClientOptions
{
    BaseUrl       = BrantaServerBaseUrl.Production,
    DefaultApiKey = configuration["Branta:ApiKey"],
    Privacy       = PrivacyMode.Loose,
});

public class CheckoutService(IBrantaService brantaService)
{
    public async Task<(Payment Payment, string Secret, string VerifyUrl)> PublishDestinationAsync(string address)
    {
        var payment = new Payment
        {
            Description = "Invoice #12345",
            Destinations =
            [
                new Destination
                {
                    Value     = address,
                    Type      = DestinationType.BitcoinAddress,
                    IsPrimary = true,
                    IsZk      = true, // set false to publish in plain
                }
            ],
            TTL      = 3600,         // optional
            Metadata = "order-12345" // optional
        };

        var (savedPayment, secret, verifyUrl) = await brantaService.AddPaymentAsync(payment);

        // For ZK: persist `secret` alongside your order — you need it to build
        // the `branta_secret` query param when rendering the payment QR / link.
        return (savedPayment, secret, verifyUrl);
    }
}
```

{% endtab %}

{% tab title="Python" %}
SDK: [`branta`](https://pypi.org/project/branta/) ([source](https://github.com/BrantaOps/branta-python))

```python
import os

from branta.enums import BrantaServerBaseUrl, DestinationType, PrivacyMode
from branta.options import BrantaClientOptions
from branta.v2 import BrantaService

service = BrantaService(BrantaClientOptions(
    base_url=BrantaServerBaseUrl.Production,
    default_api_key=os.environ["BRANTA_API_KEY"],
    privacy=PrivacyMode.Loose,
))

# Called whenever your platform issues a destination to a sender.
async def publish_destination(address: str):
    payment = (
        service.create_payment_builder()
        .add_destination(address, DestinationType.BitcoinAddress)
        .set_zk()  # omit .set_zk() to publish in plain
        .set_description("Invoice #12345")
        .add_metadata("order_id", "order-12345")
        .set_ttl(3600)
        .build()
    )

    result = await service.add_payment(payment)

    # For ZK: persist result.secret alongside your order — you need it to build
    # the `branta_secret` query param when rendering the payment QR / link.
    # result.verify_url is the public Branta verify page for this payment.
    return result
```

Privacy mode interacts with `set_zk()` on the builder: in `strict` mode (the wallet default) all destinations must be ZK or the call raises. Strict is also the SDK default — platforms typically set `privacy` explicitly to `loose` and choose per-destination.
{% endtab %}

{% tab title="Dart" %}
SDK: [`branta`](https://pub.dev/packages/branta) ([source](https://github.com/BrantaOps/branta-dart))

```dart
import 'dart:io' show Platform;

import 'package:branta/branta.dart';
import 'package:http/http.dart' as http;

final options = BrantaClientOptions(
  baseUrl: BrantaServerBaseUrl.production,
  defaultApiKey: Platform.environment['BRANTA_API_KEY']!,
  privacy: PrivacyMode.loose,
);
final brantaClient = BrantaClient(httpClient: http.Client(), defaultOptions: options);
final service = BrantaService(
  client: brantaClient,
  aesEncryption: AesEncryptionService(),
  defaultOptions: options,
);

// Called whenever your platform issues a destination to a sender.
Future<AddPaymentResult> publishDestination(String address) async {
  final payment = PaymentBuilder()
      .setDescription('Invoice #12345')
      .addDestination(address)
      .setZk() // omit .setZk() to publish in plain
      .setTtl(3600) // optional — seconds until Branta forgets this destination
      .addMetadata('order_id', 'order-12345') // optional — your own correlation id
      .build();

  final result = await service.addPaymentAsync(payment);

  // For ZK: persist result.secret alongside your order — you need it to build
  // the `branta_secret` query param when rendering the payment QR / link.
  // result.verifyUrl is the public Branta verify page for this payment.
  return result;
}
```

Privacy mode interacts with `setZk()` on destinations: in `strict` mode (the wallet default) all destinations must be ZK or the call throws. Strict is also the SDK default — platforms typically set `privacy` explicitly to `loose` and choose per-destination.
{% endtab %}

{% tab title="Kotlin" %}
SDK: [`pro.branta:branta`](https://central.sonatype.com/artifact/pro.branta/branta) ([source](https://github.com/BrantaOps/branta-kotlin))

```kotlin
val service = BrantaService(
    BrantaClientOptions(
        baseUrl = BrantaServerBaseUrl.Production,
        defaultApiKey = System.getenv("BRANTA_API_KEY"),
        privacy = PrivacyMode.Loose
    )
)

// Called whenever your platform issues a destination to a sender.
suspend fun publishDestination(address: String): AddPaymentResult {
    val payment = PaymentBuilder()
        .setDescription("Invoice #12345")
        .addDestination(address, DestinationType.BitcoinAddress)
        .setZk() // omit to publish in plain
        .setTtl(3600) // optional — seconds until Branta forgets this destination
        .addMetadata("order_id", "order-12345") // optional — your own correlation id
        .build()

    val result = service.addPayment(payment)

    // For ZK: persist `result.secret` alongside your order — you need it to build
    // the `branta_secret` query param when rendering the payment QR / link.
    // `result.verifyUrl` is the public Branta verify page for this payment.
    return result
}
```

Privacy mode interacts with `.setZk()` on the `PaymentBuilder`: in `Strict` mode (the wallet default) all destinations must be ZK or the call throws. `Strict` is also the SDK default — platforms typically set `privacy` explicitly to `Loose` and choose per-destination.
{% endtab %}

{% tab title="Rust" %}
SDK: [`branta`](https://crates.io/crates/branta) ([source](https://github.com/BrantaOps/branta-rust))

```rust
use std::env;

use branta::{
    AddPaymentResult, BrantaClientOptions, BrantaError, BrantaServerBaseUrl, BrantaService,
    DestinationType, PaymentBuilder, PrivacyMode,
};

let service = BrantaService::new(BrantaClientOptions {
    base_url: BrantaServerBaseUrl::Production,
    default_api_key: Some(env::var("BRANTA_API_KEY").expect("BRANTA_API_KEY")),
    privacy: PrivacyMode::Loose,
    hmac_secret: None,
});

// Called whenever your platform issues a destination to a sender.
async fn publish_destination(
    service: &BrantaService,
    address: &str,
) -> Result<AddPaymentResult, BrantaError> {
    let payment = PaymentBuilder::new()
        .add_destination(address, Some(DestinationType::BitcoinAddress))
        .set_zk() // omit .set_zk() to publish in plain
        .set_description("Invoice #12345")
        .add_metadata("order_id", "order-12345") // optional — your own correlation id
        .set_ttl(3600) // optional — seconds until Branta forgets this destination
        .build();

    let result = service.add_payment(payment, None).await?;

    // For ZK: persist `result.secret` alongside your order — you need it to build
    // the `branta_secret` query param when rendering the payment QR / link.
    // `result.verify_url` is the public Branta verify page for this payment.
    Ok(result)
}
```

Privacy mode interacts with `.set_zk()` on the `PaymentBuilder`: in `Strict` mode (the wallet default) all destinations must be ZK or the call returns `Err`. `Strict` is also the SDK default — platforms typically set `privacy` explicitly to `Loose` and choose per-destination.
{% endtab %}
{% endtabs %}

`ttl` accepts 30 seconds minimum, 1 year maximum; the server default is 7 days (604800 s). `metadata` is stored verbatim as a string — Branta does not parse it. The SDKs' `PaymentBuilder.addMetadata(key, value)` helpers build a JSON object if you want structured fields.

### Option 2: Raw HTTP

If your stack isn't covered by an SDK, `POST` directly. Same endpoint, same shape — you handle ZK encryption yourself if you want it (see [Zero Knowledge](/tech/api/v2/adding-payments.md#zero-knowledge) for the JavaScript reference implementation).

```bash
curl -X POST https://guardrail.branta.pro/v2/payments \
  -H "Authorization: Bearer $BRANTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Invoice #12345",
    "destinations": [
      {
        "value": "bc1qu3k6geqdjncaarsu2vq56tt8php5vsug9kasmq",
        "type": "bitcoin_address",
        "primary": true,
        "zk": false
      }
    ],
    "ttl": 3600,
    "metadata": "order-12345"
  }'
```

Full schema (all destination types, ZK fields, response shape) is on the [Adding Payments](/tech/api/v2/adding-payments.md) reference. Authentication header rules: [Authentication](/tech/authentication.md).

### Surfacing Branta in your UI

Once the destination is posted, link senders to the [`verifyUrl`](/tech/api/v2/adding-payments.md) returned by the API. You can also use [website badges](/design/website-badge.md) to indicate Branta protection at checkout, or build a custom UI per the [UI guidelines](/design/ui.md).

#### QR code / payment URI (ZK on-chain)

For **plain** destinations, the payment QR is a standard BIP-21 URI (`bitcoin:<address>`) — no Branta params needed.

For **ZK on-chain** destinations, the wallet needs two extra query parameters to look up and decrypt the record:

| Param           | Source                            | Description                                              |
| --------------- | --------------------------------- | -------------------------------------------------------- |
| `branta_id`     | `payment.destinations[n].value`   | URL-encoded encrypted ciphertext stored in Branta        |
| `branta_secret` | `secret` returned by `addPayment` | Decryption key — delivered to the sender via the QR only |

```
bitcoin:<plain_address>?branta_id=<url_encoded_branta_id>&branta_secret=<secret>
```

The plain address stays in the URI so the wallet knows where to send; `branta_id` and `branta_secret` are only for the Branta lookup. Wallets that don't recognise these params ignore them (standard BIP-21 unknown-param behaviour).

Example — building the URI from the SDK response:

```ts
const { payment, secret } = await service.addPayment({
  destinations: [{ value: address, type: DestinationType.BitcoinAddress, isPrimary: true, isZk: true }],
  // ...
});

const dest = payment.destinations.find(d => d.isPrimary);
const qrUri = `bitcoin:${address}?branta_id=${encodeURIComponent(dest.value)}&branta_secret=${secret}`;
// Encode qrUri into a QR code and render it at checkout.
```

{% hint style="info" %}
Lightning destinations (plain or ZK) use the standard `lightning:<invoice>` URI — no extra params needed. The wallet SDK handles the lookup automatically.
{% endhint %}

### Testing

* Use [staging](/tech/environments.md) for any non-live work — `https://staging.guardrail.branta.pro` with a staging API key from the staging dashboard.
* Validate the round-trip with [scan.branta.pro](https://scan.branta.pro/scan) and the [example QR codes](/setup/wallets/example-qr-codes.md) — your wallet-side rendering should match.
* The SDKs expose `isApiKeyValid()` / `IsApiKeyValidAsync()` / `is_api_key_valid()` for a cheap credential health check.
