Skip to content

About

The Tapsilat SDK for Rust

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

142 Commits

Folders and files

Repository files navigation

Tapsilat Rust SDK

Enterprise-grade Rust SDK for Tapsilat Payment Processing Platform

cargo add tapsilat

About 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.


Quick Start

Initialize the SDK

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.

Create an Order

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 order

Payment terms sent with an order need a due_date, and their dates are RFC3339 timestamps such as "2026-01-15T00:00:00Z".

Check Order Status

let status = client.get_order_status("reference-id")?;
println!("Order status: {}", status["status"]);

Features

  • 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

API Methods & Examples

Order Management

Get Order Details

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")?;

List Orders

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()
})?;

Cancel Order

match client.cancel_order("reference-id") {
    Ok(_) => println!("Order cancelled successfully"),
    Err(e) => eprintln!("Cannot cancel order: {}", e),
}

Payments, Transactions and Checkout URL

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")?;

Charge an Order

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);

Refund Operations

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")?;

Payment Terms

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")?;

Card Installments

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,
})?;

Organization

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")?;

Submerchants

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(),
})?;

Virtual POS

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()?;

Saved Cards

let cards = client.tokenization().list_saved_cards(1, 10)?;
client.tokenization().delete_saved_card(&cards.rows[0].id)?;

Tapsilat Elements Sessions

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/sessions
  • get_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.

Marketplace (API v2)

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()
})?;

Webhook Handling

Verify Webhook Signature

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.

Order Statuses

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 refunded

Error Handling

API 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),
}

Validation Utilities

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")?;

Deprecated Methods

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 as get_order_payments and charge_order.
  • Installment plans (/installments): use get_card_installments and the payment term methods.
  • terminate_order_term: use order_terminate or delete_order_term.
  • get_organization_limit_user and set_organization_limit_user.
  • get_organization_limits(): use get_organization_limits_by_id.

Authentication

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


License

This project is licensed under the MIT License - see the LICENSE file for details.


Resources

Type System

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 Result types

Tapsilat

About

The Tapsilat SDK for Rust

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages