# Docs


- **Getting started**
- [Introduction](/): Integrate with the Talentir platform to pay creators, sellers, and freelancers worldwide.
- [Quickstart](/quickstart): Send your first payout from the sandbox in a few minutes.
- Authentication
  - [Team API keys](/authentication/api-keys): Authenticate server-to-server calls with a key scoped to your own team.
  - [OAuth 2.1 with PKCE](/authentication/oauth): Let your customers connect their Talentir teams to your platform.
- Guides
  - [Create a payout](/guides/create-a-payout): Send a payment to a creator identified by handle, email, or wallet address.
  - [Create hosted sessions](/guides/hosted-sessions): Mint Talentir-hosted URLs for claiming, approval, KYB, and allowance flows.
  - [Approve and execute payouts](/guides/approve-and-execute-payouts): Move payouts from pending to paid, with a human or programmatic approval step.
  - [Set up webhooks](/guides/webhooks): Receive real-time notifications for payout lifecycle events.
- Concepts
  - [Payout lifecycle](/concepts/payout-lifecycle): The states a payout moves through, and which webhook events they emit.
  - [Scopes and permissions](/concepts/scopes-and-permissions): What each API scope grants, and which are restricted.
  - [Idempotency](/concepts/idempotency): Retry payout creation safely with customId upserts.
  - [Errors and retries](/concepts/errors-and-retries): The error format, common error codes, and how to retry safely.
  - [Breaking changes](/concepts/breaking-changes): Scheduled removals and deprecated fields.
- Reference
  - Payouts: Create and manage payouts to creators, influencers, and rightsholders with support for multiple currencies and various creator identification methods.

### 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](/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.
  - Sessions: ## Hosted Sessions

Session endpoints mint a Talentir URL that drops a user into a hosted screen — a payout claim, a payout approval, or KYB verification — without building any of that UI yourself. The endpoint validates the resource for the authenticated team (the team the token authorizes — with OAuth this can be a customer's team that connected your app, not your own) 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 format is an internal detail and may change). The URL itself confers no standing access — every screen still authenticates the visitor and gates the action (see each audience below) — so a link is safe to email or hand to its intended recipient.

## ⚠️ Two audiences — send each link to the right person

These endpoints split into **two groups with different intended users**. Pick the endpoint by *who will open the link*.

### 1. A member of the authenticated team → `POST /session/approval`, `POST /session/kyb`

For **a member of the team the token authorizes** to approve payouts or complete that team's KYB. The screen requires the visitor to **sign in *and* be a member of that team** — anyone else only gets a "request access" prompt. The sign-in code is pre-sent to the **session creator** (the OAuth authorizing user, or the team member who owns the API key), who can still switch to a different account. If the member who opens the link belongs to multiple teams, their active team is switched to this one. These are hosted product flows for the team's own members.

### 2. The payout recipient → `POST /session/payout`

For **the person being paid** — the creator / rightsholder, who is **not** a member of that team — to claim their payout. The screen shows the payout and lets the recipient **sign in or sign up with their own email** (inferred from the payout) and claim it; **no team membership is required**. The sign-in code goes to the recipient's email when known, otherwise they enter it themselves. This is a **self-serve claim + onboarding link to send to the recipient**.

> In short: **approval & KYB are for a member of the authenticated team; payout is for the external recipient being paid.** Do not send a team link to a recipient or vice versa.

## Wallet setup

The team-member flows (`/session/kyb`, `/session/approval`, `/session/allowance`) 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 passkey wallet is bound to their device); a non-owner member who opens the link is asked to have the owner do it. A team must therefore finish wallet setup before its payouts can be approved or executed. The recipient `/session/payout` claim flow is unaffected — it uses the recipient's own wallet, not the team's.

## 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. The deprecated `whitelabel` field on the payout endpoint is ignored; use `branding: "team"`. All require the `sessions:write` scope, which is granted by default.

## Examples

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

```bash
curl -X POST https://www.talentir.com/api/v1/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"
  }'
# → { "url": "<opaque Talentir URL — redirect a team member here>" }
```

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

```bash
curl -X POST https://www.talentir.com/api/v1/session/payout \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "payoutId": "0e4ba886-0bfe-4b6a-ae2e-d6d9d135dd1e",
    "redirectUrl": "https://yourapp.com/done"
  }'
# → { "url": "<opaque Talentir URL — send this to the payout recipient>" }
```
    - [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.
  - Team Management: Access team information and manage organizational settings for your Talentir account.
    - [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.
  - Webhooks: ## Webhook Management

Set up real-time event notifications to stay informed about payout status changes and other critical events.

### Sandbox vs Production

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. This lets you test your webhook integration in sandbox without touching your production webhook endpoints.

### Webhook Event Payload Schema

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

**Schema Reference**: For the complete field descriptions and types, refer to the `GET /payout/{id}` endpoint documentation.

**Note**: Optional/nullable fields are omitted from the JSON payload entirely when they are not present (they are not sent as `null`). This means you should check for the absence of a property rather than checking if it equals `null`.

### Webhook Delivery Behavior

**HTTP Method**: POST  
**Content-Type**: application/json  
**Payload**: Event-specific JSON data (see schema above)

**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, no further action needed.

### Signature Verification

Every webhook request includes a cryptographic signature to verify its authenticity. **You should always verify webhook signatures** to ensure requests are genuinely from Talentir.

**Headers Included**:
- `X-Talentir-Signature`: The HMAC-SHA256 signature
- `X-Talentir-Timestamp`: Unix timestamp when the request was signed

**Verification Steps**:

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

**Example (Node.js/TypeScript)**:
```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...
});
```

### Event Triggers

**Payout Events**: Webhooks are triggered for the following 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")
    - [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