Open Banking API — Integration Guide
The Open Banking API lets a partner service connect a client, act on their behalf after explicit consent, read accounts and balances, move money, issue and manage cards, and subscribe to events.
Base URL
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox-bank.tedr.com/open-banking/v1 |
| Production | <production base URL>/v1 |
There is no /rest segment in the path.A request looks like https://sandbox-bank.tedr.com/open-banking/v1/accounts.
The bank UI that hosts the OAuth consent page lives on a separate host — https://dev-tbank.tedr.com for the sandbox environment. You never call it directly: fetch it through GET /v1/oauth/deeplink instead of hard-coding it.
Production credentials and the production base URL are issued during onboarding.
Response envelope
Every successful response is wrapped in a status / data envelope. Paginated responses put the page inside data as nodes, count and cursor.
Pagination
All list endpoints accept pagination[limit] and pagination[skip].
Errors
Every error response carries a status of ERROR and an errors array — see Errors.
{ "status": "SUCCESS", "data": { } }{
"status": "SUCCESS",
"data": { "nodes": [], "count": 0, "cursor": 0 }
}Connecting a client through OAuth 2.0
The partner needs access to the accounts and payments of a specific client, acting on that client's behalf. The client grants that access explicitly on the bank's consent page.
About OAuth 2.0.This is an authorization protocol — it lets the client grant the partner the right to act on their behalf without ever sharing their login and password. The client decides which set of permissions (scopes) to grant.
Verification comes first.Before granting access, the client must be verified — KYC for an individual, KYB for a company. Verification happens entirely in the bank's interface, before any interaction with the partner.
Precondition — client verification before OAuth
By the time a client first reaches the partner's application and grants access, verification is already complete and the first virtual account already exists.
Path 1 — an individual connects a personal account
- The client registers in the bank UI.
- The client passes KYC — on success the system automatically creates their first virtual account.
- The client opens the partner's application and initiates the grant — the partner redirects them to the bank to confirm consent.
- After confirmation the partner receives a JWT bound to the client's personal account:
sub = userId, nocompanyId. - Every subsequent partner request runs on behalf of this client and touches only their personal accounts and payments.
Path 2 — a client registers a company and passes KYB
A company does not exist independently of a person — it is created by a client, who becomes its registrant.
- The client registers and passes KYC.
- The client creates a company and automatically becomes its registrant.
- The registrant completes KYB for the company.
- Once KYB is approved, a virtual account is created for the company automatically.
- The registrant initiates the grant in the partner's application and is redirected to the consent page.
- On the consent page the client chooses which account the grant applies to — their personal one or one of their companies. If a company is chosen, the JWT carries
sub = userIdof the registrant and acompanyId. - Every subsequent request runs for the company on behalf of the registrant.
Path 3 — connecting company employees
- Every new employee registers and passes KYC.
- The registrant adds employees, specifying a role and a share for each.
- Each employee can then grant access to the company independently, going through the OAuth flow separately.
- The resulting JWT carries
sub = userIdof the employee and the company'scompanyId.
JWT structure
Every JWT is issued for a specific user, never for a company. The key difference is whether companyId is present.
| Scenario | sub | companyId |
|---|---|---|
| Personal account | client's userId | absent |
| Company account | client's userId | present |
If companyId is present, all operations run in the company context on behalf of the user who authorized.
Fetch the deeplink
Fetch the base authorization URL instead of hard-coding it, so the same integration works across environments.
Authorization: not required.
GET https://sandbox-bank.tedr.com/open-banking/v1/oauth/deeplink{
"status": "SUCCESS",
"data": { "url": "https://dev-tbank.tedr.com/oauth/" }
}Discover the available scopes (optional)
Authorization: not required.
Query parameters: ids[], names[], scopes[], descriptions[], categories[], pagination[limit], pagination[skip].
category is INDIVIDUAL or CORPORATE.It marks which kind of account the scope applies to. It is not a grouping by module.
Scopes currently used by the API
| Scope | Grants |
|---|---|
| tb:accounts:read | Read accounts, balances and bank details. |
| tb:accounts:write | Create accounts. |
| tb:payments:read | Read payments. |
| tb:payments:write | Create and send payments. |
| tb:tariffs:read | Read tariffs, limits and fee calculations. |
| tb:personal-data:read | Read the client's personal data. |
| tb:internal-cards:read | Read cards. |
| tb:internal-cards:write | Order and manage cards. |
| tb:external-cards:read | Read Stripe cards and Stripe payment events. |
GET https://sandbox-bank.tedr.com/open-banking/v1/oauth/scopes?pagination[skip]=0&pagination[limit]=20{
"status": "SUCCESS",
"data": {
"nodes": [
{
"id": "cc2d0454-0a68-4443-b71c-14e2b60ddd2e",
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-01-15T10:00:00.000Z",
"name": "Read accounts",
"description": "Grants read-only access to accounts.",
"scope": "tb:accounts:read",
"category": "INDIVIDUAL"
}
],
"count": 1,
"cursor": 1
}
}Build the authorization URL and redirect the client
| Parameter | Required | Description |
|---|---|---|
| responseType | yes | Must be code. |
| clientId | yes | The partner client id issued during onboarding. |
| redirectUri | yes | Where the client is sent back after consent. |
| scopes | yes | Comma-separated list of the scopes being requested. |
| state | yes | An arbitrary string the partner generates. It is returned unchanged on the redirect — verify it to protect against CSRF. |
| accountCategory | no | Limits what the consent page offers: INDIVIDUAL — personal accounts only, CORPORATE — corporate accounts only, omitted — the client chooses. |
An invalid clientId or redirectUri gives no redirect back.The sender cannot be identified, so the client is never returned to the partner.
{deeplink}?responseType=code
&clientId={PARTNER_CLIENT_ID}
&redirectUri={CALLBACK_URL}
&scopes={COMMA_SEPARATED_SCOPES}
&state={ARBITRARY_STRING}
&accountCategory={INDIVIDUAL|CORPORATE} // optionalhttps://dev-tbank.tedr.com/oauth/?responseType=code
&clientId=<your-client-id>
&redirectUri=https://partner-app.com/oauth/callback
&scopes=tb:accounts:read,tb:accounts:write,tb:payments:read,tb:payments:write
&state=random-csrf-token-xyz
&accountCategory=CORPORATEThe consent page
If the client is not signed in, they authenticate first, then land on the consent page. The consent page lists the requested scopes and the partner's logo.
On "Allow" the client is redirected back to the redirectUri with state and code. The authorization code is a UUID and is valid for 60 seconds.
Errors returned on the redirect
error in the URL | Reason |
|---|---|
| access_denied | The client pressed "Deny". |
| unsupported_response_type | responseType was not code. |
| invalid_scope | A requested scope does not exist. |
https://partner-app.com/oauth/callback?state=random-csrf-token-xyz&code=<authorization-code>https://partner-app.com/oauth/callback?state=string&error=access_denied&error_description=The+user+refused+authorization
https://partner-app.com/oauth/callback?state=string&error=unsupported_response_type&error_description=The+response_type+is+invalid
https://partner-app.com/oauth/callback?state=string&error=invalid_scope&error_description=Invalid+scope+specified.Exchange the code for an access token
The code is valid for exactly 1 minute. Perform the exchange immediately after receiving the redirect.
| Field | Type | Description |
|---|---|---|
| grantTypereq | string | Always authorization_code. |
| codereq | string | The authorization code from the redirect. |
| redirectUrireq | string | Must match the one used in the authorization URL. |
| clientIdreq | string | The partner client id. |
| clientSecretreq | string | The partner client secret. Keep it server-side. |
Every failure returns the same code.An expired code, a reused code, a mismatched redirectUri or a wrong clientSecret all return OPEN-BANKING-029, without naming the specific cause. This prevents probing for the reason of a rejection.
POST https://sandbox-bank.tedr.com/open-banking/v1/oauth/access-token
Content-Type: application/json
{
"grantType": "authorization_code",
"code": "<authorization-code>",
"redirectUri": "https://partner-app.com/oauth/callback",
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>"
}{
"status": "SUCCESS",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 31536000
}
}{
"externalServiceId": "<your-client-id>",
"scopes": ["tb:accounts:read", "tb:payments:write"],
"sub": "c9788fca-ed84-49e9-a511-81af1906c393",
"jti": "2b6df3c0-6cfb-4a57-ad61-86ab371384a3",
"iss": "<issuer>",
"iat": 1739981383,
"exp": 1771517383
}{
"status": "ERROR",
"errors": [{
"title": "OAuth invalid payload for exchange code",
"code": "OPEN-BANKING-029",
"message": "Invalid payload was provided in exchange code for token."
}]
}Using the access token
Every request to a client-scoped endpoint carries the bearer token.
The token identifies the client (sub), the partner (externalServiceId) and the granted scopes. Requests are additionally checked against the access-control service; a rejected request returns 403.
Webhook endpoints authenticate differently.The webhook management endpoints (Webhooks) use x-client-id and x-api-key instead of the bearer token, because subscriptions belong to the partner service, not to a client.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...The model
VirtualAccount
A named container. The account itself has no currency and no balance fields.
VirtualBalance
The balance in one specific currency. One account can hold several balances, one per currency.
Every balance carries three amounts: availableAmount (spendable now), holdAmount (locked against active operations) and totalAmount = availableAmount + holdAmount.
Money lives on balances, not on accounts.To see how much money an account holds, request its balances, not the account.
The first account already exists
When a client passes KYC (individual) or KYB (company), their first virtual account is created automatically. The partner does nothing.
List the client's accounts
Query parameters
| Parameter | Description |
|---|---|
| ids[] | Filter by account UUIDs. |
| statuses[] | ACTIVE, PROCESSING, BLOCKED, CLOSED. |
| categories[] | INDIVIDUAL, CORPORATE. |
| types[] | Account type, e.g. PERSONAL, BUSINESS. |
| tags[] | Filter by tags. |
| excludeZeroBalances | true — only accounts with a non-zero balance in at least one currency. |
| virtualBalanceCurrencyIds[] | Only accounts holding a balance in the given currencies. |
| search | Search across description and id. |
| pagination[limit] · pagination[skip] | Pagination. |
Technical hold accounts are never returned.
Timestamps are numbers.createdAt / updatedAt are Unix timestamps in milliseconds, not ISO strings.
GET https://sandbox-bank.tedr.com/open-banking/v1/accounts
Authorization: Bearer ...{
"status": "SUCCESS",
"data": {
"nodes": [
{
"id": "05155318-22a3-45aa-9db0-0ebfe8f21e8c",
"description": "Main account",
"status": "ACTIVE",
"category": "INDIVIDUAL",
"type": "PERSONAL",
"ledgerType": "PASSIVE",
"tag": "main_account",
"createdAt": 1704067200000,
"updatedAt": 1705312800000
}
],
"count": 1,
"cursor": 1
}
}Create an additional account
| Field | Type | Description |
|---|---|---|
| descriptionopt | string | Account name / description. |
| currencyIdsopt | UUID[] | Currencies to open balances in; a zero balance is created for each. Obtain the ids from the Dictionary. |
The field is description — not name.Both fields are optional; an account created without currencyIds has no balances until they are added.
The account is returned with status PROCESSING while it is being initialized at the provider, and becomes ACTIVE afterwards.
POST https://sandbox-bank.tedr.com/open-banking/v1/accounts
Authorization: Bearer ...
Content-Type: application/json
{
"description": "Business account",
"currencyIds": [
"99d41c98-93d1-471a-a73a-4cac9e3587e7",
"2e5d9800-f467-4eb7-ad47-1f67a874a908"
]
}Read the account balances
Query parameters: currencyIds[], currencyTickers[], statuses[], pagination[limit], pagination[skip].
The parent account is an object.It is returned as virtualAccount: { id } — not as a flat virtualAccountId string. There is no networkNames[] filter.
GET https://sandbox-bank.tedr.com/open-banking/v1/accounts/{accountId}/balances
Authorization: Bearer ...{
"status": "SUCCESS",
"data": {
"nodes": [
{
"id": "3f0f1f8a-0f4d-4c2c-9a6a-2d63bd8dd0f1",
"virtualAccount": { "id": "05155318-22a3-45aa-9db0-0ebfe8f21e8c" },
"currency": {
"id": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"ticker": "USD",
"network": { "name": "FIAT" }
},
"availableAmount": "1150.50",
"holdAmount": "100.00",
"totalAmount": "1250.50",
"description": null,
"status": "ACTIVE",
"createdAt": 1704067200000,
"updatedAt": 1717237800000
}
],
"count": 1,
"cursor": 1
}
}Read a single account
Returns the same object as the list endpoint. An account that does not belong to the client returns 404.
GET https://sandbox-bank.tedr.com/open-banking/v1/accounts/{accountId}
Authorization: Bearer ...Supported payment systems: INNER, SWIFT, INTERNAL_CARDS. Each has its own endpoint and its own request shape, and each exposes four endpoints.
| Purpose | Endpoints |
|---|---|
| Create and send in one call | POST /v1/payments/inner · POST /v1/payments/swift · POST /v1/payments/internal-cards |
| Prepare without sending | POST /v1/payments/inner/prepare · POST /v1/payments/swift/prepare · POST /v1/payments/internal-cards/prepare |
| Send a prepared payment | POST /v1/payments/inner/{paymentId}/send · POST /v1/payments/swift/{paymentId}/send · POST /v1/payments/internal-cards/{paymentId}/send |
| Calculate the fee | POST /v1/payments/inner/calculate-fee · POST /v1/payments/swift/calculate-fee · POST /v1/payments/internal-cards/calculate-fee |
prepare and calculate-fee take the same body as the create call. send takes no body.
Payment direction is INCOMING or OUTGOING.
Base request fields (all payment types)
| Field | Type | Description |
|---|---|---|
| sourceCurrencyIdreq | UUID | Debit currency. |
| targetCurrencyIdreq | UUID | Credit currency. |
| sourceAmount | string (decimal) | Amount to debit. Pass either this or targetAmount. |
| targetAmount | string (decimal) | Amount to credit. Pass either this or sourceAmount. |
| quoteId | UUID | Locked quote id, required when sourceCurrencyId ≠ targetCurrencyId. |
| noteopt | string | Free-form note. |
| metadataopt | object | Free-form JSON returned back on the payment object. |
attachments is response-only.It is returned on the payment object but is not accepted in creation requests.
About quoteId
For transfers between different currencies a quoteId is required — the id of a locked quote that fixes the exchange rate at the moment of sending. Quotes are issued by the Quotes service and have a limited lifetime. If the currencies match, no quoteId is needed.
Internal transfer (INNER)
A transfer between accounts inside the system — instant, no external banks.
| Field | Type | Description |
|---|---|---|
| senderAccountIdreq | UUID | The account to debit. |
| recipientAccountIdreq | UUID | The account to credit. |
| recipientClientIdreq | UUID | The recipient client. |
| recipientClientTypereq | INDIVIDUAL | CORPORATE | The kind of recipient client. |
| recipientEmailopt | string | Recipient email. |
| recipientPhoneNumberopt | string | Recipient phone number. |
| + base fields | See base request fields. |
There is no senderClientType field.The sender is taken from the token.
POST https://sandbox-bank.tedr.com/open-banking/v1/payments/inner
Authorization: Bearer ...
Content-Type: application/json
{
"senderAccountId": "05155318-22a3-45aa-9db0-0ebfe8f21e8c",
"recipientAccountId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"recipientClientId": "c9788fca-ed84-49e9-a511-81af1906c393",
"recipientClientType": "INDIVIDUAL",
"recipientEmail": "recipient@example.com",
"recipientPhoneNumber": "+1234567890",
"sourceCurrencyId": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"targetCurrencyId": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"sourceAmount": "100.00",
"note": "Payment for services",
"metadata": { "orderId": "ORD-123" }
}SWIFT transfer
Top level
| Field | Type | Description |
|---|---|---|
| purposeOfPaymentreq | string | Purpose of the payment. |
| sourceOfFundsreq | enum | COMMISSION_OF_SALES, DIVIDEND_INCOME_FROM_SHARE, INVESTMENT_CAPITAL, REVENUE_GENERATED_FROM_BUSINESS_ACTIVITIES, SALARY, SAVING. |
| internal.accountIdreq | UUID | The sender's account. |
| externalreq | object | The recipient, see below. |
| + base fields | See base request fields. |
external
| Field | Type | Description |
|---|---|---|
| accountHolderNamereq | string | Name on the recipient account. |
| clientTypereq | INDIVIDUAL | CORPORATE | The kind of recipient. |
| bankCodereq | string | BIC, 8 or 11 characters. |
| accountNumberreq | string | Recipient account number or IBAN. |
| branchCodeopt | string | Derived from bankCode when omitted. |
| firstName · lastName · middleNameopt | string | Recipient name parts. |
| addressreq | object | countryId (UUID), postalCode, line1, line2?. |
| bankreq | object | name, countryId (UUID), cityName?, addressLine1?, addressLine2?, postalCode?. |
| intermediaryBankopt | object | bankCode, accountNumber?, name?, countryId?, cityName?, addressLine1?, addressLine2?. |
Countries are passed as countryId (UUID).Not as { "alpha3": "DEU" } — that shape returns 400. Resolve the id through the Dictionary. Country objects with alpha2 / alpha3 appear only in responses.
POST https://sandbox-bank.tedr.com/open-banking/v1/payments/swift
Authorization: Bearer ...
Content-Type: application/json
{
"purposeOfPayment": "Payment for consulting services",
"sourceOfFunds": "REVENUE_GENERATED_FROM_BUSINESS_ACTIVITIES",
"sourceCurrencyId": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"targetCurrencyId": "2e5d9800-f467-4eb7-ad47-1f67a874a908",
"sourceAmount": "1000.00",
"quoteId": "7c9f2f2e-1a4b-4f1f-9a4e-58b0c2f1a3d2",
"note": "Invoice #INV-2024-001",
"metadata": { "orderId": "ORD-123" },
"internal": {
"accountId": "05155318-22a3-45aa-9db0-0ebfe8f21e8c"
},
"external": {
"accountHolderName": "Acme Corp GmbH",
"clientType": "CORPORATE",
"bankCode": "DEUTDEFFXXX",
"accountNumber": "DE89370400440532013000",
"branchCode": "XXX",
"firstName": null,
"lastName": null,
"middleName": null,
"address": {
"countryId": "6f0f2a51-1f4c-4a3e-9f4a-4b5b2d1c9a10",
"postalCode": "10117",
"line1": "Unter den Linden 1",
"line2": null
},
"bank": {
"name": "Deutsche Bank AG",
"countryId": "6f0f2a51-1f4c-4a3e-9f4a-4b5b2d1c9a10",
"cityName": "Berlin",
"addressLine1": "Taunusanlage 12",
"addressLine2": null,
"postalCode": "60325"
},
"intermediaryBank": {
"bankCode": "CHASUS33",
"accountNumber": "9876543210",
"name": "JPMorgan Chase",
"countryId": "a1c3e5f7-2b4d-4e6f-8a0b-1c2d3e4f5a6b"
}
}
}Internal cards payment (INTERNAL_CARDS)
Moves money between the client's cards and accounts: top up a card from an account, withdraw from a card to an account, or transfer card to card.
| Field | Type | Description |
|---|---|---|
| typereq | ACCOUNT_TO_CARD | CARD_TO_ACCOUNT | CARD_TO_CARD | Direction of the operation. |
| senderreq | object | cardId?, accountId?, email?, phoneNumber? — fill the one matching type. |
| recipientreq | object | cardId?, accountId?, clientId (required), clientType (required), email?, phoneNumber?. |
| + base fields | See base request fields. |
POST https://sandbox-bank.tedr.com/open-banking/v1/payments/internal-cards
Authorization: Bearer ...
Content-Type: application/json
{
"type": "CARD_TO_CARD",
"sourceCurrencyId": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"targetCurrencyId": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"sourceAmount": "100.00",
"note": "Card to card transfer",
"metadata": { "orderId": "ORD-123" },
"sender": {
"cardId": "0f2d6d63-4d9e-4a01-9d0f-9a3b6f1f2c77"
},
"recipient": {
"cardId": "c1b2a3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"clientId": "c9788fca-ed84-49e9-a511-81af1906c393",
"clientType": "INDIVIDUAL"
}
}Each payment system has its own calculate-fee endpoint: /v1/payments/inner/calculate-fee, /v1/payments/swift/calculate-fee and /v1/payments/internal-cards/calculate-fee.
POST https://sandbox-bank.tedr.com/open-banking/v1/payments/swift/calculate-fee
Authorization: Bearer ...
Content-Type: application/json{
"status": "SUCCESS",
"data": {
"totalFee": "10.00",
"totalAmount": "1010.00",
"currencyTicker": "USD",
"originalAmount": "1000.00",
"originalTotalAmount": "1010.00",
"originalTotalFee": "10.00",
"originalProviderFee": "7.00",
"originalBankFee": "3.00"
}
}Browse payments
Query parameters
| Parameter | Description |
|---|---|
| paymentSystems[] | INNER, SWIFT, INTERNAL_CARDS. |
| statuses[] | DRAFT, PROCESSING, ON_REVIEW, NEED_ACTION, SUCCESSFUL, REFUNDED, DECLINED. |
| directions[] | INCOMING, OUTGOING. |
| dateRange[from] · dateRange[to] | Range over the creation date. |
| settledAt[from] · settledAt[to] | Range over the settlement date. |
| currencyIds[] | By currency on either side. |
| sourceCurrencyIds[] | By debit currency. |
| targetCurrencyIds[] | By credit currency. |
| isFavorite | Favourites only. |
| search | Full-text search. |
| pagination[limit] · pagination[skip] | Pagination. |
Filters that do not exist.There are no massPaymentIds[], commentIds[] or scoringRiskLevels[] filters.
GET /v1/payments/{paymentId} returns a single payment; its shape depends on paymentSystem.
GET https://sandbox-bank.tedr.com/open-banking/v1/payments
?paymentSystems[]=SWIFT
&directions[]=OUTGOING
&dateRange[from]=2026-01-01T00:00:00Z
Authorization: Bearer ...| Field | Type | Description |
|---|---|---|
| id | UUID | Payment id. |
| paymentSystem | enum | INNER, SWIFT, INTERNAL_CARDS. |
| status | enum | Current status. |
| previousStatus | enum | Previous status, when the payment has changed state. |
| direction | enum | INCOMING, OUTGOING. |
| isFavorite | boolean | Favourite flag. |
| note | string | Free-form note. |
| purposeOfPayment | string | Purpose of the payment. |
| attachments | array | Attached files. Response-only. |
| metadata | object | null | Whatever was sent on creation. |
| sender · recipient | object | clientId, clientType. |
| amount · fee · totalAmount | object | Each with exchangeRate, source, target. |
| createdAt · updatedAt · settled | date | Lifecycle timestamps. |
Per-system additions: INNER and INTERNAL_CARDS add senderClient / recipientClient; INTERNAL_CARDS also adds type; SWIFT adds sourceOfFunds, internal and external with the full bank details.
Bank details for incoming transfers
Scope: tb:accounts:read — the client wants to receive an incoming transfer; the partner requests the details to pass to the sender.
currencyIds is optional — without it the details for every currency of the account are returned.
How to read routes
| Route | Use |
|---|---|
| INNER | For receiving from another user of the system; passing the sender the virtualAccountId is enough. |
| SWIFT | For an international bank transfer; pass on beneficiary and beneficiaryBank. |
| BSB · IFSC | The same idea for Australia / India, when the account has such a route. |
One currency can expose several routes at once; the sender picks whichever suits them.
The values shown here are placeholders.Real beneficiary details are returned per account and must not be reproduced as examples in the documentation.
GET https://sandbox-bank.tedr.com/open-banking/v1/accounts/{accountId}/bank-details
?currencyIds[]=99d41c98-93d1-471a-a73a-4cac9e3587e7
Authorization: Bearer ...{
"status": "SUCCESS",
"data": {
"virtualAccountId": "05155318-22a3-45aa-9db0-0ebfe8f21e8c",
"bankDetails": [
{
"currency": {
"id": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"ticker": "USD",
"network": { "name": "FIAT" }
},
"routes": [
{
"schema": "INNER",
"details": {
"virtualAccountId": "05155318-22a3-45aa-9db0-0ebfe8f21e8c"
}
},
{
"schema": "SWIFT",
"details": {
"beneficiary": {
"name": "Example Holdings Ltd",
"address": "1 Example Street, Exampletown, EX1 2AB"
},
"beneficiaryBank": {
"account": "0000000000",
"code": "EXMPUS33XXX",
"name": "EXAMPLE BANK",
"address": null,
"country": {
"alpha2": "US",
"alpha3": "USA",
"name": "United States"
}
},
"correspondentBanks": [],
"remittanceInformation": [
"For own account transfer",
"Actual purpose of payment"
]
}
}
]
}
]
}
}Order physical and virtual cards, read their details in encrypted form, and manage their state.
Card data never travels in the clear.Card details and the PIN are encrypted with the public key from GET /v1/cards/public-key — both when you read them and when you set a new PIN.
v1/cards/public-key
Returns the public key used to encrypt card data (PIN) before sending it, and to receive encrypted card details.
Authorization: not required.
{
"status": "SUCCESS",
"data": { "publicKey": "-----BEGIN PUBLIC KEY-----..." }
}v1/cards/can-order-card
Whether the current client may order a card (tariff, KYC status and so on).
{ "status": "SUCCESS", "data": { "result": true } }Query: ids[], pagination[limit], pagination[skip].
| Field | Description |
|---|---|
| id · name · description | Program identity. |
| currencies[] | Currencies the program can be issued in. |
| processingType | VISA or MASTERCARD. |
| cardType | PHYSICAL or VIRTUAL. |
| cost | issue, service, currency. |
GET https://sandbox-bank.tedr.com/open-banking/v1/cards/programs
Authorization: Bearer ...Query: ids[], pagination[limit], pagination[skip]. Each method contains id, name, description and cost (amount, currency).
GET https://sandbox-bank.tedr.com/open-banking/v1/cards/delivery-methods
Authorization: Bearer ...v1/cards — order a card
| Field | Type | Description |
|---|---|---|
| programIdreq | UUID | Card program. |
| cardholderNamereq | string | Name embossed on the card. |
| phoneNumberreq | string | Cardholder phone. |
| deliveryopt | object | Required for physical cards. |
| delivery.deliveryMethodIdreq | UUID | Required inside delivery. |
| delivery.address.line1req | string | Street address. |
| delivery.address.line2opt | string | Apartment, suite and so on. |
| delivery.address.cityreq | string | City. |
| delivery.address.countryCodereq | string | ISO alpha-3, e.g. DEU. |
| delivery.address.zipCodereq | string | Postal code. |
| delivery.address.regionopt | string | Region or state. |
| delivery.address.nameOrCompanyNamereq | string | Recipient of the delivery. |
The delivery data is nested.The body is not { programId, deliveryMethodId, deliveryAddress, phoneNumber }, and cardholderName is required.
The card is returned with orderStatus: PROCESSING.
POST https://sandbox-bank.tedr.com/open-banking/v1/cards
Authorization: Bearer ...
Content-Type: application/json
{
"programId": "80d9a3a3-787c-4475-bf2c-14e05424acac",
"cardholderName": "DOMINIC HOPKINS",
"phoneNumber": "+1234567890",
"delivery": {
"deliveryMethodId": "5f4d42e5-60cb-42bc-b0d4-2b73d063560f",
"address": {
"line1": "50 St Denys Road",
"line2": "Apt. 5",
"city": "Adlington",
"countryCode": "DEU",
"zipCode": "32584",
"region": "England",
"nameOrCompanyName": "Dominic Hopkins"
}
}
}v1/cards — list and read
Query: ids[], orderStatuses[], states[], types[], pagination[limit], pagination[skip].
There is no statuses[] filter.A card has two independent status fields — orderStatus and state — and they are filtered separately.
Status fields
| Field | Values |
|---|---|
| orderStatus | PROCESSING, WAITING_PAY, CREATING_CARD, DISPATCHED, ACTIVATION_CARD, APPROVED, DECLINED. |
| state | ACTIVATION_IN_PROGRESS, ACTIVE, INACTIVE, BLOCKED, CLOSED, EXPIRED. |
| type | PHYSICAL or VIRTUAL. |
Card object
| Field | Type | Description |
|---|---|---|
| id | UUID | Card id. |
| clientId | UUID | Owner. |
| programId | UUID | Card program. |
| holderName | string | Name embossed on the card. |
| holderPhone | string | Cardholder phone. |
| orderStatus | enum | Where the order stands. |
| state | enum | Whether the card can be used. |
| type | PHYSICAL | VIRTUAL | Card form factor. |
| processingType | VISA | MASTERCARD | Payment network. |
| isDefault | boolean | The client's default card. |
| virtualAccountId | UUID | null | Linked account, when there is one. |
GET https://sandbox-bank.tedr.com/open-banking/v1/cards?states[]=ACTIVE&types[]=VIRTUAL
Authorization: Bearer ...Card requisites and PIN
Return the card details (number, CVV, expiry) and the PIN, encrypted with the public key passed in the path.
Decrypt the returned string with your private key on your side.
{
"status": "SUCCESS",
"data": { "requisites": "<encrypted string>" }
}{ "phoneNumber": "+1234567890" }The PIN is transmitted encrypted, never as plain digits.Encrypt it with the key from GET /v1/cards/public-key. Sending "1234" will be rejected by the card processor.
{ "pin": "<PIN encrypted with the public key>" }Closing a card is irreversible.A closed card cannot be reactivated — order a new one instead.
PATCH https://sandbox-bank.tedr.com/open-banking/v1/cards/{cardId}/block
Authorization: Bearer ...v1/tariffs/limits/check
Scope: tb:tariffs:read — check whether an operation fits within the client's limits.
Query parameters
| Field | Type | Description |
|---|---|---|
| operationNamereq | enum | OUTGOING or INCOMING. |
| amountreq | string | Amount to check. |
| currencyTickerreq | string | For example USD, EUR. |
The parameter is operationName, not operationType.Its values are OUTGOING / INCOMING — not SWIFT_OUTGOING / INNER. The response is not { allowed, remainingAmount, currency }.
GET https://sandbox-bank.tedr.com/open-banking/v1/tariffs/limits/check
?operationName=OUTGOING
&amount=500.00
¤cyTicker=EUR
Authorization: Bearer ...{
"status": "SUCCESS",
"data": {
"result": true,
"reason": null,
"expect": {
"available": "4500.00",
"min": "1.00",
"max": "10000.00"
},
"source": { "currencyTicker": "EUR", "amount": "500.00" },
"target": { "currencyTicker": "EUR", "amount": "500.00" },
"availableLimits": {
"daily": "4500.00",
"weekly": "20000.00",
"monthly": "50000.00",
"annual": "500000.00"
}
}
}v1/tariffs/limits
Query: ids[], clientIds[], pagination[limit], pagination[skip]. There is no operationTypes[] filter.
Limit object
id, clientId, name, description, isBaseCurrency, daily, weekly, monthly, annual, min, max, and available (daily, weekly, monthly, annual).
GET https://sandbox-bank.tedr.com/open-banking/v1/tariffs/limits
Authorization: Bearer ...v1/tariffs/:clientId/current-tariff
The response contains the tariff itself only.Limits and fees are requested separately — see limits and fee calculation.
{
"status": "SUCCESS",
"data": {
"id": "9e6f1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Standard",
"description": null,
"billingPeriod": "MONTHLY"
}
}v1/tariffs/fees/calculate
Query parameters
| Field | Type | Description |
|---|---|---|
| operationNamereq | enum | INCOMING, OUTGOING, STRIPE_SHOPPING or STRIPE_TOP_UP. |
| amountreq | string | Amount to calculate the fee for. |
| currencyTickerreq | string | For example USD, EUR. |
| inclusiveInAmountopt | boolean | Whether the fee is included in amount. |
{
"status": "SUCCESS",
"data": {
"source": {
"currencyTicker": "EUR",
"amount": "100.00",
"totalAmount": "105.00",
"totalFee": "5.00"
},
"target": {
"currencyTicker": "EUR",
"amount": "100.00",
"totalAmount": "105.00",
"totalFee": "5.00"
}
}
}v1/stripe/public-key
Base path: /v1/stripe · Scope: tb:external-cards:read — Stripe is used to top an account up from an external bank card.
Returns the Stripe publishable key for initialising Stripe.js or the Stripe SDK on your side. Authorization: not required.
GET https://sandbox-bank.tedr.com/open-banking/v1/stripe/public-keyCard data never passes through this API.cardTokenId is the token returned by the Stripe SDK after the client enters their card data.
{ "cardTokenId": "tok_1QpQqOCUHBOJJ0oY9zGxGYtQ" }v1/stripe/cards — list and read
Query: ids[], pagination[limit], pagination[skip].
Card object: id, clientId, brand, country, expMonth, expYear, name.
GET https://sandbox-bank.tedr.com/open-banking/v1/stripe/cards
Authorization: Bearer ...v1/stripe/payment-intent
Top a virtual account up from an attached Stripe card.
| Field | Type | Description |
|---|---|---|
| amountreq | string | Top-up amount. |
| creditorAccountIdreq | UUID | Virtual account to credit. |
| debitorCardIdreq | string | Attached Stripe card. |
| descriptionopt | string | Shown on the Stripe payment. |
Returns { "id": "pi_…", "clientId": "…" }. The fee is taken from the client's tariff, taking the card's country into account.
{
"amount": "100",
"creditorAccountId": "f1df072f-d728-4038-8d1f-f569eaafa92d",
"debitorCardId": "card_1QpQqOCUHBOJJ0oYUYH7TKeJ",
"description": "Top-up via Stripe"
}v1/dictionary/currencies
Reference data needed to build requests: every currencyId and countryId used in payments and account creation comes from here.
Query: pagination[limit], pagination[skip].
Resolve ids once and cache them.currencyIds are required by account creation and by every payment request.
{
"status": "SUCCESS",
"data": {
"nodes": [
{
"id": "99d41c98-93d1-471a-a73a-4cac9e3587e7",
"name": "US Dollar",
"ticker": "USD",
"precision": 2,
"type": "FIAT",
"symbol": "$"
}
],
"count": 1,
"cursor": 1
}
}v1/dictionary/countries
Query: pagination[limit], pagination[skip].
SWIFT payments take countryId, not an alpha code.Resolve the id here before building an SWIFT request.
{
"status": "SUCCESS",
"data": {
"nodes": [
{
"id": "6f0f2a51-1f4c-4a3e-9f4a-4b5b2d1c9a10",
"name": "Germany",
"code": "DEU",
"phoneCode": "+49"
}
],
"count": 1,
"cursor": 1
}
}Webhooks
Subscribe to events (accounts, payments, cards) and receive POST notifications on your own URL. Managing subscriptions is a CRUD over a Webhook object.
Authentication
These endpoints are authenticated with partner-level headers, not with a client bearer token.
| Field | Type | Description |
|---|---|---|
| x-client-idreq | UUID | Your client id. |
| x-api-keyreq | UUID | Your API key. |
Both headers are required on every request in this chapter.A missing or malformed header returns 403.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/webhooks | Create a subscription. |
| GET | /v1/webhooks | List subscriptions (paginated). |
| GET | /v1/webhooks/{webhookId} | Read one subscription. |
| PATCH | /v1/webhooks/{webhookId} | Update a subscription. |
| DELETE | /v1/webhooks/{webhookId} | Delete a subscription (irreversible). |
x-client-id: <your-client-id>
x-api-key: <your-api-key>Create a subscription
| Field | Type | Description |
|---|---|---|
| urlreq | string (URL) | Where the POST notifications are delivered. |
| secretreq | string | Secret used to sign outgoing notifications; never returned back. |
| eventsreq | enum[] | At least one event from the catalogue. |
| isActivereq | boolean | Whether the subscription is enabled. |
| descriptionopt | string | Free-form description. |
secret is not returned.The response is the subscription object — id, createdAt, description, isActive, url, events. Store the secret on your side.
POST https://sandbox-bank.tedr.com/open-banking/v1/webhooks
x-client-id: <your-client-id>
x-api-key: <your-api-key>
Content-Type: application/json
{
"url": "https://partner-app.com/webhooks/payments",
"secret": "<your-signing-secret>",
"events": ["INNER_PAYMENT_SUCCEEDED", "ACCOUNTS_CREATED"],
"isActive": true,
"description": "Webhook to notify when payments are updated."
}List and read subscriptions
Query: ids[], pagination[limit], pagination[skip]. Only subscriptions belonging to your x-client-id are returned. events come back sorted alphabetically.
GET https://sandbox-bank.tedr.com/open-banking/v1/webhooks?pagination[skip]=0&pagination[limit]=10
x-client-id: <your-client-id>
x-api-key: <your-api-key>Update and delete
All fields are optional; only what is sent is changed.
The event list is modified incrementally.Use attach / detach — the list is not overwritten with a new array, and at least one event must remain attached.
DELETE /v1/webhooks/{webhookId} removes the subscription permanently and returns the deleted object.
{
"isActive": false,
"url": "https://partner-app.com/webhooks/payments-v2",
"description": "Updated webhook.",
"events": {
"attach": ["SWIFT_PAYMENT_SUCCEEDED"],
"detach": ["ACCOUNTS_CREATED"]
}
}Event catalogue
A notification is delivered only to partners that have both an active subscription and the matching scope granted by the client.
| Group | Events | Scope required |
|---|---|---|
| Accounts | ACCOUNTS_ORDERED, ACCOUNTS_CREATED | tb:accounts:read |
| INNER payments | INNER_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| SWIFT payments | SWIFT_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| INTERNAL_CARDS payments | INTERNAL_CARDS_PAYMENT_CREATED / _UPDATED / _SUCCEEDED / _DECLINED / _REFUNDED | tb:payments:read |
| Cards | INTERNAL_CARD_ORDERED, INTERNAL_CARD_UPDATED, INTERNAL_CARD_EXPIRES, INTERNAL_CARD_EXPIRED | tb:internal-cards:read |
| Stripe cards | STRIPE_CARD_CREATED, STRIPE_CARD_EXPIRES, STRIPE_CARD_EXPIRED | tb:external-cards:read |
| Stripe payments | STRIPE_PAYMENT_CREATED, STRIPE_PAYMENT_UPDATED | tb:external-cards:read |
What arrives at your URL
| Field | Type | Description |
|---|---|---|
| id | UUID | Unique id of this delivery — use it for idempotency. |
| webhookId | UUID | The subscription the notification belongs to. |
| event | string | Event name from the catalogue. |
| timestamp | number | Unix time in seconds. |
| data | object | The entity snapshot: the payment, the account list or the card. |
Headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-SIGNATURE | HMAC-SHA1(secret, raw body) in hex, where secret is the one you set on the subscription. |
Compute the HMAC over the raw body.Sign the bytes exactly as received, before any JSON parsing.
What your endpoint must return
Any 2xx status counts as a successful delivery. The response body is ignored — 200 with an empty body, 204, or 200 with any JSON all work equally.
Delivery guarantees
Deliveries are at-least-once: the same event can arrive more than once, so deduplicate by id. The order of deliveries is not guaranteed.
Retries
The response timeout is 10 seconds; a timeout counts as a failed delivery. Failed deliveries are retried on a fixed schedule — 20 attempts in total: three after 15 s, one after 30 s, three after 60 s, two after 320 s, five after ~2.8 h, five after 12 h and a final one after 24 h.
An exhausted schedule disables the subscription.It is switched to isActive: false with the reason RETRY_TIMEOUT — fix the receiver and re-enable it with PATCH.
{
"id": "9b1c8f0e-7d3a-4e2b-8a11-6c5d4e3f2a10",
"webhookId": "5f4d42e5-60cb-42bc-b0d4-2b73d063560f",
"event": "INNER_PAYMENT_SUCCEEDED",
"timestamp": 1748689100,
"data": { }
}const crypto = require('crypto')
const expected = crypto
.createHmac('sha1', SECRET)
.update(rawBody)
.digest('hex')
const ok = expected === req.headers['x-signature']15, 15, 15, 30, 60, 60, 60, 320, 320, 10240 × 5, 43200 × 5, 86400| HTTP status | When |
|---|---|
| 400 | Field validation failed. |
| 403 | The access-control service denied the request, or x-client-id / x-api-key are missing on webhook endpoints. |
| 404 | The account, payment or card does not exist or does not belong to the client. |
| 500 | Internal error, or the provider is unavailable. |
{
"status": "ERROR",
"errors": [
{
"title": "Account not found",
"code": "OPEN-BANKING-011",
"message": "Account not found."
}
]
}Frequently returned codes
| Code | Meaning |
|---|---|
| OPEN-BANKING-003 | Forbidden. |
| OPEN-BANKING-004 | Validation failed. |
| OPEN-BANKING-008 | Currency not found. |
| OPEN-BANKING-009 | Country not found. |
| OPEN-BANKING-011 | Account not found. |
| OPEN-BANKING-012 | Card not found. |
| OPEN-BANKING-029 | Invalid payload in the code-for-token exchange. |
| OPEN-BANKING-030 | Invalid authorization token. |
| OPEN-BANKING-033 | Payment not found. |
| OPEN-BANKING-034 | Payment is in an invalid state for this operation. |
| OPEN-BANKING-038 | User not found. |
| OPEN-BANKING-042 | Webhook not found. |
| OPEN-BANKING-046 | Unsupported currency. |
| OPEN-BANKING-047 | Limit not found. |
| OPEN-BANKING-048 | Limit check failed. |
| OPEN-BANKING-050 | Fee calculation failed. |
| OPEN-BANKING-051 | Card is in an invalid state for this operation. |
| OPEN-BANKING-053 | Route not found. |