> ## Documentation Index
> Fetch the complete documentation index at: https://musubinetwork.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Programmatic Access

> REST + SSE for custodians with custom integration or real-time reconciliation needs.

<Note>
  Most custodians should use scheduled downloads — see [Audit Exports](/custodian/integration/audit-exports). This page is for the minority of custodians who need programmatic access: real-time accounting reconciliation or downstream systems that can't consume batch exports.
</Note>

Musubi's backend exposes REST + SSE APIs that you can integrate directly. This page covers authentication, the proposal/quote workflow programmatically (as an alternative to the [Console](/custodian/integration/console)), and real-time settlement events.

## When to use this

* Your downstream systems are event-driven (real-time, not batch-ingested)
* Your accounting system reconciles continuously and can't wait for daily exports
* You're automating the accept/reject authorization decision itself
* You need sub-second settlement notifications (most custodians do not)

If none of those apply, use the Console + Audit Exports — the integration is less work and covers the same ground.

## Authentication

JWT bearer token, refreshed roughly every hour. Full details in [Authentication](/authentication). Typical pattern: cache the token, refresh \~60 s before expiry, retry once on 401 after forcing a refresh.

```java theme={null}
// Token provider — caches the JWT and refreshes on expiry.
@Component
public class MusubiAuthProvider {
  private final WebClient auth;
  private final AtomicReference<Token> cached = new AtomicReference<>();

  public record Token(String value, Instant expiresAt) {}

  public MusubiAuthProvider(WebClient.Builder b, @Value("${musubi.backend-url}") String url) {
    this.auth = b.baseUrl(url).build();
  }

  public String token() {
    var t = cached.get();
    if (t != null && Instant.now().isBefore(t.expiresAt().minusSeconds(60))) return t.value();
    return refresh();
  }

  private synchronized String refresh() {
    var t = auth.post().uri("/auth/token")
        .bodyValue(Map.of(/* your IdP credentials */))
        .retrieve().bodyToMono(Token.class).block();
    cached.set(t);
    return t.value();
  }
}
```

## Proposal + quote workflow via REST

| Endpoint                                                   | Purpose                                                     |
| ---------------------------------------------------------- | ----------------------------------------------------------- |
| `GET /api/v1/orders?status=PENDING`                        | List incoming proposals awaiting your accept/reject         |
| `POST /api/v1/orders/{intent_id}/accept`                   | Sender custodian accepts the proposal — proceeds to quoting |
| `GET /api/v1/orders/{intent_id}/quotes`                    | Review competing market maker quotes                        |
| `POST /api/v1/orders/{intent_id}/quotes/{quote_id}/accept` | Sender custodian co-signs the chosen quote                  |
| `GET /api/v1/orders/events` (SSE)                          | Real-time `order_updated` and `quote_received` events       |

Full endpoint shapes, request/response examples: [API Reference](/custodian/api-reference).

```java theme={null}
// Typed DTOs. Match the shapes in /custodian/api-reference.
public record ApiResponse<T>(T data) {}
public record OrderDto(
    String intentId, String status, String senderPartyId,
    String sourceCurrency, String targetCurrency, String targetAmount,
    String sourceAmountMax, Instant createdAt, Instant expiresAt) {}
public record QuoteDto(
    String quoteId, String intentId, String marketMakerPartyId,
    String fxRate, String sourceAmount, String targetAmount,
    Instant submittedAt, Instant validUntil, String status) {}
```

