diff --git a/README.md b/README.md
index afc7d57..4b8f1ef 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,9 @@ If you are an AI agent implementing this SDK, follow the steps below exactly. If
If they are on the receive side, ask one follow-up:
- **Platform** — single-tenant, one API key.
-- **Parent Platform** — multi-tenant, manages multiple child platforms each with their own API key and HMAC secret.
+- **Parent Platform** — multi-tenant, manages multiple child platforms. Two variants:
+ - **Per-client keys** — each child has its own API key and HMAC secret; you scope requests per-call.
+ - **Shared key** — one API key and HMAC secret for all children; tag the child per-payment with `setChildPlatform()`.
**2. Follow the matching Quick Start section below.**
@@ -47,9 +49,13 @@ Receive side (platforms):
- Always call `.setZk()` on the `PaymentBuilder` before calling `addPaymentAsync`. Plain-text destinations are rejected in `strict` mode.
- Store the `secret` from `AddPaymentResult` alongside the invoice — it is required to reconstruct the verify URL for the wallet.
-Receive side (parent platforms), in addition to the platform rules:
-- Include `hmacSecret` in `BrantaClientOptions`.
-- Pass per-call `BrantaClientOptions` to scope requests to the correct child platform's API key.
+Receive side (parent platforms — per-client keys), in addition to the platform rules:
+- Include `hmacSecret` in `BrantaClientOptions` but omit `defaultApiKey` at service setup.
+- Pass per-call `BrantaClientOptions` with each child's API key to scope requests.
+
+Receive side (parent platforms — shared key), in addition to the platform rules:
+- Include both `defaultApiKey` and `hmacSecret` in `BrantaClientOptions`.
+- Call `.setChildPlatform(name, logoUrl: ..., logoLightUrl: ...)` on the builder to tag each payment with the child's branding.
# Quick Start
@@ -155,7 +161,12 @@ final result = await service.addPaymentAsync(payment);
## For Parent Platforms (Receive Side)
-Parent platforms sign requests with HMAC in addition to the API key. Pass per-call `BrantaClientOptions` to scope requests to each child platform.
+Parent platforms sign requests with HMAC. Choose a variant based on how API keys are structured.
+
+
+Shared key — one API key covers all children (Recommended)
+
+Set up with a single API key and HMAC secret; identify the child platform per-payment.
```dart
import 'package:branta/branta.dart';
@@ -163,7 +174,7 @@ import 'package:http/http.dart' as http;
final options = BrantaClientOptions(
baseUrl: BrantaServerBaseUrl.staging,
- defaultApiKey: '',
+ defaultApiKey: '',
hmacSecret: '',
privacy: PrivacyMode.strict,
);
@@ -178,12 +189,52 @@ final payment = PaymentBuilder()
.setDescription('Order #1234')
.addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
.setZk()
+ .setChildPlatform('ChildBrand', logoUrl: 'https://example.com/logo.png')
.setTtl(600)
.build();
final result = await service.addPaymentAsync(payment);
```
+
+
+
+Per-client keys — each child has its own API key
+
+Set up the service with the shared HMAC secret only; pass each child's API key per-call.
+
+```dart
+import 'package:branta/branta.dart';
+import 'package:http/http.dart' as http;
+
+final options = BrantaClientOptions(
+ baseUrl: BrantaServerBaseUrl.staging,
+ hmacSecret: '',
+ privacy: PrivacyMode.strict,
+);
+final brantaClient = BrantaClient(httpClient: http.Client(), defaultOptions: options);
+final service = BrantaService(
+ client: brantaClient,
+ aesEncryption: AesEncryptionService(),
+ defaultOptions: options,
+);
+
+final payment = PaymentBuilder()
+ .setDescription('Order #1234')
+ .addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
+ .setZk()
+ .setTtl(600)
+ .build();
+
+// Scope to the child platform's API key per-call
+final result = await service.addPaymentAsync(
+ payment,
+ options: BrantaClientOptions(defaultApiKey: ''),
+);
+```
+
+
+
# Privacy
`PrivacyMode` controls whether plain-text on-chain lookups are allowed.
diff --git a/lib/src/v2/classes/payment_builder.dart b/lib/src/v2/classes/payment_builder.dart
index 45260e7..08b11fc 100644
--- a/lib/src/v2/classes/payment_builder.dart
+++ b/lib/src/v2/classes/payment_builder.dart
@@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart';
import '../../enums/destination_type.dart';
import '../models/destination.dart';
import '../models/payment.dart';
+import '../models/platform.dart';
class PaymentBuilder {
final Payment _payment = Payment(destinations: []);
@@ -50,5 +51,10 @@ class PaymentBuilder {
return this;
}
+ PaymentBuilder setChildPlatform(String name, {String? logoUrl, String? logoLightUrl}) {
+ _payment.childPlatform = Platform(name: name, logoUrl: logoUrl, logoLightUrl: logoLightUrl);
+ return this;
+ }
+
Payment build() => _payment;
}
diff --git a/lib/src/v2/models/payment.dart b/lib/src/v2/models/payment.dart
index fdb2d94..0fbc1b3 100644
--- a/lib/src/v2/models/payment.dart
+++ b/lib/src/v2/models/payment.dart
@@ -11,6 +11,7 @@ class Payment {
String? platformLogoUrl;
String? platformLogoLightUrl;
Platform? parentPlatform;
+ Platform? childPlatform;
String? btcPayServerPluginVersion;
/// Runtime-only flag set by [BrantaService] after metadata decryption.
@@ -27,6 +28,7 @@ class Payment {
this.platformLogoUrl,
this.platformLogoLightUrl,
this.parentPlatform,
+ this.childPlatform,
this.btcPayServerPluginVersion,
});
@@ -67,6 +69,7 @@ class Payment {
if (platformLogoLightUrl != null)
'platform_logo_light_url': platformLogoLightUrl,
if (parentPlatform != null) 'parent_platform': parentPlatform!.toJson(),
+ if (childPlatform != null) 'child_platform': childPlatform!.toJson(),
if (btcPayServerPluginVersion != null)
'btc_pay_server_plugin_version': btcPayServerPluginVersion,
};
diff --git a/test/branta_test.dart b/test/branta_test.dart
index d75a699..e2d786f 100644
--- a/test/branta_test.dart
+++ b/test/branta_test.dart
@@ -670,6 +670,68 @@ void main() {
final dest = Destination(value: 'addr');
expect(dest.toJson().containsKey('type'), isFalse);
});
+
+ test('setChildPlatform sets name', () {
+ final payment = PaymentBuilder()
+ .addDestination(_bitcoinAddress)
+ .setChildPlatform('Acme')
+ .build();
+
+ expect(payment.childPlatform!.name, equals('Acme'));
+ });
+
+ test('setChildPlatform optional URLs default to null', () {
+ final payment = PaymentBuilder()
+ .addDestination(_bitcoinAddress)
+ .setChildPlatform('Acme')
+ .build();
+
+ expect(payment.childPlatform!.logoUrl, isNull);
+ expect(payment.childPlatform!.logoLightUrl, isNull);
+ });
+
+ test('setChildPlatform with URLs sets urls', () {
+ final payment = PaymentBuilder()
+ .addDestination(_bitcoinAddress)
+ .setChildPlatform('Acme',
+ logoUrl: 'https://example.com/logo.png',
+ logoLightUrl: 'https://example.com/logo-light.png')
+ .build();
+
+ expect(payment.childPlatform!.logoUrl, equals('https://example.com/logo.png'));
+ expect(payment.childPlatform!.logoLightUrl, equals('https://example.com/logo-light.png'));
+ });
+
+ test('setChildPlatform returns builder', () {
+ final builder = PaymentBuilder().addDestination(_bitcoinAddress);
+ final result = builder.setChildPlatform('Acme');
+
+ expect(result, same(builder));
+ });
+
+ test('setChildPlatform serializes to toJson', () {
+ final payment = PaymentBuilder()
+ .addDestination(_bitcoinAddress)
+ .setChildPlatform('Acme', logoUrl: 'https://example.com/logo.png')
+ .build();
+
+ final json = payment.toJson();
+ final cp = json['child_platform'] as Map;
+
+ expect(cp['name'], equals('Acme'));
+ expect(cp['logo_url'], equals('https://example.com/logo.png'));
+ expect(cp.containsKey('logo_light_url'), isFalse);
+ });
+
+ test('no childPlatform omitted from toJson', () {
+ final payment = PaymentBuilder()
+ .addDestination(_bitcoinAddress)
+ .build();
+
+ final json = payment.toJson();
+
+ expect(json.containsKey('child_platform'), isFalse);
+ });
});
// -------------------------------------------------------------------------