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

# API reference

> Base URLs, Bearer-JWT authentication, the error shape, and the full endpoint map for the payroll-api BFF.

`payroll-api` is a Fastify **BFF** (backend-for-frontend): a public-but-authenticated surface
the browser SPA calls. It holds Confiroll's operational credentials internally and never
exposes a database or a user key.

<Card title="Interactive endpoint reference" icon="code" href="/api-reference/health/service-health">
  The **Core endpoints** and **Payroll resources** groups in this tab document every
  endpoint below, with request/response schemas and a request playground, generated from
  <code>openapi.yaml</code>.
</Card>

## Base URLs

<CodeGroup>
  ```text payroll-api (BFF) theme={"system"}
  https://api.confiroll.com
  ```

  ```text Self-hosted SDP (headless) theme={"system"}
  https://sdp.confiroll.com
  ```
</CodeGroup>

The SDP host exposes only operational surfaces (`/health`, `/.well-known/stellar.toml`); it
is not part of the payroll API. Every payroll route lives under `https://api.confiroll.com`.

## Endpoints at a glance

Every route the live build serves. `Auth` marks whether a Bearer session JWT is required.

| Method | Path                    | Auth | Purpose                                                                                          |
| ------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------ |
| `GET`  | `/health`               | No   | Liveness plus config presence (`ok`, `network`, `privy`, `sdp`, `sponsor`).                      |
| `GET`  | `/auth/sep10/challenge` | No   | Return a SEP-10 challenge transaction for a `G-address` to sign.                                 |
| `POST` | `/auth/sep10/verify`    | No   | Verify the signed challenge and issue a session (`kind: "stellar"`).                             |
| `POST` | `/auth/privy`           | No   | Exchange a Privy access token for a session (`kind: "privy"`).                                   |
| `POST` | `/transfer`             | Yes  | Fee-bump a browser-signed confidential transfer (Fork B). Stellar sessions only.                 |
| `POST` | `/batch`                | Yes  | Run a confidential batch through the SDP relay. Returns `501` unless batch execution is enabled. |
| `GET`  | `/batch/{jobId}`        | Yes  | Poll the in-memory job created by `POST /batch`.                                                 |
| `POST` | `/withdraw`             | Yes  | Returns `501`. The withdraw operation runs in the client tooling.                                |
| `POST` | `/auditor/disclose`     | Yes  | Returns `501`. Disclosure runs in the client tooling.                                            |

<Note>
  The interactive playground for every endpoint is under the **Core endpoints** and **Payroll
  resources** groups in this tab. Use it to inspect schemas and fire real requests against
  `https://api.confiroll.com`.
</Note>

## Authentication

Protected routes require a **Bearer session JWT**:

```http theme={"system"}
Authorization: Bearer <sessionJWT>
```

The session is an HS256 JWT carrying `sub`, `kind`, and an expiry (default **1 hour**). You
obtain one of two ways, both returning `{ session, kind, sub }`. Store `session` and send it as
the Bearer token on every protected call.

<Tabs>
  <Tab title="SEP-10 (wallet)">
    A challenge-response handshake proves you control a Stellar account. The resulting session
    has `kind: "stellar"` and `sub` set to your `G-address`. This is the session that can call
    `POST /transfer`.

    <Steps>
      <Step title="Request a challenge">
        Ask for a challenge transaction for your account. The challenge is single-use and
        expires in **5 minutes**.

        ```http theme={"system"}
        GET /auth/sep10/challenge?account=GABC123...XYZ
        ```

        ```json Response theme={"system"}
        {
          "transaction": "AAAAAgAAAAA...base64 challenge XDR...",
          "network_passphrase": "Test SDF Network ; September 2015"
        }
        ```
      </Step>

      <Step title="Sign the challenge with your wallet">
        Sign the returned `transaction` with `signTransaction` from Stellar Wallets Kit. You
        sign the envelope only; no key leaves the wallet.
      </Step>

      <Step title="Verify and receive a session">
        Post the signed XDR back. The API validates the signature and issues the session.

        ```json POST /auth/sep10/verify theme={"system"}
        { "transaction": "AAAAAgAAAAA...signed challenge XDR..." }
        ```

        ```json Response theme={"system"}
        {
          "session": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
          "kind": "stellar",
          "sub": "GABC123...XYZ"
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Privy (email)">
    Exchange a Privy access token for a session. The resulting session has `kind: "privy"` and
    `sub` set to the Privy user id. A Privy session cannot call `POST /transfer` (it returns
    `501`).

    <Steps>
      <Step title="Obtain a Privy access token">
        Complete the Privy email login in the browser SDK. Privy hands your client an access
        token.
      </Step>

      <Step title="Exchange it for a session">
        Post the token. The API verifies it server-side with `@privy-io/server-auth` and issues
        the session.

        ```json POST /auth/privy theme={"system"}
        { "token": "<privy-access-token>" }
        ```

        ```json Response theme={"system"}
        {
          "session": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
          "kind": "privy",
          "sub": "did:privy:clxxxxxxxxxxxxxxxx"
        }
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

