Elixir client for the Teya API.
Installation
Add teya to your dependencies in mix.exs:
def deps do
[
{:teya, "~> 0.4.0"}
]
endConfiguration
# config/runtime.exs
config :teya,
client_id: System.fetch_env!("TEYA_CLIENT_ID"),
client_secret: System.fetch_env!("TEYA_CLIENT_SECRET"),
token_url: "https://identity.teya.com/connect/token",
base_url: "https://api.teya.com",
scopes: [
# list only the scopes your application needs — see table below
]OAuth tokens are fetched automatically and refreshed before expiry. Only request the scopes your application needs.
Scope reference
Obtain credentials from the Teya Developer Portal.
Usage
Hosted Checkout
Redirect customers to a Teya-hosted payment page:
params = %{
"amount" => %{"currency" => "GBP", "value" => 1000},
"type" => "SALE",
"success_url" => "https://example.com/success",
"failure_url" => "https://example.com/failure"
}
case Teya.Checkout.create_session(params) do
{:ok, %{"session_url" => url}} ->
# redirect the customer to url
{:error, %Teya.Error{code: code, message: message}} ->
# handle error
endPoll for the result after the customer returns:
{:ok, session} = Teya.Checkout.get_session(session_id)
session["payment_status"] # "NONE" | "SUCCESS" | "FAILED"
session["session_status"] # "ACTIVE" | "PROCESSING" | "COMPLETED" | "EXPIRED"Direct Card Processing (Embedded UI)
Process a card payment from your own payment form:
params = %{
"amount" => %{"currency" => "GBP", "value" => 1000},
"type" => "SALE",
"initiator" => "CUSTOMER",
"store_id" => "your-store-uuid",
"payment_method" => %{
"type" => "CARD",
"card" => %{
"number" => "4111111111111111",
"expiry_month" => "12",
"expiry_year" => "2028",
"cvc" => "123"
}
}
}
case Teya.Transaction.create(params) do
{:ok, %{"type" => "ONLINE_TRANSACTION", "online_transaction" => txn}} ->
txn["status"] # "SUCCESS" | "FAILURE" | "PENDING"
{:ok, %{"type" => "REDIRECT_TRANSACTION_RESPONSE"} = resp} ->
# 3DS challenge required — redirect customer to:
resp["redirect_transaction_response"]["redirect_url"]
{:error, %Teya.Error{} = err} ->
# handle error
endPay By Link
Generate a shareable payment link:
{:ok, %{"payment_link" => url}} =
Teya.PayByLink.create(%{
"amount" => %{"currency" => "GBP", "value" => 5000},
"expires_at" => "2024-12-31T23:59:59Z"
})Capture a Pre-authorisation
:ok = Teya.Capture.create(transaction_id)Refund
{:ok, _} = Teya.Refund.create(%{"transaction_id" => transaction_id})Card-Present (Direct Terminal Integration)
Process a payment where your software supplies the raw card data from a POS terminal (EMV TLV, encrypted track, PIN block). For Teya-managed terminals accessed through ePOS middleware, see POSLink instead.
params = %{
"type" => "SALE",
"entry_mode" => "CONTACT_EMV",
"amounts" => %{"amount" => 1000, "currency" => "GBP"},
"emv_data" => "9F2608AABBCCDD112233",
"track_data" => %{
"encryption_key_id" => "key-1",
"encrypted_track" => "...",
"encryption_ksn" => "ksn-1"
},
"transacted_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
{:ok, response} = Teya.CardPresent.create(params)
response["status"] # "SUCCESS" | "FAILURE" | "PENDING"Reversal
Void a transaction before it settles with the card network. Use a refund
(Teya.Refund) for transactions that have already settled.
# Reverse by transaction ID
{:ok, response} = Teya.Reversal.create(%{
"reversal_reason" => "CARD_REVERSAL",
"transaction_id" => transaction_id
})
# Or by the idempotency key used when creating the original transaction
{:ok, response} = Teya.Reversal.create(%{
"reversal_reason" => "COMMUNICATION_REVERSAL",
"idempotency_key" => original_idempotency_key
})
response["status"] # "SUCCESS" | "FAILURE" | "PENDING" | "ACKNOWLEDGED"Dynamic Currency Conversion (DCC)
Before a card-present transaction, check whether the cardholder's card is eligible for DCC and retrieve a rate quote. No OAuth credentials are required for this endpoint.
case Teya.DCC.quote(%{
"store_id" => store_id,
"card_first9" => String.slice(card_number, 0, 9),
"base_amount" => 1000,
"base_currency" => "GBP"
}) do
{:ok, offer} ->
# Offer the cardholder: pay offer["cardholder_amount"] offer["cardholder_currency"]
# If accepted, include the quote in the card-present transaction:
dcc_params = %{
"quoted_at" => offer["quoted_at"],
"cardholder_amount" => %{
"amount" => offer["cardholder_amount"],
"currency" => offer["cardholder_currency"]
}
}
Teya.CardPresent.create(Map.put(card_present_params, "dcc", dcc_params))
{:error, %Teya.Error{code: code}} when code in ["NON_ELIGIBLE_CARD", "SAME_CURRENCY"] ->
# Card not eligible — proceed without DCC
Teya.CardPresent.create(card_present_params)
endPOSLink (Card-Present Terminals)
POSLink integrates ePOS software with physical payment terminals. Discover available stores and terminals, then create payment requests and stream their status in real time.
Discover stores and terminals
{:ok, %{"stores" => stores}} = Teya.POSLink.Store.list()
store_id = hd(stores)["store_id"]
{:ok, %{"terminals" => terminals}} = Teya.POSLink.Store.list_terminals(store_id)
terminal_id = hd(terminals)["terminal_id"]Take a card-present payment
Create a payment request and subscribe to real-time status updates via SSE. Events arrive as messages to the calling process:
params = %{
"store_id" => store_id,
"terminal_id" => terminal_id,
"requested_amount" => %{"amount" => 1000, "currency" => "GBP"}
}
{:ok, %{"payment_request_id" => id}} = Teya.POSLink.Payment.create(params)
{:ok, _task} = Teya.POSLink.Payment.subscribe(id, self())
receive do
{:poslink_payment, ^id, "full", %{"status" => "SUCCESSFUL"} = data} ->
# payment complete — data contains full transaction metadata
{:poslink_payment, ^id, _type, %{"status" => "FAILED"}} ->
# card declined or terminal error
{:poslink_payment, ^id, _type, %{"status" => status}} when status in ["NEW", "IN_PROGRESS"] ->
# intermediate state — keep waiting
{:poslink_payment_error, ^id, reason} ->
# connection or auth failure
endsubscribe/2 returns immediately; the task runs under Teya.TaskSupervisor
and sends messages until the server closes the stream. The second argument is
the recipient pid and defaults to self().
Task lifecycle: The spawned task is not linked to the caller and is not restarted by the supervisor. If the SSE stream drops mid-payment (network error, server restart), the task sends
{:poslink_payment_error, id, reason}and exits — there is no automatic reconnection. To recover, callTeya.POSLink.Payment.list/1to poll the current status, or callsubscribe/2again with the samepayment_request_id.
Cancel a payment
{:ok, _} = Teya.POSLink.Payment.cancel(payment_request_id)POSLink refunds
{:ok, _} = Teya.POSLink.Refund.create(%{
"store_id" => store_id,
"payment_request_id" => payment_request_id
})Print a receipt
Submit a receipt print job and stream its printer status:
{:ok, %{"receipt_id" => receipt_id}} =
Teya.POSLink.Receipt.create(%{
"store_id" => store_id,
"terminal_id" => terminal_id,
"content" => %{"type" => "JSON", "data" => %{"total" => "£10.00"}}
})
{:ok, _task} = Teya.POSLink.Receipt.subscribe_status(receipt_id, self())
receive do
{:poslink_receipt, ^receipt_id, _type, %{"status" => "PRINTED"}} -> :ok
{:poslink_receipt, ^receipt_id, _type, %{"status" => "FAILED"}} -> handle_failure()
{:poslink_receipt_error, ^receipt_id, reason} -> handle_error(reason)
endIdempotency Keys
POST and PATCH requests automatically include a random Idempotency-Key header. Supply your own to safely retry a request:
Teya.Checkout.create_session(params, idempotency_key: order_id)Error Handling
All functions return {:ok, body} or {:error, %Teya.Error{}}:
case Teya.Checkout.create_session(params) do
{:ok, response} -> response
{:error, %Teya.Error{code: "TOO_MANY_REQUESTS"}} -> {:error, :rate_limited}
{:error, %Teya.Error{code: "UNAUTHORISED"}} -> {:error, :unauthorized}
{:error, %Teya.Error{status: status}} -> {:error, status}
endTroubleshooting
Rate limiting (TOO_MANY_REQUESTS)
Teya returns HTTP 429 when you exceed the rate limit. Back off exponentially and retry using the same idempotency key to avoid duplicate operations:
case Teya.POSLink.Payment.create(params, idempotency_key: ref) do
{:error, %Teya.Error{code: "TOO_MANY_REQUESTS"}} ->
Process.sleep(1_000)
Teya.POSLink.Payment.create(params, idempotency_key: ref)
result ->
result
end3DS redirect flow
When Teya.Transaction.create/2 returns
{:ok, %{"type" => "REDIRECT_TRANSACTION_RESPONSE"}}, the cardholder must
complete a 3DS challenge before the payment is authorised. Redirect them to
resp["redirect_transaction_response"]["redirect_url"] and poll
Teya.Transaction.get/1 after they return to your success_url / failure_url.
SSE stream disconnects mid-payment
If a {:poslink_payment_error, id, _reason} message arrives before a terminal
status ("SUCCESSFUL", "FAILED", "CANCELLED"), the SSE connection dropped.
The payment may or may not have completed on the terminal. Check the current
state with Teya.POSLink.Payment.list/1 (filter by payment_request_id), then
re-subscribe with Teya.POSLink.Payment.subscribe/2 if still in progress.
Auth token refresh failures
If the token endpoint is unreachable, the auth process schedules a retry after
10 seconds. While retrying, API calls return {:error, reason}. The cached
token (if any) remains usable until it expires. Once connectivity is restored,
the retry succeeds automatically — no restart required.
Development
Requirements
- Elixir 1.17+, Erlang/OTP 25+ (see
.tool-versionsfor exact versions used locally) - Homebrew (macOS/Linux) for dev tooling
Setup
Install dependencies and git hooks:
./bin/setup
mix setup
./bin/setup installs actionlint,
check-jsonschema, and
Lefthook via Homebrew, then
activates the pre-commit hooks.
This installs pre-commit hooks (mix format, mix compile) and pre-push hooks
(mix credo, mix test).
Common commands
mix deps.get # install dependencies
mix test # run tests
mix format # format code
mix docs # generate documentation
Tests use Req.Test to stub HTTP — no network access or real credentials required.