Pay on behalf of your customers
Connect your customers' Talentir teams through OAuth, walk them through verification and funding, and create their first payout from your product.
A platform integration creates payouts for its customers, not for itself. Each customer has their own Talentir team, which holds the balance, carries the verification, and approves the payouts. Your product connects to that team through OAuth, and every Talentir screen the customer or their recipients need is a hosted session you mint and redirect to.
The steps below use $ACCESS_TOKEN, the OAuth token of the connected team, where a single-team integration would use an API key.
The onboarding lifecycle
A customer passes through five stages before their first payout can settle. Your product drives each stage and reads the team's state from the API in between.
| Stage | Who acts | Your calls | Done when |
|---|---|---|---|
| Connect | The customer, on the OAuth screens | /authorize, then /token | You hold tokens for the team |
| Verify | The team owner, on a hosted kyb screen | GET /team, POST /sessions | kybStatus is verified |
| Fund | The customer, at their bank | GET /team, GET /team/accounts | missing is empty and balance.available covers the payout |
| Pay | Your product | POST /payouts | The payout is created |
| Approve and send | A team member, on a hosted payout_approval screen; then your product | POST /sessions, POST /payouts/{id}/send-claim-link | The payout is completed |
Every stage can be re-entered. Each time the customer opens your integration page, read GET /team and GET /team/accounts and show the next step they need. Do not treat the return to your redirectUrl as proof that a hosted screen was completed: the customer can leave a screen early, and verification can still be in review.
1. Create an OAuth client
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: teams created through your authorize flow are credited to your platform, and hosted sessions can carry your white-label branding. Partner-program enrollment (referral terms, white-label) is handled by Talentir; contact us to enroll.
2. Connect a customer's team
Redirect the customer to Talentir
Generate a PKCE verifier and challenge and a random state, store both against the customer's session, and redirect the customer to the authorize endpoint:
https://www.talentir.com/api/auth/oauth2/authorize?response_type=code&client_id=<client_id>
&redirect_uri=<redirect_uri>
&scope=team:read payouts:read payouts:write counterparties:read counterparties:write webhooks:write sessions:write offline_access
&code_challenge=<challenge>&code_challenge_method=S256
&resource=https://www.talentir.com/api/v2
&state=<state>
&login_hint=finance@customer.com
&team_name=Customer%20GmbHOn the hosted screens the customer signs in or signs up, selects or creates their team, and consents to the scopes.
- PKCE is mandatory, and
resourcemust be sent on both/authorizeand/token. offline_accessgives you a refresh token, so the connection survives the access token's expiry.login_hintpre-fills the email and sends the one-time sign-in code right away.team_namepre-fills the team name when a new customer has no team yet. Pass the company name from your records, so the team is created under the right name.team_hintpre-selects a team when the customer connects a second time.
The protocol details, discovery document, and every hint are in OAuth 2.1 with PKCE. The scopes are listed in Scopes and permissions.
Handle the callback
Talentir sends the customer back to your redirect_uri with code and state in the query string. Check that state matches what you stored, then exchange the code on your server:
curl -X POST "https://www.talentir.com/api/auth/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "redirect_uri=<redirect_uri>" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "code_verifier=<verifier>" \
-d "resource=https://www.talentir.com/api/v2"{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "team:read payouts:read payouts:write counterparties:read counterparties:write webhooks:write sessions:write offline_access"
}Store the refresh token against the customer. The access token expires after an hour; get a new one with grant_type=refresh_token, the stored refresh_token, and the same resource. If the customer declines consent, the callback carries error=access_denied instead of a code.
Read the connected team
The token acts on the team the customer selected. Read it once and save its id with the customer record:
curl "$BASE_URL/team" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "team",
"id": "team_4399dd07-cf1b-414b-bfbc-91a88dd73ec5",
"name": "Customer GmbH",
"legalName": "Customer GmbH",
"address": {
"line1": "Musterstraße 1",
"line2": null,
"city": "Berlin",
"state": null,
"postalCode": "10115",
"country": "DE"
},
"kybStatus": "not_submitted",
"missing": ["wallet", "kyb"],
"createdAt": "2026-09-24T10:00:00.000Z",
"updatedAt": "2026-09-24T10:00:00.000Z"
}kybStatus and missing are the fields that drive the next stages. Register the team's webhook now as well, so the events of the first payout reach you.
3. Get the team verified
A team cannot approve payouts until it has set up its wallet, completed business verification (KYB), and made its first deposit. Two fields of GET /team show how far the team is: missing and kybStatus.
missing lists every step the team must still complete before it can approve payouts. It is empty when nothing but the balance and the allowance blocks approval. It is an open enum: treat a value you do not know as a step that is left.
| Value | Step |
|---|---|
wallet | The owner sets up the team wallet |
kyb | Business verification |
first_deposit | The first bank deposit activates the verified account. kybStatus already reads verified at this point. See Fund the balance |
address | The registered business address is incomplete |
vat_number | An EU team has no VAT number, or it does not match the address country |
kybStatus is the state of the verification. Show the customer the matching state:
kybStatus | Meaning | What your product shows |
|---|---|---|
not_submitted | The owner has not submitted the application yet | A button that sends the owner to the verification screen |
in_review | Submitted, Talentir is reviewing | "Verification in review". Nothing to do; poll GET /team until it changes |
verified | Talentir verified the business | Continue to funding |
denied | Rejected | Ask the customer to contact Talentir support |
While the status is not_submitted, mint the verification screen and redirect the owner to it:
curl -X POST "$BASE_URL/sessions" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "kyb",
"redirectUrl": "https://yourapp.com/onboarding/verification",
"branding": "platform"
}'The team owner completes verification on the hosted page. If the team has no wallet yet, the same screen first prompts the owner to create it (a passkey wallet), then continues. The sign-in code goes to the member who connected the team; set loginHint to another member's email when someone else should complete it.
A kyb session can only be minted while the status is not_submitted. Once the application is submitted, the request fails with 409 KYB_ALREADY_SUBMITTED, so check kybStatus first instead of catching the error.
When the customer returns to your redirectUrl, read GET /team again. In production the status is now in_review, and it becomes verified after Talentir's review. In the sandbox, the screen offers a one-click Complete instantly button, and the status is verified on return.
You can already create payouts while verification is in review. Only approval, send-claim-link, and pay are blocked; they fail with 422 VERIFICATION_REQUIRED until missing is empty, and details.missing says what is left.
4. Fund the balance
Payouts are approved against the team's balance, so the customer must wire money in before the first approval. GET /team/accounts returns one account per settlement currency, with its balance and, once verification is complete, the bank details to fund it:
curl "$BASE_URL/team/accounts" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "list",
"data": [
{
"object": "account",
"currency": "EUR",
"balance": { "total": "0.00", "reserved": "0.00", "available": "0.00" },
"depositInstructions": [
{
"type": "bank_iban",
"ownership": "dedicated",
"beneficiary": "Customer GmbH",
"iban": "CH93 0076 2011 6238 5295 7",
"bic": "KLARCH22XXX",
"bankName": "Bivial AG",
"bankAddress": "Industriestrasse 24, 6300 Zug, Switzerland",
"reference": null
}
]
}
]
}Show the customer a "Fund your account" page built from this response:
- Pick the account whose
currencythe customer wants to fund. Transfers must be sent in that currency. depositInstructionsis ordered by preference; show the first entry, and offer the others (for examplebank_swiftfor a transfer from outside SEPA) as alternatives.typeuses the payment method type names. The fields differ bytype:ibanandbicforbank_iban,sortCodeandaccountNumberforbank_uk,routingNumberandaccountNumberforbank_achandbank_wire.typeis an open enum. Skip an instruction whosetypeyou do not know.- Always show
beneficiaryas the recipient name of the transfer. - When
referenceis notnull, the transfer must carry it as the payment reference. Without it the money cannot be attributed to the team. It isnullwhen the account itself belongs to the team (ownershipisdedicated). depositInstructionsis empty untilkybStatusisverified. Send the customer back to verification in that case.
There is no webhook for an incoming deposit. Poll GET /team and GET /team/accounts, and unlock the next step when missing is empty and balance.available covers the payouts you plan to create. The first deposit removes first_deposit from missing. total is everything the account holds, reserved is the sum of approved payouts that are not paid yet, and available is total minus reserved: what a new approval can draw on.
In the sandbox, no bank transfer is needed: the customer opens Wallet in the sandbox dashboard and clicks Deposit 1,000 EUR. The balance updates immediately.
5. Create the first payout
Create payouts with the team's token exactly as for your own team. The simplest form needs only the recipient's email:
curl -X POST "$BASE_URL/payouts" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customId": "invoice-1001",
"description": "Payment for September campaign",
"amount": "100.00",
"currency": "EUR",
"recipient": { "type": "email", "email": "jane@example.com" }
}'The response is the payout in status created. Nothing moves yet: the payout waits for the customer's approval. Use customId to carry your own reference, so {id} in every later call can be your key instead of the payout_… id.
Every payout field, the batch form, and the sandbox failure scenarios are in Pay with a payout link. To push money to a saved bank account, PayPal, or crypto wallet instead, create the counterparty in the customer's book and follow Pay directly.
6. Let the customer approve
An OAuth client typically cannot approve through the API. Instead, your product shows the payouts that are ready and sends a team member to a hosted approval screen. Before you do, check the two preconditions yourself, so the customer does not hit a wall on the screen: missing on GET /team is empty, and balance.available covers the sum of the payouts.
curl -X POST "$BASE_URL/sessions" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "payout_approval",
"payoutIds": ["payout_0e4ba886-0bfe-4b6a-ae2e-d6d9d135dd1e"],
"branding": "platform",
"redirectUrl": "https://yourapp.com/payouts"
}'The member reviews and approves with their passkey, then returns to your redirectUrl. Approval moves the payout to approved and reserves the amount; you see it as payout.approved on the webhook or as status on GET /payouts/{id}. See Approve payouts for the other options and the errors approval can return.
If Talentir has granted your client payouts:approve, approval runs without a human in the loop, and the team owner must first set a daily spending allowance. Mint that screen with type: "allowance". With that scope, andThen: "approve_and_send_claim_link" on create does steps 5 to 7 in one request.
7. Send the payout
An approved payout waits for you to send it. For a payout to an email address, let Talentir email the claim link:
curl -X POST "$BASE_URL/payouts/invoice-1001/send-claim-link" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"The recipient signs in with a one-time code, chooses a payout method, and claims. The claim moves the payout to requested, and completed follows when the transfer settles; in the sandbox that happens instantly. To send the link from your own product instead, mint it with type: "payout_claim"; add branding: "platform" to show your brand on the claim screen. For a payout to a counterparty, call POST /payouts/{id}/pay.
8. Receive events
Webhooks are per team. Register one for each connected team with its token, and verify the signature with that team's secret. A webhook stops when the customer revokes your app:
curl -X POST "$BASE_URL/webhooks" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/talentir", "events": ["payout.*", "counterparty.*"] }'Every event carries teamId, the id of the team it belongs to, so one URL can serve every connected team. Events cover payouts and counterparties only. Verification and deposits have no events; poll GET /team and GET /team/accounts for those. See Set up webhooks for the payload and signature.
Hosted sessions
POST /sessions mints a signed, stateless Talentir URL you send a user to. No iframe or UI work on your side. The endpoint validates the resource for the authenticated team and returns a session with a url.
Sessions are stateless: nothing is stored and there is no GET. Treat the returned url as opaque; redirect the user to it as is, and do not parse or build it. The URL confers no standing access, since every screen authenticates the visitor and gates the action, so a link is safe to email or hand to its intended recipient.
Types and audiences
type | Audience | Purpose | Extra fields |
|---|---|---|---|
payout_claim | The payout recipient (not a team member) | Sign up or sign in and claim a payout | payoutId |
payout_approval | A member of the authenticated team | Review and approve created payouts | payoutIds (at least one), optional loginHint |
kyb | A team member | Complete business verification | optional loginHint |
allowance | The team owner | Set the daily spending allowance | optional loginHint |
A payout_approval session checks every payout before it mints the URL, with the codes of POST /payouts/approve: an unknown payout fails with 404 PAYOUT_NOT_FOUND, and a payout that is not created fails with 409 PAYOUT_NOT_APPROVABLE. error.index is the position of that payout in payoutIds.
Team links (payout_approval, kyb, 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. The sign-in code is pre-sent to the session creator; the optional loginHint field overrides who receives it.
The recipient link (payout_claim) is for the person being paid, who is not a member of the team. The screen shows the payout and lets the recipient sign in or sign up with their own email and claim it.
Do not send a team link to a recipient or vice versa.
It is safe to send a team link to someone who is not yet a member: after signing in they see a "Request access" screen. Requesting access emails the team's owner(s) with the requester's name and email and a one-click link to invite them. Access is never granted automatically. Once invited and accepted, the same session link works.
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. The recipient claim flow uses the recipient's own wallet and is unaffected.
Common fields
| Field | Required | Effect |
|---|---|---|
type | yes | The screen, see above. |
redirectUrl | yes | Where the user is returned after completing or leaving the screen, and the target of the screen's "Go Back" control. Must be an absolute https URL (http only for localhost). |
branding | no | talentir (default), team, or platform. Selects the host and logo for the hosted flow. |
All session types require the sessions:write scope, which is granted by default.
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 and a 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: the session operates on the connected customer team, while the page chrome carries your brand. It is rejected for API-key calls with 403 BRANDING_UNAVAILABLE, since an API key has no operating platform. The same code refuses team or platform when the branded team has no white-label feature or subdomain.
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.
Response
{
"object": "session",
"id": "session_…",
"type": "payout_claim",
"url": "https://www.talentir.com/…",
"createdAt": "2026-09-24T10:00:00.000Z"
}The URL does not expire.