API Docs
Guides

Pay directly

Save the people and companies you pay as counterparties, then push payouts to their bank account, PayPal, or crypto wallet without a claim link.

You already hold the recipient's details, or you collect them in your own form. Save the recipient as a counterparty: a person or company in your team's book with name, address, payment method, and tax identity. A payout addressed to a counterparty is pushed to its saved payment method with POST /payouts/{id}/pay. No claim link, no recipient sign-in, no Talentir screen.

1. Read the country

The identity and address fields of a counterparty are fixed; the tax identity and the payment method are not. Which tax fields a counterparty needs depends on its country and type, and on your team's DAC7 setting. They can change without a new API version. Do not hardcode them: read them from GET /countries when you build your form, and use the entry whose id is the address country.

curl "$BASE_URL/countries" \
  -H "Authorization: Bearer $TALENTIR_API_KEY"
{
  "object": "list",
  "data": [
    …,
    {
      "object": "country",
      "id": "DE",
      "name": "Germany",
      "taxRequirements": {
        "individual": {
          "fields": [
            {
              "key": "taxId",
              "label": "Tax identification number",
              "localName": "Steuernummer",
              "format": "10 to 13 digits, e.g. 123/456/78901",
              "description": "Use your Steuernummer for invoices and tax reporting. It is different from your personal Steuer-ID. …",
              "pattern": "^(\\d{10,13}|\\d{2,3}\\/\\d{3,4}\\/\\d{4,5})$",
              "example": null
            },
            { "key": "vatNumber", "label": "VAT number", "localName": null, "format": null, "description": null, "pattern": "^(DE)\\d{9}$", "example": "DE123456789" },
            { "key": "vatExempt", "label": "Not registered for VAT", "localName": null, "format": null, "description": null, "pattern": null, "example": null },
            { "key": "dateOfBirth", "label": "Date of birth", "localName": null, "format": null, "description": null, "pattern": null, "example": null }
          ],
          "anyOf": [["taxId", "dateOfBirth", "vatNumber"], ["taxId", "dateOfBirth", "vatExempt"]]
        },
        "organization": {
          "fields": [
            { "key": "taxId", "label": "Tax identification number", "localName": "Steuernummer of the company (or W-IdNr)", "format": "…", "description": "…", "pattern": "…", "example": null },
            { "key": "vatNumber", "label": "VAT number", "localName": null, "format": null, "description": null, "pattern": "^(DE)\\d{9}$", "example": "DE123456789" },
            { "key": "vatExempt", "label": "Not registered for VAT", "localName": null, "format": null, "description": null, "pattern": null, "example": null },
            { "key": "registrationNumber", "label": "Registration number", "localName": "Handelsregisternummer", "format": "HRA or HRB and a number, e.g. HRB 12345", "description": "…", "pattern": "^[A-Z]{2,3}\\d{1,7}[A-Z]{0,2}$", "example": null }
          ],
          "anyOf": [["taxId", "registrationNumber", "vatNumber"], ["taxId", "registrationNumber", "vatExempt"]]
        }
      }
    },
    …
  ]
}
  • taxRequirements.individual and taxRequirements.organization each list the fields the country takes for that type, in the order your form shows them. Each field has the key to send in taxIdentity, a label for your form, a pattern the value must match after uppercasing and removing whitespace, and an example where one exists.
  • localName, format, and description help the user fill in the field: what the country calls the number, what it looks like, and where to find it. Show them as help text next to the input. They are in English, and they are null where Talentir has no guidance. Today that is every field except the tax identification number in EU countries and the US, and the registration number in EU countries.
  • anyOf lists the accepted combinations as sets of keys. Send every field of any one set. In France, for example, an individual sends taxId, dateOfBirth, and a VAT status, or birthPlaceCity, birthPlaceCountry, dateOfBirth, and a VAT status. An empty list means no tax data is required.
  • In a country with VAT (the EU, Switzerland, and the United Kingdom), every set holds the VAT status: vatNumber when the counterparty is registered for VAT, or vatExempt: true when it is not, for example as a small business. Talentir does not assume either. A counterparty can be saved without a VAT status, for example a payer of a payment request, but it cannot be paid with an invoice: until it has one, missing contains tax_identity.
  • A field in fields but in no set is optional.
  • A US counterparty gives its Form W-9 data, whatever your DAC7 setting, unless Talentir turned W-9 collection off for your team. GET /countries shows whether the US fields are required. An individual sends taxId: an SSN, ITIN, or EIN. An organization sends taxClassification and taxId, its EIN. When taxClassification is single_member_llc, the organization also sends ownerName, and taxId is the owner's SSN, ITIN, or EIN. Until then, missing names tax_identity and the counterparty cannot be paid.
  • The rules already reflect your team's DAC7 setting; you never evaluate DAC7 yourself.

