# Introduction (/)



The Talentir API lets you integrate payouts into your own product. Talentir runs the payments, carries the liability, and handles recipient verification, so you do not build banking integrations or a ledger.

## What you can do [#what-you-can-do]

* **Create payouts**: send payments to creators using social media handles (`@username`), email addresses, or wallet addresses.
* **Mint hosted sessions**: generate Talentir-hosted URLs for payout claiming, payout approval, KYB verification, and spending allowance — optionally under your own white-label branding.
* **Manage webhooks**: receive real-time notifications for payout status changes.
* **Access team information**: read your team details, members, balances, and deposit instructions.

## Base URLs [#base-urls]

| Environment | Base URL                              |
| ----------- | ------------------------------------- |
| Production  | `https://www.talentir.com/api/v1`     |
| Sandbox     | `https://sandbox.talentir.com/api/v1` |

The sandbox is a standalone environment with its own persistent database. Sign up self-service; your teams, API keys, and webhooks stay put between visits. All external providers (banking, PayPal, blockchain, compliance screening) are simulated: no real money moves, and payouts settle instantly and synchronously. Business verification (KYB) completes with one click, wallets are simulated, and every payout method has a passing and a failing test scenario (see [Create a payout](/guides/create-a-payout)).

## How to use these docs [#how-to-use-these-docs]

<Cards>
  <Card description="Send your first payout from the sandbox in a few minutes." href="/quickstart" title="Quickstart" />

  <Card description="Team API keys for your own team, OAuth 2.1 with PKCE for platforms." href="/authentication/api-keys" title="Authentication" />

  <Card description="Create payouts, mint hosted sessions, approve, and receive webhooks." href="/guides/create-a-payout" title="Guides" />

  <Card description="Every endpoint, parameter, and response schema, generated from the live spec." href="/reference/payouts/payout.create" title="API Reference" />
</Cards>

