> 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/receive/custom-integration.md).

# Custom Integration

POST your payment destinations to Branta from your own code — SDK or raw HTTP.

If none of [BTCPay, Take My Sats, or Zaprite](/setup/receive/integrations.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/receive.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.

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/receive.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/receive.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. In `strict` mode (the wallet default, and the SDK default) every destination must be zero-knowledge or the call fails. Set privacy to `loose` and choose per destination.

{% 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 };
}
```

{% 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
```

{% 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;
}
```

{% 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
}
```

{% 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)
}
```

{% endtab %}

{% tab title="Go" %}
SDK: [`github.com/BrantaOps/branta-go`](https://pkg.go.dev/github.com/BrantaOps/branta-go) ([source](https://github.com/BrantaOps/branta-go))

```go
package main

import (
	"context"
	"os"

	"github.com/BrantaOps/branta-go"
)

func publishDestination(ctx context.Context, service *branta.BrantaService, address string) (branta.AddPaymentResult, error) {
	payment := branta.NewPaymentBuilder().
		AddDestination(address, branta.BitcoinAddress).
		SetZk(). // omit SetZk() to publish in plain
		SetDescription("Invoice #12345").
		AddMetadata("order_id", "order-12345"). // optional — your own correlation id
		SetTTL(3600). // optional — seconds until Branta forgets this destination
		Build()

	result, err := service.AddPayment(ctx, payment, nil)
	if err != nil {
		return branta.AddPaymentResult{}, err
	}

	// 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, nil
}

func main() {
	service := branta.NewBrantaService(branta.BrantaClientOptions{
		BaseURL:       branta.Production,
		DefaultAPIKey: os.Getenv("BRANTA_API_KEY"),
		Privacy:       branta.PrivacyLoose,
	})
	_, _ = publishDestination(context.Background(), service, "bc1q...")
}
```

{% endtab %}

{% tab title="Swift" %}
SDK: [branta-swift](https://github.com/BrantaOps/branta-swift) (Swift Package Manager or CocoaPods)

```swift
import Branta

let service = BrantaService(options: BrantaClientOptions(
    baseURL: .production,
    defaultAPIKey: ProcessInfo.processInfo.environment["BRANTA_API_KEY"],
    privacy: .loose
))

// Called whenever your platform issues a destination to a sender.
func publishDestination(address: String) async throws -> AddPaymentResult {
    let payment = PaymentBuilder()
        .setDescription("Invoice #12345")
        .addDestination(address, type: .bitcoinAddress)
        .setZK() // omit setZK() to publish in plain
        .setTTL(3600) // optional — seconds until Branta forgets this destination
        .addMetadata(key: "order_id", value: "order-12345") // optional — your own correlation id
        .build()

    let result = try await 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
}
```

{% 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/receive.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 [Receive](/tech/api/receive.md) reference.

### Surfacing Branta in your UI

Once the destination is posted, link senders to the [`verifyUrl`](/tech/api/receive.md) returned by the API.

#### 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 %}