GET /countries lists every country a counterparty can live in, ordered by id. A country Talentir does not pay to is not listed, and a counterparty with an address there fails with VALIDATION_FAILED.

2. Create the counterparty

curl -X POST "$BASE_URL/counterparties" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customId": "creator-1042",
    "identity": { "type": "individual", "firstName": "Jane", "lastName": "Doe" },
    "email": "jane@example.com",
    "address": { "line1": "Musterstraße 1", "city": "Berlin", "postalCode": "10115", "country": "DE" },
    "paymentMethod": { "type": "sepa", "iban": "DE89370400440532013000", "currency": "EUR" },
    "taxIdentity": { "taxId": "12345678901", "dateOfBirth": "1990-05-04", "vatExempt": true }
  }'

identity.type is individual (with firstName and lastName) or organization (with name), and cannot change after creation. taxIdentity is checked against the country's rules for address.country and identity.type; a violation is 400 VALIDATION_FAILED with one issue per failing path. The VAT status is the exception: a save without it succeeds, and the counterparty lists tax_identity in missing.

The response is the counterparty. An empty missing means pay can succeed. Otherwise missing names the gaps: address, payment_method, or tax_identity. A payout with withInvoice: false needs no tax data, so pay ignores tax_identity for it. To list only the counterparties you can pay, use GET /counterparties?payable=true. A counterparty can be saved incomplete and completed later with PATCH /counterparties/{id}, where a nested object (address, paymentMethod, taxIdentity) is replaced as a whole and null clears it. Tax data belongs to one country: a PATCH that changes address.country without a new taxIdentity clears the tax identity, so send the tax data for the new country with the move.

Choose the payment method

paymentMethod.type picks the rail and the account details: sepa, ch_sic, gb_fps (sort code), gb_fps_iban, us_ach, us_fedwire, swift (account number and BIC), swift_iban, paypal, venmo, or crypto. Each type has one price and its own account fields, documented in the reference. The older types bank_iban, bank_swift, bank_ach, bank_wire and bank_uk still work in requests: bank_iban becomes the local type of its currency (sepa, ch_sic or gb_fps_iban) when that type pays to the IBAN's country, else swift_iban, and responses use the new type. Which currencies a fiat rail settles in, and which tokens crypto pays out, can change without a new API version. Do not hardcode them: read them from GET /payment-method-types when you build your form.

curl "$BASE_URL/payment-method-types" \
  -H "Authorization: Bearer $TALENTIR_API_KEY"
{
  "object": "list",
  "data": [
    { "object": "payment_method_type", "id": "sepa", "name": "SEPA", "currencies": ["EUR"], "assets": null, "bankCountries": ["AT", "BE", …], "replaces": "bank_iban" },
    …,
    {
      "object": "payment_method_type",
      "id": "crypto",
      "name": "Crypto",
      "currencies": null,
      "bankCountries": null,
      "assets": [
        { "id": "eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "network": "Base", "currency": "USD" },
        …
      ]
    }
  ]
}
  • id is the value for paymentMethod.type, and name is a label for your form.
  • currencies lists the codes a fiat rail takes as paymentMethod.currency. A currency the rail does not settle in is 400 VALIDATION_FAILED on paymentMethod.currency.
  • assets lists the tokens crypto takes as paymentMethod.asset, each with its network and the currency it is pegged to. A token without a peg, such as ETH, has currency: null.
  • bankCountries lists the bank countries a rail pays to at its price. For an IBAN, the country is its first two letters. Details for a bank in another country are 400 VALIDATION_FAILED on paymentMethod.iban: for example, a Turkish IBAN does not take sepa, so send it as swift_iban. null means every country, or a type without a bank account.

Pay to a crypto wallet

A crypto payment method takes the token to send as a CAIP-19 asset, and the walletAddress that receives it:

"paymentMethod": {
  "type": "crypto",
  "asset": "eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "walletAddress": "0x…"
}

Talentir checks that the address is valid on the asset's network, checksum included. An address that fails the check is 400 VALIDATION_FAILED on paymentMethod.walletAddress. An asset that is not in the crypto entry of GET /payment-method-types is 400 VALIDATION_FAILED on paymentMethod.asset. Talentir does not check who controls the wallet: you vouch for the address. A crypto transfer cannot be reversed, so confirm the address with the recipient before you pay.

A token without a peg (ETH, SOL, BTC) reads back with paymentMethod.currency: null. A payout to it sends the value of its amount in that token.

3. Create the payout and pay it

curl -X POST "$BASE_URL/payouts" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customId": "INV-42",
    "description": "Invoice 42",
    "amount": "100.00",
    "currency": "EUR",
    "recipient": { "type": "counterparty", "counterpartyId": "counterparty_7c1d2f30-…" },
    "andThen": "approve_and_pay"
  }'