<Warning>
  Only **Stellar (SEP-10)** sessions can call `POST /transfer`. A Privy session gets a `501` on
  that route. `/transfer` only fee-bumps a transaction whose source **is your own session
  account** (otherwise `403`).
</Warning>

## POST /transfer

The live non-custodial write path. You build and sign a `confidential_transfer` in the browser
(employer as tx source, Fork B), then hand the API only the signed inner envelope. The API binds
`tx.source == session.sub`, then the sponsor fee-bumps it (outer fee = inner fee times 2) and
submits. On-chain the `fee_account` is the sponsor and your XLM delta is 0. Details in
[Fee sponsorship](/developers/fee-sponsorship).

```bash theme={"system"}
curl -X POST https://api.confiroll.com/transfer \
  -H "Authorization: Bearer <sessionJWT>" \
  -H "Content-Type: application/json" \
  -d '{ "signedXDR": "AAAAAgAAAAA...base64 signed inner tx..." }'
```

```json Response theme={"system"}
{ "hash": "d4a1c0b7...", "status": "SUCCESS" }
```

<ResponseField name="hash" type="string">
  The on-chain transaction hash of the submitted fee-bump. Look it up on stellar.expert testnet.
</ResponseField>

<ResponseField name="status" type="string">
  `SUCCESS` once the sponsor polls the submission to completion over Soroban RPC.
</ResponseField>

The endpoint accepts `{ signedXDR }` and fee-bumps it.

## Error shape

Every handled error returns the same shape with an appropriate status code:

```json theme={"system"}
{ "error": "human-readable message" }
```

(Unknown routes fall through to the framework's default `{ statusCode, error, message }`
404.) The API limits request bodies to **256 KiB**.

| Status | When it occurs                                                                                                                                                                                |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | The request body or query is malformed or missing a required field (for example `POST /transfer` without `signedXDR`, or a challenge request without `account`).                              |
| `401`  | The Bearer session JWT is missing, expired, or fails verification on a protected route.                                                                                                       |
| `403`  | On `POST /transfer`, the inner transaction's source is not your session account. You may only fee-bump your own account.                                                                      |
| `422`  | The sponsor refused the fee-bump: the target contract is not on the allow-list, the inner fee exceeds the cap, or the per-account quota is exhausted.                                         |
| `501`  | `POST /transfer` from a **Privy** session; `POST /batch` when `BATCH_ENABLED` is off; and `POST /withdraw` and `POST /auditor/disclose` always, because those operations run **client-side**. |
| `500`  | An unexpected server error.                                                                                                                                                                   |

## Endpoints

<CardGroup cols={2}>
  <Card title="Core endpoints" icon="circle-check">
    `GET /health`, the auth calls (`/auth/sep10/challenge`, `/auth/sep10/verify`,
    `/auth/privy`), `POST /transfer`, `POST /batch` with `GET /batch/{jobId}`, `POST /withdraw`,
    and `POST /auditor/disclose`.
  </Card>

  <Card title="Payroll resources" icon="table">
    The SDP-aligned data API: `GET /me`, `/contractors*`, `/batches*`, `/payouts`, and
    `/funding/*`.
  </Card>
</CardGroup>

<Note>
  Good to know:

  * `POST /transfer` requires a Stellar (SEP-10) session and accepts `{ signedXDR }`.
  * `POST /withdraw` and `POST /auditor/disclose` return `501` because those operations run
    client-side, where you hold both keys.
</Note>

## FAQ

<AccordionGroup>
  <Accordion title="Which session type do I need for POST /transfer?">
    A **Stellar (SEP-10)** session. `/transfer` binds the inner transaction's source to your
    session account, and only a SEP-10 session carries a `G-address` as its `sub`. A Privy
    (email) session returns `501` on that route. Sign in with a wallet to run confidential
    transfers.
  </Accordion>

  <Accordion title="Why did my transfer return 403 instead of 401?">
    `401` means the request had no valid session. `403` means the session is valid but the inner
    transaction's `source` is not your session account. The API refuses to fee-bump anyone
    else's transaction, so build the inner transaction with your own account as the source.
  </Accordion>

  <Accordion title="What is the difference between a 422 and a 501 on /transfer?">
    A `501` on `/transfer` means the session kind is wrong (a Privy session). A `422` means the
    session and binding were fine but the **sponsor refused**: the target contract is off the
    allow-list, the inner fee is over the cap, or the per-account quota is exhausted. See
    [Fee sponsorship](/developers/fee-sponsorship) for the guard-rails.
  </Accordion>
</AccordionGroup>