* Each endpoint page has a **Test** button that opens the Scalar API client for the endpoint.
* The machine-readable specification lives at [`/api/v1/spec.json`](https://www.talentir.com/api/v1/spec.json). Agent-readable page dumps are at [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt).


# Quickstart (/quickstart)



This walkthrough uses the sandbox, where no real money moves and payouts settle instantly.

## 1. Get an API key [#1-get-an-api-key]

1. Sign up at [sandbox.talentir.com](https://sandbox.talentir.com) and create a team.
2. Go to **Dashboard → Integrations → API keys** and create a key.
3. Send it as a Bearer token on every request.

```bash
export TALENTIR_API_KEY="tal_..."
export BASE_URL="https://sandbox.talentir.com/api/v1"
```

## 2. Create a payout [#2-create-a-payout]

```bash
curl -X POST "$BASE_URL/payout" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Payment for YouTube channel campaign",
    "creatorHandle": "@mrbeast",
    "verificationMethod": "youtube-channel",
    "payoutAmount": "100.00",
    "currency": "EUR",
    "customId": "campaign-42"
  }'
```

The response contains the payout `id`, its `status` (`created`), and an `action` field that tells you whether the payout was created or updated (payouts are upserted by `customId` — see [Idempotency](/concepts/idempotency)).

## 3. Send the recipient a claim link [#3-send-the-recipient-a-claim-link]

Mint a hosted session URL and send it to the person being paid:

```bash
curl -X POST "$BASE_URL/session/payout" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payoutId": "<payout id from step 2>",
    "redirectUrl": "https://yourapp.com/done"
  }'
```

The returned `url` is an opaque Talentir-hosted claim screen. The recipient signs in or signs up with their own email and claims the payout. Treat the URL as opaque and do not parse it.

## 4. Check the status [#4-check-the-status]

```bash
curl "$BASE_URL/payout/<payout id>" \
  -H "Authorization: Bearer $TALENTIR_API_KEY"
```

In the sandbox, a claimed payout settles instantly, so you can watch it move through the [payout lifecycle](/concepts/payout-lifecycle) right away. For production integrations, subscribe to [webhooks](/guides/webhooks) instead of polling.

## Next steps [#next-steps]

* [Approve and execute payouts](/guides/approve-and-execute-payouts) — payouts start out pending; someone or something must approve them.
* [OAuth 2.1 with PKCE](/authentication/oauth) — for platform integrations that act on behalf of connected customer teams.
* [Set up webhooks](/guides/webhooks) — real-time payout status notifications.


# Team API keys (/authentication/api-keys)



All endpoints require Bearer token authentication. The simplest token is a team API key.

## Create a key [#create-a-key]

Go to **Dashboard → Integrations → API keys** and create a key. Store it in your secret manager; it is shown once.

## Use it [#use-it]

Send the key on every request:

```bash
curl "https://www.talentir.com/api/v1/team" \
  -H "Authorization: Bearer $TALENTIR_API_KEY"
```

API key access is scoped to the team that owns the key. To act on behalf of other teams (your customers), use [OAuth 2.1](/authentication/oauth) instead.

## Scopes [#scopes]

API keys carry the default scopes: read and write payouts, webhooks, sessions, and team information and settings. Scopes are fixed when a key is created, so a key created before `team:manage` existed must be edited (or replaced) to receive it. The `payouts:approve` scope — creating payouts as pre-approved — is only granted to keys of teams with the `payout.api_approve` permission. See [Scopes and permissions](/concepts/scopes-and-permissions).

## Good practice [#good-practice]

* Keys are environment-specific: a sandbox key does not work in production.
* Rotate keys from the dashboard; deleting a key revokes it immediately.
* Never embed keys in client-side code. Calls belong on your server.


# OAuth 2.1 with PKCE (/authentication/oauth)



For platform integrations, use the OAuth 2.1 authorization code flow with PKCE: you redirect your customer to Talentir's hosted authorization screens, they sign in (or sign up), select or create their team, and consent. Your server then exchanges the code for tokens scoped to that team.

## Get an OAuth client [#get-an-oauth-client]

**Create it in the dashboard (recommended):** go to **Settings → OAuth Clients** in your Talentir team dashboard, enter a name and your callback URL(s), and you receive a `client_id` and `client_secret` (shown once, with copy and download). Clients created there are *attributed to your team*, which unlocks the partner-program features: teams created through your authorize flow are credited to your platform, and hosted sessions can carry your white-label branding.

**Deprecated — dynamic client registration (RFC 7591)** via the `registration_endpoint` still works (it needs no dashboard or signup, and AI/MCP tooling relies on it), but it is deprecated for platform integrations and may be restricted in the future. Dynamically registered clients are anonymous — no attribution, no platform branding — and cannot be managed or edited afterwards.

## Discovery [#discovery]

```
GET /.well-known/oauth-authorization-server/api/auth
```

This returns the `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, and `jwks_uri`.

## Requirements [#requirements]

* `code_challenge` / `code_challenge_method=S256` — PKCE is mandatory.
* `resource=<base_url>/api/v1` on **both** `/authorize` and `/token`, so the token's `aud` claim binds to the API v1 resource (for production: `https://www.talentir.com/api/v1`).
* The user must select a team during authorization; the chosen team is encoded in the `https://talentir.com/oauth/team_id` claim.
* Include `offline_access` in the scope list to receive a refresh token.

## Authorization hints [#authorization-hints]

Add any of these query parameters to the `/authorize` request to pre-fill the hosted screens when you already know who is connecting. They are *hints only* — the authenticated account and the selected team are always verified server-side.

| Parameter    | Value                               | Effect                                                                                                                                                                                                       |
| ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `login_hint` | Email address                       | Pre-fills the email and emails the one-time sign-in code automatically (social/passkey sign-in is hidden). If the user is already signed in with a different email, they are asked to switch accounts first. |
| `team_hint`  | A team `id` (UUID) from `GET /team` | Pre-selects that team on the team step. Ignored if the signed-in user is not a member of it.                                                                                                                 |
| `team_name`  | String                              | Pre-fills the suggested name when a new user who has no team yet is prompted to create one.                                                                                                                  |

Example (URL-encode the values in practice):

```
https://www.talentir.com/api/auth/oauth2/authorize?response_type=code&client_id=<client_id>
  &redirect_uri=<redirect_uri>&scope=team:read payouts:write
  &code_challenge=<challenge>&code_challenge_method=S256
  &resource=https://www.talentir.com/api/v1
  &login_hint=creator@example.com
  &team_hint=4399dd07-cf1b-414b-bfbc-91a88dd73ec5
```

Teams created during your authorize flow are automatically attributed to your platform when your OAuth client was created in the dashboard. Partner-program enrollment (referral terms, white-label) is handled by Talentir — contact us to enroll.

## Scopes [#scopes]

See [Scopes and permissions](/concepts/scopes-and-permissions) for the full table.

## Try OAuth from the console [#try-oauth-from-the-console]

Open an endpoint in the API reference and select **Test** to use the Scalar API client. You need a `client_id` to run the OAuth flow. The quickest way for testing is a throwaway dynamically registered client:

```bash
curl -sX POST https://sandbox.talentir.com/api/auth/oauth2/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Talentir API Docs",
    "redirect_uris": ["https://sandbox.talentir.com/api/v1"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"]
  }' | jq
```

Copy the `client_id` from the response, open the console, click **Authorize**, paste it, and run the flow. Docs-testing clients are dynamically registered, so they are anonymous — no attribution or platform branding.


# Approve and execute payouts (/guides/approve-and-execute-payouts)



A payout created through the API starts out pending: it exists, the recipient can be notified, but no money moves until the payout is approved by the sending team. This split keeps the API safe by default — `payouts:write` can never move funds on its own.

## Option 1: approve in the dashboard [#option-1-approve-in-the-dashboard]

Team members with approval permission see pending payouts in the Talentir dashboard and approve them there. No integration work needed.

## Option 2: approval sessions [#option-2-approval-sessions]

Mint a hosted approval screen for a team member with `POST /session/approval` and a list of `payoutIds`. The member reviews and approves the batch on a Talentir-hosted page. See [Create hosted sessions](/guides/hosted-sessions).

This is the recommended pattern for platforms: your product shows "payouts ready", the customer clicks through to the hosted screen, reviews, and approves with their passkey.

## Option 3: pre-approved creation [#option-3-pre-approved-creation]

With the `payouts:approve` scope, `POST /payout` accepts `preApproved: true` and creates the payout already approved. The scope is restricted — it is granted only to admin-provisioned OAuth clients and to API keys for teams with the `payout.api_approve` permission. It is not available via dynamic client registration.

Pre-approved payouts are subject to balance enforcement: when the total of all open (approved but not yet paid out) payouts would exceed the team wallet balance, the request is rejected with HTTP 422 and error code `INSUFFICIENT_BALANCE` (the error `data` carries `walletBalance`, `totalOpenAmount`, and `currency`).

## Wallet and allowance [#wallet-and-allowance]

Approval and execution run against the team's wallet, a passkey wallet held by the team owner:

* A team must finish wallet setup before its payouts can be approved or executed. The hosted flows prompt the owner automatically if the wallet is missing.
* The owner can cap automated spending with a daily allowance; mint the hosted screen for it with `POST /session/allowance`.

## After approval [#after-approval]

Once approved and funded, Talentir requests the transfer with the payout method the recipient chose during claiming. Track progress via [webhooks](/guides/webhooks) (`approved` → `requested` → `completed`) or `GET /payout/{id}`. The full state machine is described in [Payout lifecycle](/concepts/payout-lifecycle).


# Create a payout (/guides/create-a-payout)



`POST /payout` creates (or updates) a payout for a recipient. The recipient does not need a Talentir account yet — they claim the payout later through a [hosted session](/guides/hosted-sessions).

## Identify the recipient [#identify-the-recipient]

Pick a `verificationMethod` that matches what you know about the recipient:

| `verificationMethod` | Recipient identified by   | Required fields                   |
| -------------------- | ------------------------- | --------------------------------- |
| `email`              | Email address             | `email`                           |
| `youtube-channel`    | YouTube handle            | `creatorHandle` (starts with `@`) |
| `tiktok`             | TikTok handle             | `creatorHandle` (starts with `@`) |
| `instagram`          | Instagram handle          | `creatorHandle` (starts with `@`) |
| `wallet_address`     | Talentir user ID / wallet | `walletAddress`                   |

The recipient proves control of that identity during the claim flow — for social handles by connecting the account, for email by signing in with a one-time code.

## Request [#request]

```bash
curl -X POST "$BASE_URL/payout" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Payment for YouTube channel campaign",
    "creatorHandle": "@mrbeast",
    "verificationMethod": "youtube-channel",
    "payoutAmount": "100.00",
    "currency": "EUR",
    "customId": "campaign-42"
  }'
```

Useful optional fields:

* `customId` — your identifier. Sending the same `customId` again updates the existing payout instead of creating a duplicate; the response's `action` field says `created` or `updated`. See [Idempotency](/concepts/idempotency).
* `payoutType` — `manual` (default) or `manual-immutable` (source-of-truth fields cannot be updated after creation).
* `availableOn` — a UTC calendar date (`YYYY-MM-DD`) from which the payout can be claimed. Omit for immediately claimable.
* `tags` — free-form strings for categorizing payouts; filterable in `GET /payouts`.
* `notifications` — `allowed` (default) or `not-allowed` if you run your own notification system.
* `preApproved` — create the payout already approved. Requires the `payouts:approve` scope; see [Approve and execute payouts](/guides/approve-and-execute-payouts).

Currencies: `USD`, `EUR`, `CHF`, `GBP`. Amounts are decimal strings, minimum `0.1`.

For every parameter and the full response schema, see the generated **Payouts** reference pages.

## Balance enforcement [#balance-enforcement]

For teams with balance enforcement enabled (the default), pre-approved payouts are rejected with HTTP 422 and error code `INSUFFICIENT_BALANCE` when the total of all open (approved but not yet paid out) payouts would exceed the team wallet balance. The error `data` contains `walletBalance`, `totalOpenAmount`, and `currency`.

## Sandbox test scenarios [#sandbox-test-scenarios]

In the sandbox, every payout method has a passing and a failing scenario. **Success is the default**: any regular recipient details settle the payout instantly and synchronously, including the webhook events a real payout would emit. To exercise the failure path, have the recipient claim with these magic values:

| Method                                                                                                            | Failure trigger                                                               |
| ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| SEPA (`bank-iban`), SWIFT (`bank-swift`), ACH (`bank-ach`), Fedwire (`bank-wire`), UK Faster Payments (`bank-uk`) | Account holder name containing **SANDBOX FAIL**                               |
| PayPal (`paypal`)                                                                                                 | Recipient email containing **sandbox-fail** (e.g. `sandbox-fail@example.com`) |
| Venmo (`venmo`)                                                                                                   | Recipient phone number ending in **0000**                                     |
| Crypto (`crypto`)                                                                                                 | Destination wallet address `0x000000000000000000000000000000000000dEaD`       |

A triggered failure behaves like a real provider rejection: the transfer is dispatched, then declined, and the payout never reaches `completed` (no `completed` webhook fires; the payout stays `requested` while the failure is handled).


# Create hosted sessions (/guides/hosted-sessions)



The four `POST /session/*` endpoints mint stateless, unauthenticated Talentir URLs you send your users to — no iframe or UI work on your side. The endpoint validates the resource for the authenticated team (with OAuth this can be a customer's team that connected your app) and returns a `url`.

Sessions are **stateless**: nothing is stored. **Treat the returned `url` as opaque** — redirect the user to it as-is; do not build, parse, or depend on its shape. The URL itself confers no standing access — every screen still authenticates the visitor and gates the action — so a link is safe to email or hand to its intended recipient.

## Two audiences — send each link to the right person [#two-audiences--send-each-link-to-the-right-person]

| Endpoint             | Audience                                                | Purpose                              |
| -------------------- | ------------------------------------------------------- | ------------------------------------ |
| `/session/payout`    | The payout **recipient** (a creator, not a team member) | Sign up / sign in and claim a payout |
| `/session/approval`  | A **member** of the authenticated team                  | Review and approve pending payouts   |
| `/session/kyb`       | A team member                                           | Complete business verification       |
| `/session/allowance` | The team **owner**                                      | Set the daily spending allowance     |

**Team links** (`/session/approval`, `/session/kyb`, `/session/allowance`) are for members of the team the token authorizes. The screen requires the visitor to sign in *and* be a member of that team — anyone else gets a "request access" prompt. The sign-in code is pre-sent to the session creator; the optional `loginHint` field overrides who receives it.

**The recipient link** (`/session/payout`) is for the person being paid, who is **not** a member of your team. The screen shows the payout and lets the recipient sign in or sign up with their own email and claim it; no team membership is required.

Do not send a team link to a recipient or vice versa.

## Non-members can request access [#non-members-can-request-access]

It is safe to send a team-scoped session link to someone who is not (yet) a member of the team: after signing in they see a "Request access" screen instead of the flow. Requesting access emails the team's owner(s) with the requester's name and email and a one-click link to invite them from team settings — access is never granted automatically. Once invited and accepted, the same session link works.

## Wallet setup [#wallet-setup]

The team-member flows all operate on the team's wallet — a passkey wallet held by the **team owner**. If the team has not set one up yet, the hosted screen first prompts the owner to create it and then continues to the requested flow. Only the owner can complete this step; a non-owner member is asked to have the owner do it. A team must finish wallet setup before its payouts can be approved or executed. The recipient claim flow is unaffected — it uses the recipient's own wallet.

## Common parameters [#common-parameters]

| Parameter     | Required | Effect                                                                                                                                     |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `redirectUrl` | yes      | Where the user is returned after completing or leaving the screen — also the target of the screen's "Go Back" control. Must be http/https. |
| `branding`    | no       | `talentir` (default), `team`, or `platform`. Selects the host and logo for the hosted flow.                                                |

Each endpoint adds its own input — `payoutId` (payout), `payoutIds` (approval), and optional `loginHint` values for the team-member flows. All require the `sessions:write` scope, which is granted by default.

## White-label branding [#white-label-branding]

| `branding`           | Hosted on                                                                                | Requires                                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `talentir` (default) | The main Talentir host                                                                   | —                                                                                                         |
| `team`               | The **authenticated team's** white-label subdomain (`{slug}.talentir.com`) with its logo | The team's white-label feature + configured subdomain                                                     |
| `platform`           | The subdomain and logo of the **team operating your OAuth client**                       | An OAuth-authenticated call with a dashboard-created client, and the operating team's white-label feature |

`platform` is the mode for platforms embedding Talentir-hosted flows in their own product: the session operates on the connected customer team, while the page chrome carries *your* brand. It is rejected for API-key calls (an API key has no operating platform). The deprecated `whitelabel` field on `/session/payout` is ignored — use `branding: "team"` instead. White-label features and subdomains are provisioned by Talentir as part of the partner program.

Sandbox and preview hosts do not provide per-team subdomains. On these hosts, Talentir keeps the session on the current host and carries the verified brand in a signed session token.

## Examples [#examples]

**Team link** — a member of the authenticated team approves payouts:

```bash
curl -X POST "$BASE_URL/session/approval" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "branding": "platform",
    "payoutIds": ["0e4ba886-0bfe-4b6a-ae2e-d6d9d135dd1e"],
    "redirectUrl": "https://yourapp.com/done"
  }'
```

**Recipient link** — the creator being paid claims their payout:

```bash
curl -X POST "$BASE_URL/session/payout" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payoutId": "0e4ba886-0bfe-4b6a-ae2e-d6d9d135dd1e",
    "redirectUrl": "https://yourapp.com/done"
  }'
```


# Set up webhooks (/guides/webhooks)



Register a webhook to stay informed about payout status changes instead of polling.

## Manage subscriptions [#manage-subscriptions]

* `POST /webhook` — subscribe a URL to events.
* `GET /webhook` — list your subscriptions.
* `DELETE /webhook/{id}` — remove one.

Webhooks live in the environment they were created in: a webhook registered via the sandbox API only fires for sandbox events, and one registered via the production API only fires for production events.

## Event payload [#event-payload]

When events occur, webhooks receive HTTP POST requests with a JSON payload that matches the payout schema returned by `GET /payout/{id}`.

Optional/nullable fields are omitted from the JSON payload entirely when they are not present (they are not sent as `null`). Check for the absence of a property rather than checking if it equals `null`.

## Event triggers [#event-triggers]

Webhooks fire for the payout lifecycle events:

* Payout creation (status `created`)
* Payout approval (status `approved`)
* Payout request (status `requested`)
* Payout completion (status `completed`)
* Payout deletion (status `deleted`)
* Payout expiration (status `expired`)

## Delivery behavior [#delivery-behavior]

**Method**: POST, `Content-Type: application/json`.

**Request headers**:

* `X-Talentir-Signature`: HMAC-SHA256 signature for payload verification
* `X-Talentir-Timestamp`: Unix timestamp (seconds) when the webhook was sent

**Retry behavior**:

* **Server errors (5xx)**: retried up to 10 times with exponential backoff (10s, 20s, 40s, 80s, 160s, 320s, 640s, 1280s, 2560s, 5120s). Total retry window is approximately 2.8 hours.
* **Rate limiting (429)**: retried after a 60 second delay.
* **Gone (410)**: the webhook subscription is automatically deleted. No retry.
* **Other client errors (4xx)**: not retried. These indicate a permanent failure (e.g. invalid endpoint configuration).
* **Success (2xx)**: delivery confirmed.

## Verify signatures [#verify-signatures]

Every webhook request includes a cryptographic signature. **Always verify webhook signatures** to ensure requests are genuinely from Talentir.

1. Extract the signature and timestamp from the request headers.
2. Construct the signed payload: `{timestamp}.{raw_request_body}`.
3. Compute HMAC-SHA256 using your webhook's signing secret.
4. Compare the computed signature with `X-Talentir-Signature` using timing-safe comparison.
5. Optionally, reject requests with timestamps older than 5 minutes to prevent replay attacks.

```typescript
import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  timestamp: string,
  signingSecret: string
): boolean {
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = createHmac('sha256', signingSecret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison to prevent timing attacks
  const sigBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expectedSignature);
  if (sigBuffer.length !== expectedBuffer.length) return false;

  return timingSafeEqual(sigBuffer, expectedBuffer);
}

// In your webhook handler:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-talentir-signature'];
  const timestamp = req.headers['x-talentir-timestamp'];
  const payload = JSON.stringify(req.body);

  // Reject requests older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return res.status(400).send('Timestamp too old');
  }

  if (!verifyWebhookSignature(payload, signature, timestamp, YOUR_SIGNING_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook...
});
```


# Breaking changes (/concepts/breaking-changes)



Every breaking change is announced here before it takes effect. A deprecated field keeps working until its removal date. Additive changes — new endpoints, new optional fields, new enum values — are not breaking and are not listed, so write clients that ignore fields they do not know.

## Scheduled removals [#scheduled-removals]

| Date       | Endpoint    | Change                                                                   |
| ---------- | ----------- | ------------------------------------------------------------------------ |
| 2026-09-23 | `GET /team` | `balances[].depositInfo` is removed. Use the top-level `deposits` array. |

## Deprecated, no removal date yet [#deprecated-no-removal-date-yet]

| Deprecated since | Endpoint                                           | Field                                                            | Use instead                                         |
| ---------------- | -------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------- |
| 2025-10-27       | `GET /payout/{id}`, `GET /payouts`                 | `uuid`                                                           | `id`                                                |
| 2025-12-04       | `GET /team`                                        | `teamUuid`                                                       | `id`                                                |
| 2026-02-11       | `POST /payout`, `GET /payout/{id}`, `GET /payouts` | `handleType`                                                     | `verificationMethod`                                |
| 2026-06-21       | `GET /payout/{id}`, `GET /payouts`                 | `url`                                                            | A fresh claim URL from `POST /session/payout`       |
| 2026-07-28       | `POST /session/payout`                             | `whitelabel` (accepted but ignored)                              | `branding: "team"`                                  |
| 2026-07-28       | OAuth                                              | Dynamic client registration (RFC 7591) for platform integrations | An OAuth client created in Settings → OAuth Clients |


# Errors and retries (/concepts/errors-and-retries)



## Error format [#error-format]

Errors are JSON with a machine-readable `code`, an HTTP `status`, a human-readable `message`, and sometimes structured `data`:

```json
{
  "code": "INSUFFICIENT_BALANCE",
  "status": 422,
  "message": "Insufficient balance to approve this payout",
  "data": {
    "walletBalance": "50.00",
    "totalOpenAmount": "150.00",
    "currency": "EUR"
  }
}
```

## Common codes [#common-codes]

| Status | Code                      | Meaning                                                                                                 |
| ------ | ------------------------- | ------------------------------------------------------------------------------------------------------- |
| 401    | `UNAUTHORIZED`            | Missing or invalid token                                                                                |
| 403    | `FORBIDDEN`               | Token lacks a required scope or permission                                                              |
| 404    | `NOT_FOUND`               | Resource does not exist for the authenticated team                                                      |
| 409    | `CONFLICT`                | Update rejected, e.g. an immutable payout                                                               |
| 422    | `INPUT_VALIDATION_FAILED` | The request body failed validation; `message` is a readable summary and `data` carries per-field issues |
| 422    | `INSUFFICIENT_BALANCE`    | Pre-approved payout exceeds the team wallet balance                                                     |
| 500    | `INTERNAL_SERVER_ERROR`   | Unexpected server fault                                                                                 |

## Retry guidance [#retry-guidance]

* **4xx** errors are yours to fix — retrying the identical request fails again (except 429).
* **5xx** errors and network timeouts are safe to retry with exponential backoff. Payout creation is an upsert keyed on `customId`, so a retried create never duplicates a payout — see [Idempotency](/concepts/idempotency).
* Write clients that **ignore unknown fields**: additive changes (new endpoints, new optional fields, new enum values) are not breaking and happen without notice. See [Breaking changes](/concepts/breaking-changes).


# Idempotency (/concepts/idempotency)



`POST /payout` is an &#x2A;*upsert keyed on `customId`**. There is no separate idempotency-key header.

## How it works [#how-it-works]

Give every payout a stable `customId` from your own system (an order ID, a campaign line, an invoice number):

* If no payout with that `customId` exists for your team, one is created.
* If one exists, the request **updates** it instead of creating a duplicate.

The response tells you which happened:

```json
{
  "id": "0e4ba886-0bfe-4b6a-ae2e-d6d9d135dd1e",
  "customId": "campaign-42",
  "status": "created",
  "action": "updated"
}
```

So a timeout-and-retry loop is safe: send the same request again and you get the same payout back with `action: "updated"`.

## Locking a payout down [#locking-a-payout-down]

If the payout must not change after creation — for example when the request data comes from a signed source — create it with `payoutType: "manual-immutable"`. Source-of-truth fields then cannot be updated afterwards; an attempt returns HTTP 409.

## Look up by your own ID [#look-up-by-your-own-id]

`GET /payout/{id}` accepts either the Talentir `id` (UUID) or your `customId` — pass `id_type=custom_id` as a query parameter. See the generated **Payouts** reference for details.

## What is not idempotent [#what-is-not-idempotent]

Session minting (`POST /session/*`) returns a fresh URL each call; that is by design (URLs are stateless and disposable). Webhook subscription creation is not keyed — check `GET /webhook` before subscribing if you might retry.


# Payout lifecycle (/concepts/payout-lifecycle)



Every payout moves through a small state machine. Each transition emits a [webhook event](/guides/webhooks) whose payload matches the `GET /payout/{id}` schema.

## States [#states]

| Status      | Meaning                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `created`   | The payout exists and is pending approval by the sending team. The recipient can already be invited to claim.                              |
| `approved`  | The sending team approved the payout (dashboard, approval session, or `preApproved` creation). Funds are reserved against the team wallet. |
| `requested` | The recipient claimed the payout and chose a payout method; the transfer has been requested from the payment provider.                     |
| `completed` | The transfer settled. Terminal.                                                                                                            |
| `deleted`   | The payout was deleted before completion. Terminal.                                                                                        |
| `expired`   | The payout expired unclaimed. Terminal.                                                                                                    |

The happy path is `created` → `approved` → `requested` → `completed`.

## Notes [#notes]

* Approval order is not fixed: a recipient can claim before approval — the transfer waits until the payout is both approved and funded.
* A provider rejection after `requested` does not complete the payout: it stays `requested` while the failure is handled (in the sandbox you can force this with the failure triggers listed in [Create a payout](/guides/create-a-payout)).
* `availableOn` delays claimability, not creation: the payout exists immediately but can only be claimed from that date.
* Deleting is only possible while no transfer is in flight.


# Scopes and permissions (/concepts/scopes-and-permissions)



Every token — team API key or OAuth access token — carries scopes that gate what it can do.

## Scopes [#scopes]

| Scope             | Grants                                                                                                                                                                                                              |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `team:read`       | Read team information                                                                                                                                                                                               |
| `team:manage`     | Update team settings (DAC7 reporting)                                                                                                                                                                               |
| `payouts:read`    | List and read payouts                                                                                                                                                                                               |
| `payouts:write`   | Create payouts (kept pending; no approval/execution)                                                                                                                                                                |
| `payouts:approve` | Create payouts as pre-approved. **Restricted** — only granted to admin-provisioned OAuth clients and to API keys for teams with the `payout.api_approve` permission. Not available via dynamic client registration. |
| `webhooks:read`   | List webhook subscriptions                                                                                                                                                                                          |
| `webhooks:write`  | Create and delete webhook subscriptions                                                                                                                                                                             |
| `sessions:write`  | Mint hosted session URLs (granted by default)                                                                                                                                                                       |

For OAuth, include `offline_access` in the scope list to receive a refresh token.

## Which team does a token act on? [#which-team-does-a-token-act-on]

* A **team API key** is scoped to the team that owns the key.
* An **OAuth access token** is scoped to the team the user selected during authorization, carried in the `https://talentir.com/oauth/team_id` claim. For platforms this is usually a customer's team, not your own.

## Safety model [#safety-model]

`payouts:write` alone can never move money: created payouts stay pending until a team member approves them (see [Approve and execute payouts](/guides/approve-and-execute-payouts)). Money movement always requires either the restricted `payouts:approve` scope or an explicit human approval backed by the team's passkey wallet and daily allowance.


# Create (/reference/payouts/payout.create)

Create or update a new payout for team members.

For teams with balance enforcement enabled (the default), pre-approved payouts (`preApproved: true`) are rejected with HTTP 422 and error code `INSUFFICIENT_BALANCE` when the total of all open (approved but not yet paid out) payouts would exceed the team wallet balance. The error `data` contains `walletBalance`, `totalOpenAmount`, and `currency`.

# Get (/reference/payouts/payout.get)

Retrieve a specific payout by ID or custom ID.

By default, the endpoint expects an ID. To query by custom ID instead, add the query parameter `id_type=custom_id`.

Examples:
- Get by ID: `GET /payout/{id}`
- Get by custom ID: `GET /payout/{custom_id}?id_type=custom_id`

# List (/reference/payouts/payout.list)

Retrieve a list of payouts for the authenticated team.

# Payout (/reference/sessions/session.payout)

Audience: the payout RECIPIENT — the creator/rightsholder being paid, who is not a member of the authenticated team. Validate a payout for the authenticated team and return a Talentir URL to send to that recipient: they sign in or sign up with their own email (inferred from the payout) and claim the payout. No team membership is required.

# Approval (/reference/sessions/session.approval)

Audience: a MEMBER OF THE AUTHENTICATED TEAM (the team the token authorizes — with OAuth this can be a customer's team, not your own). Validate pending payouts for that team and return a Talentir URL where a team member reviews and approves them. The screen requires the visitor to sign in and be a member of that team; the sign-in code is pre-sent to the session creator's email.

# Kyb (/reference/sessions/session.kyb)

Audience: a MEMBER OF THE AUTHENTICATED TEAM (the team the token authorizes — with OAuth this can be a customer's team, not your own). Validate the authenticated team and return a Talentir URL where a team member completes KYB (Know Your Business) verification. The screen requires the visitor to sign in and be a member of that team; the sign-in code is pre-sent to the session creator's email.

# Allowance (/reference/sessions/session.allowance)

Audience: the OWNER of the authenticated team (the team the token authorizes — with OAuth this can be a customer's team, not your own). Returns a Talentir URL where the team owner sets or edits the daily spending allowance for automatic payouts. The screen requires the visitor to sign in and be the team owner; the sign-in code is pre-sent to the session creator's email.

# Get (/reference/team-management/team.get)

Retrieve information for the authenticated team: name, KYB status, total converted wallet balance, balances, bank deposit instructions per currency, and the team's members with their roles (owners first). The per-balance 'depositInfo' field is deprecated in favor of the top-level 'deposits' array and will be removed on 2026-09-23.

# Update (/reference/team-management/team.update)

Update settings of the authenticated team. Currently: whether the team's payouts are reportable under DAC7, which decides if EU-resident payees must provide their tax identity before claiming.

# List (/reference/webhooks/webhook.list)

List all webhooks for your team. Webhooks are per environment: a webhook registered on the sandbox only fires for sandbox events, one registered on production only fires for production events.

# Create (/reference/webhooks/webhook.create)

Create a new webhook to receive event notifications. Webhooks live in the environment they were created in: register on sandbox.talentir.com to receive sandbox events, and on www.talentir.com to receive production events.

# Delete (/reference/webhooks/webhook.delete)

Delete a webhook