Enterprise-grade Rust SDK for Tapsilat Payment Processing Platform
cargo add tapsilat
Tapsilat is Turkey's leading fintech platform providing comprehensive payment processing solutions for businesses of all sizes. Our cutting-edge technology enables secure, fast, and reliable payment transactions with support for multiple payment methods, currencies, and advanced fraud protection.
use tapsilat::{Config, TapsilatClient};
let client = TapsilatClient::from_api_key("your-bearer-token")?;
// Or with advanced configuration
let config = Config::new("your-bearer-token")
.with_base_url("https://panel.tapsilat.dev/api/v1")
.with_timeout(30); // seconds, applied to the whole request
let client = TapsilatClient::new(config)?;The base URL points at API v1. Marketplace methods call API v2, derived from it by
replacing the trailing /api/v1 with /api/v2.
Request structs implement Default, so set the fields you need and fill the rest with
..Default::default(). Unset optional fields are left out of the request.
use tapsilat::{CreateBuyerRequest, CreateOrderRequest};
let order = client.create_order(CreateOrderRequest {
amount: 150.75,
currency: "TRY".to_string(),
locale: "tr".to_string(),
conversation_id: Some("order-12345".to_string()),
buyer: CreateBuyerRequest {
name: "John".to_string(),
surname: "Doe".to_string(),
email: Some("john.doe@example.com".to_string()),
gsm_number: Some("+90 555 123 45 67".to_string()), // cleaned to +905551234567
birth_date: Some("1990-01-02".to_string()),
..Default::default()
},
redirect_success_url: Some("https://shop.example/success".to_string()),
redirect_failure_url: Some("https://shop.example/failure".to_string()),
..Default::default()
})?;
println!("Order ID: {:?}", order.order_id);
println!("Reference ID: {:?}", order.reference_id);
println!("Checkout URL: {:?}", order.checkout_url); // filled from the created orderPayment terms sent with an order need a due_date, and their dates are RFC3339
timestamps such as "2026-01-15T00:00:00Z".
let status = client.get_order_status("reference-id")?;
println!("Order status: {}", status["status"]);- Orders: create, read, list with filters, cancel, refund, charge, payments and transactions
- Payment terms: read, add, replace, delete and refund terms; terminate orders
- Card installments and the installment table
- Organization settings, currencies, users and sub-organizations
- Submerchants, settlement approval and payout events
- Virtual POS and VPOS submerchants
- Saved cards (tokenization)
- Tapsilat Elements sessions for the native Android and iOS card sheet
- Subscriptions
- Marketplace sellers, payouts, payout operations and policies (API v2)
- Webhook signature verification (
Tapsilat-Signature), callback parsing and order statuses - GSM, installment, email, amount and identity number validation
let order = client.get_order("reference-id")?;
println!("Order amount: {:?}", order.amount);
println!("Order status: {:?} {:?}", order.status, order.status_enum);
let same_order = client.get_order_by_conversation_id("order-12345")?;use tapsilat::OrderListFilter;
let page = client.get_order_list(1, 10, None)?;
println!("Found {} orders", page["total"]);
let paid = client.get_order_list_with_filter(&OrderListFilter {
page: 1,
per_page: 50,
start_date: Some("2026-01-01".to_string()),
status: Some(3),
..Default::default()
})?;match client.cancel_order("reference-id") {
Ok(_) => println!("Order cancelled successfully"),
Err(e) => eprintln!("Cannot cancel order: {}", e),
}use tapsilat::GetOrderPaymentsRequest;
let details = client.get_order_payment_details("reference-id", None)?;
let by_conversation = client.get_order_payment_details_by_conversation_id("order-12345")?;
let transactions = client.get_order_transactions("reference-id")?; // a JSON array, or null
let payments = client.get_order_payments(GetOrderPaymentsRequest {
order_reference_id: Some("reference-id".to_string()),
..Default::default()
})?;
println!("Merchant net amount: {}", payments.total_merchant_net_amount);
let checkout_url = client.get_checkout_url("reference-id")?;use tapsilat::OrderChargeRequest;
let result = client.charge_order(OrderChargeRequest {
order_reference_id: "reference-id".to_string(),
retry: false,
})?;
println!("Charged: {} ({})", result.is_success, result.message);use tapsilat::RefundOrderRequest;
// Partial refund, optionally limited to one basket item or item payment
let refund = client.refund_order(RefundOrderRequest {
amount: 50.0,
reference_id: "reference-id".to_string(),
..Default::default()
})?;
// Full refund
let full_refund = client.refund_all_order("reference-id")?;Term dates use the YYYY-MM-DD format. order_id is the order's ID (order_id from
create_order), not its reference ID.
use tapsilat::{OrderPaymentTermCreateDTO, OrderPaymentTermUpdateDTO, OrderTermRefundRequest};
client.create_order_term(OrderPaymentTermCreateDTO {
order_id: "order-id".to_string(),
term_reference_id: "term-1".to_string(),
amount: 50.0,
due_date: "2026-02-01".to_string(),
term_sequence: 1,
required: true,
status: "PENDING".to_string(),
..Default::default()
})?;
let term = client.get_order_term("term-1")?;
// An update replaces the term, so send every field
client.update_order_term(OrderPaymentTermUpdateDTO {
term_reference_id: "term-1".to_string(),
amount: Some(60.0),
due_date: Some("2026-02-15".to_string()),
required: Some(true),
status: Some("PENDING".to_string()),
term_sequence: Some(1),
..Default::default()
})?;
client.refund_order_term(OrderTermRefundRequest {
term_id: "term-id".to_string(),
amount: 10.0,
reference_id: Some("reference-id".to_string()),
term_payment_id: None,
})?;
client.delete_order_term("order-id", "term-1")?;
client.order_terminate("reference-id")?;use tapsilat::{CardInstallmentsRequest, InstallmentTableRequest};
let options = client.get_card_installments(&CardInstallmentsRequest {
bin: "54066200".to_string(),
amount: 1000.0,
currency: "TRY".to_string(),
is_three_d: true,
})?;
for option in options.installments {
println!("{} x {} (total {})", option.installment, option.monthly_payment, option.total_amount);
}
let table = client.get_installment_table(&InstallmentTableRequest {
amount: 1000.0,
currency: "TRY".to_string(),
is_three_d: false,
})?;let settings = client.organization().settings()?;
let currencies = client.organization().currencies()?;
let presets = client.list_organization_currency_presets()?;
client.create_organization_currency("EUR")?;
// Most endpoints accept currency units such as "TRY"; resolve one to its UUID when needed
let currency_id = client.resolve_currency_id("TRY")?;
let suborganizations = client.organization().list_suborganizations(1, 20)?;
let limits = client.get_organization_limits_by_id("organization-id")?;use tapsilat::{SubmerchantCreateRequest, SubmerchantPaymentAction};
let submerchants = client.submerchants();
submerchants.create(SubmerchantCreateRequest {
locale: "tr".to_string(),
conversation_id: "conv-1".to_string(),
name: "Seller".to_string(),
email: "seller@example.com".to_string(),
gsm_number: "+905551234567".to_string(),
sub_merchant_type: "PERSONAL".to_string(),
identity_number: "11111111111".to_string(),
currency_id: "TRY".to_string(),
..Default::default()
})?;
let page = submerchants.list(1, 20)?;
let submerchant = submerchants.get(&page.rows[0].id)?;
submerchants.approve_payment(SubmerchantPaymentAction {
locale: "tr".to_string(),
conversation_id: "conv-1".to_string(),
payment_transaction_id: "transaction-id".to_string(),
acquirer: "acquirer".to_string(),
})?;A VPOS update replaces the whole configuration, so read it first and send every field back.
use tapsilat::VposUpdateRequest;
let vpos = client.vpos();
let current = vpos.get("vpos-id")?;
vpos.update("vpos-id", VposUpdateRequest {
name: current.name,
bank_name: current.bank_name,
failover: true,
main_installment: true,
..Default::default()
})?;
let acquirers = vpos.list_acquirers()?;let cards = client.tokenization().list_saved_cards(1, 10)?;
client.tokenization().delete_saved_card(&cards.rows[0].id)?;Tapsilat Elements is the native Android and iOS card sheet. Your backend creates a session
with its secret API key and passes only the session's client_secret to the app, which opens
the sheet with it. Card data goes from the sheet to Tapsilat encrypted and never reaches your
backend.
use tapsilat::{ElementsSession, ElementsSessionCreateRequest};
let created = client.create_elements_session(ElementsSessionCreateRequest {
purpose: ElementsSession::PURPOSE_PAY.to_string(),
order_reference_id: Some("reference-id".to_string()),
save_card: true,
..Default::default()
})?;
let session_id = created.session.id.clone().unwrap_or_default();
// Send session_id and created.client_secret to your app. Do not log or store the secret.
// Later, read the outcome on your backend instead of trusting the app
let outcome = client.get_elements_session(&session_id)?;
if outcome.status.as_deref() == Some(ElementsSession::STATUS_SUCCEEDED) {
// fulfil the order; outcome.card holds the brand, last4 and expiry
}
// Cancel a session that is still waiting for a card
client.cancel_elements_session(&session_id)?;create_elements_session(request) -> Result<ElementsSessionWithSecret>:POST /elements/sessionsget_elements_session(id) -> Result<ElementsSession>:GET /elements/sessions/{id}cancel_elements_session(id) -> Result<ElementsSession>:POST /elements/sessions/{id}/cancel
The same calls are available as client.elements().create_session(request),
get_session(id) and cancel_session(id).
A pay session needs order_reference_id. A tokenize session saves a card for user_id,
with mode set to ElementsSessionCreateRequest::MODE_SECURE (3D Secure), MODE_HALF_SECURE
or MODE_NON_SECURE. Cancelling a session that is confirming or has completed returns it
unchanged.
status, purpose and the card brand are strings, so values added later still decode;
compare them with the ElementsSession::STATUS_*, ElementsSession::PURPOSE_* and
ElementsCardSummary::BRAND_* constants. Timestamps are RFC 3339 strings.
The client secret is returned only by create_elements_session. ElementsSessionWithSecret
prints it as [redacted] with {:?}, and serializing it with serde_json keeps it, so you
can still send the session to the app.
Seller and payout revisions are opaque strings. Send them back exactly as received.
use tapsilat::{MarketplaceApprovalPolicyInput, MarketplacePayoutApproval, MarketplacePayoutFilter};
let marketplace = client.marketplace();
let sellers = marketplace.list_submerchants(1, 20)?;
let payouts = marketplace.list_payouts(&MarketplacePayoutFilter {
status: Some("WAITING_APPROVAL".to_string()),
page: 1,
per_page: 20,
..Default::default()
})?;
marketplace.approve_payouts(MarketplacePayoutApproval {
ids: payouts.rows.iter().map(|p| p.id.clone()).collect(),
})?;
// The approval policy currency must be a UUID; a unit such as "TRY" is resolved for you
marketplace.create_approval_policy(MarketplaceApprovalPolicyInput {
name: "Large orders".to_string(),
priority: 1,
enabled: Some(true),
min_amount: Some("1000".to_string()),
currency_id: "TRY".to_string(),
mode: "manual".to_string(),
..Default::default()
})?;Tapsilat signs every callback it sends to your callback URLs, and to an order's
payment_success_url and payment_failure_url, with the webhook secret from the panel's
callback settings. The Tapsilat-Signature header looks like t=1726480000,v1=5257a8…:
v1 is the hex HMAC-SHA256 of the timestamp, a dot and the raw body.
WebhookModule::verify_webhook_with checks it in constant time and rejects a timestamp more
than 5 minutes from the server clock, so a captured callback cannot be replayed. Pass the raw
body, not re-encoded JSON.
use http::HeaderMap; // axum::http::HeaderMap, or your framework's header map
use tapsilat::{WebhookEvent, WebhookModule, WebhookVerifyOptions, WEBHOOK_SIGNATURE_HEADER};
/// `body` is the raw request body. Returns the HTTP status to answer with.
fn handle_callback(headers: &HeaderMap, body: &[u8], secret: &str) -> u16 {
let signature = headers
.get(WEBHOOK_SIGNATURE_HEADER) // "Tapsilat-Signature"
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
let options = WebhookVerifyOptions::default();
if !WebhookModule::verify_webhook_with(body, signature, secret, &options) {
return 400;
}
let Ok(event) = WebhookEvent::from_payload(body) else {
return 400;
};
println!(
"{:?} callback for order {:?} ({:?}): {:?}",
event.event_type, // Some("order_lite") (the default), "order", "order_payment_detail", ...
event.reference_id(), // the order's reference ID, whatever the callback type
event.conversation_id(),
event.status(), // e.g. Some(OrderStatus::Paid), when the callback type carries a status
);
if event.is_paid() {
// fulfil the order
}
200
}WebhookModule::verify_webhook(payload, signature, secret) runs the same check on a &str
body with the default options. A different tolerance in seconds can be set with
WebhookVerifyOptions::default().with_tolerance(600) (0 disables the timestamp check), and
with_now(unix_seconds) compares against a fixed time in tests. verify_webhook_advanced
also reports why a signature was rejected. A bare hex HMAC of the body, with or without a
sha256= prefix, is still accepted for signatures made the way earlier SDK versions
documented.
OrderStatus has the numeric status codes that callbacks and order details carry, the names
get_order_status returns, and the groups most integrations need:
use tapsilat::OrderStatus;
let status: OrderStatus = "Waiting for payment".parse()?; // also "7"; OrderStatus::from_code(7)
status.code(); // 7
status.name(); // "Waiting for payment"
status.is_paid(); // Paid, Completed, Sub merchant payment approved
status.is_failed(); // Cancelled, Fraud, Rejected, Failure, Expired, Terminated
status.is_refunded(); // Refunded, Partially refundedAPI errors carry the HTTP status, the body's status, code and message, any field
errors, whether the request can be retried, and the raw body. A response that is not JSON,
such as an HTML page from a wrong base URL, is also reported as an ApiError.
Elements routes nest the error as
{"error": {"code": "session_not_found", "message": "...", "retryable": false}}; code,
message and retryable are read from that object. retryable is false when the body does
not say.
use tapsilat::TapsilatError;
match client.cancel_order("reference-id") {
Err(TapsilatError::ApiError { status_code, code, message, errors, retryable, .. }) => {
eprintln!("{} [{:?}] {} {:?} retryable={}", status_code, code, message, errors, retryable);
}
Err(other) => eprintln!("{}", other),
Ok(response) => println!("{}", response),
}use tapsilat::Validators;
// Clean a phone number the way create_order does
let gsm = Validators::validate_gsm_number("+90 (555) 123-45-67")?; // "+905551234567"
// Require a Turkish mobile number and normalize it
let turkish = Validators::validate_gsm("0555 123 45 67")?; // "905551234567"
// Parse a comma-separated installment list
let installments = Validators::parse_installments("1,3,6")?; // [1, 3, 6]
Validators::validate_email("user@example.com")?;
Validators::validate_amount(99.99)?;
Validators::validate_identity_number("10000000146")?;These methods call routes the API does not have and always fail. They are kept for compatibility:
client.payments()(/payments): use the order methods, such asget_order_paymentsandcharge_order.- Installment plans (
/installments): useget_card_installmentsand the payment term methods. terminate_order_term: useorder_terminateordelete_order_term.get_organization_limit_userandset_organization_limit_user.get_organization_limits(): useget_organization_limits_by_id.
Use Bearer Token authentication:
// Simple initialization
let client = TapsilatClient::from_api_key("your-bearer-token")?;
// Or via environment variable
use std::env;
let api_key = env::var("TAPSILAT_BEARER_TOKEN")?;
let client = TapsilatClient::from_api_key(api_key)?;Get your API token from the Tapsilat Dashboard → Settings → API Keys
This project is licensed under the MIT License - see the LICENSE file for details.
- Website: https://tapsilat.dev
- Issues: GitHub Issues
- Examples: See examples/advanced_usage.rs for a complete implementation
All Rust types are organized in src/types/ with comprehensive documentation including:
- Strong compile-time type safety
- Zero-cost abstractions
- Memory safety guarantees
- Comprehensive error handling with
Resulttypes