```java theme={null}
// Service — list proposals, review quotes, accept.
@Service
public class MusubiCustodianClient {
  private final WebClient rest;

  public MusubiCustodianClient(WebClient.Builder b,
                               @Value("${musubi.backend-url}") String url,
                               MusubiAuthProvider auth) {
    this.rest = b.baseUrl(url)
        .filter((req, next) -> next.exchange(
            ClientRequest.from(req).headers(h -> h.setBearerAuth(auth.token())).build()))
        .build();
  }

  public List<OrderDto> listPending() {
    return rest.get().uri("/api/v1/orders?status=PENDING")
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<ApiResponse<List<OrderDto>>>() {})
        .map(ApiResponse::data)
        .block();
  }

  public List<QuoteDto> listQuotes(String intentId) {
    return rest.get().uri("/api/v1/orders/{id}/quotes", intentId)
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<ApiResponse<List<QuoteDto>>>() {})
        .map(ApiResponse::data)
        .block();
  }

  public OrderDto acceptProposal(String intentId) {
    return rest.post().uri("/api/v1/orders/{id}/accept", intentId)
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<ApiResponse<OrderDto>>() {})
        .map(ApiResponse::data)
        .block();
  }

  public OrderDto acceptQuote(String intentId, String quoteId) {
    // Co-signs a pending QuoteAcceptanceProposal — the institution emitted
    // it via its own POST .../accept (returns 202). This call exercises
    // AcceptQuoteProposal: atomic FXOrder.AcceptQuote (PENDING → QUOTED)
    // + FXQuote.Accept on the winner + QuoteRequest.DeactivateRequest +
    // FXQuote.RejectQuote on losers. Returns 200 + the QUOTED FXOrder.
    return rest.post().uri("/api/v1/orders/{id}/quotes/{qid}/accept", intentId, quoteId)
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<ApiResponse<OrderDto>>() {})
        .map(ApiResponse::data)
        .block();
  }

  public RejectAck rejectQuote(String intentId, String quoteId, String rejectionDetail) {
    // Declines the pending QuoteAcceptanceProposal — β quote-tainting:
    // the proposed FXQuote becomes terminal QUOTE_REJECTED. Other PENDING
    // quotes are unaffected; the institution can propose a different one.
    // Server enforces rejection_detail >= 20 chars (DAML ensure + service
    // guard) — backend returns 422 if shorter.
    return rest.post().uri("/api/v1/orders/{id}/quotes/{qid}/reject", intentId, quoteId)
        .body(Map.of("rejection_detail", rejectionDetail))
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<ApiResponse<RejectAck>>() {})
        .map(ApiResponse::data)
        .block();
  }
}
```

Error handling worth adding: catch `WebClientResponseException.Unauthorized` on 401 → force-refresh the token and retry once; surface `Conflict` on 409 (expired quote, order already advanced) back to whatever subsystem triggered the call. For `rejectQuote`, surface `UnprocessableEntity` on 422 (rejection\_detail too short) as a client-side validation bug — auditors expect ≥ 20 chars.

## Real-time settlement events via SSE

```java theme={null}
public record OrderEvent(
    String intentId, String status, String transactionHash,
    Instant settledAt, String sourceAmountActual, String targetAmount,
    String fxRate) {}
```

```java theme={null}
@Component
public class MusubiSettlementListener {
  private static final Logger log = LoggerFactory.getLogger(MusubiSettlementListener.class);

  private final WebClient rest;
  private final AccountingService accounting;

  public MusubiSettlementListener(WebClient.Builder b,
                                  @Value("${musubi.backend-url}") String url,
                                  MusubiAuthProvider auth,
                                  AccountingService accounting) {
    this.rest = b.baseUrl(url)
        .filter((req, next) -> next.exchange(
            ClientRequest.from(req).headers(h -> h.setBearerAuth(auth.token())).build()))
        .build();
    this.accounting = accounting;
  }

  @EventListener(ApplicationReadyEvent.class)
  public void subscribe() {
    var type = new ParameterizedTypeReference<ServerSentEvent<OrderEvent>>() {};
    rest.get().uri("/api/v1/orders/events")
        .accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(type)
        .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1))
            .maxBackoff(Duration.ofSeconds(30))
            .doBeforeRetry(s -> log.warn("SSE disconnected, retrying: {}", s.failure().toString())))
        .filter(sse -> "order_updated".equals(sse.event()))
        .mapNotNull(ServerSentEvent::data)
        .filter(ev -> "SETTLED".equals(ev.status()))
        .doOnNext(this::handle)
        .subscribe();
  }

  private void handle(OrderEvent ev) {
    log.info("Settled {} at {} (tx={})", ev.intentId(), ev.settledAt(), ev.transactionHash());
    accounting.reconcile(ev.intentId(), ev.transactionHash(), ev.settledAt(),
                         ev.sourceAmountActual(), ev.targetAmount(), ev.fxRate());
  }
}
```

## Production notes

* **Idempotency.** Handle `SETTLED` events idempotently — SSE reconnects can replay the latest event. Key your accounting write by `intentId + transactionHash`.
* **Ordering.** `order_updated` events are per-contract — you may see `EXECUTING` then `SETTLED` for the same intent. Filter to `SETTLED` only if that's the only state your accounting cares about.
* **Backpressure.** If your downstream is slow, switch `.doOnNext` to `.concatMap(ev -> Mono.fromCallable(() -> handle(ev)).subscribeOn(Schedulers.boundedElastic()))` so slow handlers don't starve the event loop.

## Other stacks

Same shape, different clients:

* **Node** — `fetch` for REST; `EventSource` (native) or `eventsource` package for SSE
* **Python** — `httpx` or `requests` for REST; `httpx-sse` or `sseclient-py` for SSE
* **Kotlin** — same `WebClient` as Java; or `ktor-client-cio` + `SSE`

The pattern in all cases: authenticate with JWT, filter SSE to `order_updated` with `status == SETTLED`, dispatch to your downstream handlers with idempotency keyed on `intentId + transactionHash`.