The payout fields are the same as for a payout link; only the recipient differs.

To show the recipient what arrives after fees and currency conversion, estimate the payout before this request.

andThen: "approve_and_pay" creates, approves, and pays in one request. It needs payouts:approve and payouts:write, and either does all three steps or none: a counterparty whose missing is not empty fails the whole request with 422 COUNTERPARTY_NOT_PAYABLE and leaves no payout behind. The response is the payout in status requested.

Without payouts:approve, create the payout, have it approved, then call:

curl -X POST "$BASE_URL/payouts/INV-42/pay" \
  -H "Authorization: Bearer $TALENTIR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

pay needs payouts:write: it moves what approval already reserved. Paying a failed payout again also needs payouts:approve (see Handle the outcome). It is refused with 409 PAYOUT_NOT_PAYABLE when the payout is not approved or failed, with 422 RECIPIENT_NOT_COUNTERPARTY when its recipient is not a counterparty, with 409 PAYOUT_NOT_YET_AVAILABLE before availableOn, and with 409 PAYOUT_NOT_PAYABLE while availableInDays is set. pay does not schedule; call it on the day.

A counterparty with an email can receive a claim link instead of a push.

4. Handle the outcome

Settlement is asynchronous. Subscribe to webhooks for payout.completed and payout.failed, or poll GET /payouts/{id}.

A failed payout carries a failureCode to branch on (account_closed, account_invalid, name_mismatch, or other; an open enum) and a failureReason written for you, never shown to the recipient. failureReason is null while the reason is not known. The payout keeps its approval. Fix the counterparty with PATCH /counterparties/{id} and call pay again; the payout returns to requested. This second pay needs payouts:approve as well as payouts:write, because the money goes to the counterparty's current payment method, which the approval did not cover. Without payouts:approve, pay it again in the dashboard.

Changing a counterparty's address, payment method, or tax identity moves every approved payout to it back to created, because you now pay a different destination. Re-approve them afterwards.

In the sandbox, a counterparty whose identity.lastName or identity.name contains SANDBOX FAIL, whose PayPal or Venmo email or Venmo handle contains sandbox-fail, or whose wallet address is 0x000000000000000000000000000000000000dEaD, makes the payout fail. A bank, PayPal, or Venmo failure becomes failed at once, with the failureCode account_invalid. See the sandbox test scenarios.

Recipient-managed counterparties

When a recipient claims a payout, Talentir keeps their details as a counterparty with recipientManaged: true. Such a counterparty refuses PATCH and DELETE, because the recipient owns the data. To take over, create a counterparty with the same email; it replaces the recipient-managed one in lists and on new payouts.

Delete a counterparty

DELETE /counterparties/{id} is refused with 409 COUNTERPARTY_IN_USE while a payout to it is created, approved, requested, or failed.

After the delete, the counterparty is gone from the API. Every endpoint answers 404 COUNTERPARTY_NOT_FOUND for it, and lists do not include it. Payouts to it keep its id in recipient.counterpartyId. Its customId is free again for a new counterparty.