omise
Version, currently main branch1 version
- main branchlatestAug 4, 2026
github.com/hostari/omise-crystal
No description declared in shard.yml.
Installation
# Add this to your shard.yml
dependencies:
omise:
github: hostari/omise-crystal
branch: mainmain is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
~> 1.19.2- License
- MIT
- Author
- Xavier Luis Ablaza
Dependencies
This version declares no dependencies.
README
Omise for Crystal
A minimal, dependency-free Crystal client for Omise Payment Links+ hosted checkout and core-API card, PromptPay, and TrueMoney Wallet JumpApp charges. The core API is pinned to 2019-05-29; Payment Links+ uses its separately documented API. Supports Crystal ~> 1.19.2.
Installation
Add the shard to your application's shard.yml:
dependencies:
omise:
path: ../omise-crystal
Adjust the path to the local checkout relative to your application's shard.yml.
Then run shards install and require it:
require "omise"
Create a hosted Checkout Session
Hosted checkout uses Omise Payment Links+, not the legacy core API's /links endpoint. Payment Links+ has its own dashboard, API key, hostname, team ID, and template ID. Retrieve these from the Payment Links+ dashboard and its Settings > API Keys and Team Settings pages.
checkout = Omise::PaymentLinksClient.new(
api_key: ENV["OMISE_PAYMENT_LINKS_API_KEY"]
)
session = checkout.create_checkout_session(
template_id: ENV["OMISE_PAYMENT_LINKS_TEMPLATE_ID"].to_i64,
team_id: ENV["OMISE_PAYMENT_LINKS_TEAM_ID"].to_i64,
amount: 10_000_i64, # THB 100.00 in minor units
name: "Order #42",
currency: "THB",
multiple_usage: false,
features: {"Type" => "Game server"}, # only when required by the template
custom_webhook: "https://merchant.example/omise-webhook",
return_url: "https://merchant.example/payment-status"
)
# In your framework's authenticated, CSRF-protected POST action:
response.redirect session.url
The client sends the Payment Links+ API key directly in Authorization to POST https://linksplus-api.omise.co/external/links, as required by Omise's documentation. Do not use OMISE_PUBLIC_KEY, OMISE_SECRET_KEY, or a webhook secret here; those are different credentials.
The returned Omise::CheckoutSession exposes the documented Link fields, including id, team_id, name, amount, currency, multiple_usage, transaction_id, and transaction_url. session.url aliases transaction_url. The client checks the request-correlated response fields and only accepts an active HTTPS checkout URL on the exact linksplus.omise.co host with no userinfo or nonstandard port.
Payment Links+ creation requires an existing template. Any feature fields configured on that template are conditionally required and must be supplied through features. If currency or multiple_usage is omitted, Omise uses the template's setting. custom_webhook and return_url are optional; as an intentional client-side security policy, this shard requires both to use HTTPS without userinfo or a nonstandard port. Omise does not document an idempotency-key parameter for this endpoint, so applications should persist the resulting Link ID against their order and avoid retrying an ambiguous request blindly.
The shard's included Omise::Event and Omise::Verifier handle core Omise PromptPay charge completion only. Payment Links+ webhooks use a separate integration surface. Authenticate and reconcile those events according to your Payment Links+ dashboard configuration before fulfillment.
Create a card charge
Card details must be tokenized in the customer's browser with Omise.js. This shard intentionally accepts only the resulting single-use tokn_... token; never send or store a PAN, expiry date, or security code on your Crystal backend.
client = Omise::Client.new(
public_key: ENV["OMISE_PUBLIC_KEY"],
secret_key: ENV["OMISE_SECRET_KEY"]
)
charge = client.create_card_charge(
token: browser_supplied_token,
amount: 10_000_i64, # minor units
currency: "THB", # normalized to uppercase; defaults to THB
description: "Order #42",
metadata: {"order_id" => "42"},
return_uri: "https://merchant.example/payments/return"
)
case charge.result
when Omise::ChargeResult::Successful
# Wait for/reconcile the authoritative charge.complete event before fulfillment.
when Omise::ChargeResult::AuthorizationRequired
response.redirect charge.authorization_uri.not_nil!.to_s
when Omise::ChargeResult::Failed
Log.warn { "card charge failed: #{charge.failure_code}" } # never log tokens or keys
else
# Do not fulfill an indeterminate/pending charge.
end
create_card_charge posts amount, currency, and card to the core /charges endpoint using secret-key Basic authentication. description, metadata[...], and return_uri are optional. Input is validated before transport. Return URLs must use HTTPS, except that HTTP is accepted for the loopback development hosts localhost, 127.0.0.1, and ::1. A returned authorization URL is accepted only when it is HTTPS on the exact api.omise.co host, uses the default port, has no userinfo, and has a non-root /payments/... path.
Omise::Charge supports nullable source, card, customer/schedule IDs, lifecycle timestamps, and string metadata fields. Use source_type?, source_of_type, source_of_type!, promptpay?, card?, and the typed instrument helpers rather than assuming either payment instrument is present. Lifecycle helpers include successful?, authorization_required?, failed?, expired?, reversed?, terminal?, and the typed result; failure details are available as failure_code and failure_message.
Save a card and charge renewals
Convert an unused browser token into an Omise customer before it is consumed by a direct charge:
customer = client.create_customer(
token: browser_supplied_token,
email: current_user.email,
metadata: {"user_id" => current_user.id.to_s}
)
first_charge = client.create_customer_card_charge(
customer_id: customer.id,
amount: 10_000_i64,
return_uri: "https://merchant.example/payments/card-return",
metadata: {"invoice_id" => invoice.id.to_s}
)
For an unattended renewal, send the stored first-charge ID so the provider can associate the recurring cycle:
renewal = client.create_recurring_card_charge(
customer_id: customer.id,
amount: 10_000_i64,
first_charge: stored_first_charge_id,
metadata: {"invoice_id" => invoice.id.to_s}
)
The renewal helper always sends transaction_indicator=MIT and defaults recurring_reason to standing_order; attended customer-card charges default to CIT. A specific reusable card_id can be supplied, or omitted to use the customer's default card. Use add_customer_card with a fresh browser token to add a replacement card. Store only provider customer/card identifiers and display-safe card fields—never raw card data.
Create a PromptPay charge
Amounts are integers in minor currency units (satang). PromptPay charges use THB.
client = Omise::Client.new(
public_key: ENV["OMISE_PUBLIC_KEY"],
secret_key: ENV["OMISE_SECRET_KEY"]
)
charge = client.create_promptpay_charge(
10_000_i64, # THB 100.00
description: "Order #42",
metadata: {"order_id" => "42"}
)
puts charge.id
puts charge.promptpay_qr_uri
The client first creates a source with the public key, then creates its charge with the secret key. promptpay_qr_uri reads charge.source.scannable_code.image.download_uri and raises Omise::ResponseError if Omise does not return that path.
Create a TrueMoney JumpApp charge
TrueMoney Wallet App Redirection is available for Thailand in THB. Its documented limits are THB 20 through THB 50,000 (2,000 through 5,000,000 minor units). The client creates the source and charge in one secret-key request and validates the provider redirect before exposing it:
charge = client.create_truemoney_jumpapp_charge(
amount: 10_000_i64,
return_uri: "https://merchant.example/payments/truemoney-return",
description: "Order #42",
metadata: {"order_id" => "42"}
)
response.redirect charge.authorization_uri.not_nil!.to_s
Only exact HTTPS api.omise.co and pay.omise.co authorization hosts are accepted. The authorization session is short-lived, and returning from the wallet app is not proof of payment. Reconcile a signed webhook—or explicitly retrieve the charge on return—against the stored order. Omise::TrueMoneyJumpAppVerifier retrieves the authoritative charge and accepts successful, failed, or expired terminal outcomes.
Use a different HTTPS API origin for testing if needed. A base path is preserved and API paths are joined beneath it:
client = Omise::Client.new("pkey", "skey", api_base: "https://example.test/v1")
The client rejects bases without a host and bases containing userinfo, a query, or a fragment. HTTPS is mandatory by default. Plain HTTP can only be enabled explicitly for loopback test servers:
client = Omise::Client.new(
"pkey",
"skey",
api_base: "http://127.0.0.1:3000",
allow_insecure_api_base: true
)
Transport limits
The default Omise::HTTPTransport uses finite connect, read, and write timeouts of 5, 30, and 30 seconds. It caps response bodies at 1 MiB and raises Omise::TransportError if that cap is exceeded. Applications can tune these limits while retaining the production transport:
transport = Omise::HTTPTransport.new(
connect_timeout: 3.seconds,
read_timeout: 15.seconds,
write_timeout: 15.seconds,
max_response_body_bytes: 512_i64 * 1024
)
client = Omise::Client.new("pkey", "skey", transport: transport)
A custom Omise::Transport can still be injected for tests or specialized networking. The response cap protects API responses; it does not limit incoming webhook request bodies. Configure a request-body limit in your HTTP server or reverse proxy before reading webhook JSON.
Verify a webhook
Verify the signature over the unmodified request body, parse it, then retrieve and verify the charge through Omise:
raw_body = request.body.not_nil!.gets_to_end
Omise::WebhookSignature.verify!(
raw_body,
request.headers["Omise-Signature"],
request.headers["Omise-Signature-Timestamp"],
ENV["OMISE_WEBHOOK_SECRET"]
)
event = Omise::Event.parse(raw_body)
verified_charge = Omise::Verifier.new(client).verify(event) # PromptPay
# Or, for a card order:
verified_card_charge = Omise::CardVerifier.new(client).verify(event)
# Or, for TrueMoney Wallet:
verified_truemoney_charge = Omise::TrueMoneyJumpAppVerifier.new(client).verify(event)
# Only now transition your expected order, idempotently.
Omise sends the affected charge directly in the event's data field. Both event.data and the convenience method event.charge return that charge.
Omise::Verifier requires charge.complete, requires PromptPay, retrieves GET /charges/:id with the secret key, compares charge ID, amount, currency, and source ID with the webhook, then requires the retrieved charge to have paid == true and status == "successful". Omise::CardVerifier accepts charge.create (needed for successful non-3DS cards) and charge.complete, retrieves card events authoritatively, and compares the card ID. It accepts either a successful or failed terminal card charge so applications can reconcile 3DS declines and unknown outcomes; nonterminal card charges are rejected.
Webhook security and idempotency
When webhook signing is enabled in the Omise dashboard, Omise::WebhookSignature.verify! validates the current HMAC-SHA256 protocol, accepts comma-separated signatures during secret rotation, and rejects timestamps outside a five-minute replay window by default. Pass the exact raw request bytes; parsing or re-serializing first changes the signed payload. The dashboard secret is expected in its documented Base64 form.
Always retrieve the charge from Omise before trusting paid or status; both Omise::Verifier and Omise::CardVerifier do this. Signature verification authenticates delivery, while retrieval confirms the provider's current state. Neither proves that the charge belongs to the order named by your route or session.
Your application must additionally:
- for PromptPay, reconcile the verified charge ID, source ID, exact amount, and
THBcurrency against the stored order; - for cards, reconcile the verified charge ID, expected amount and currency, and the stored order/customer association—matching a provider card ID does not establish order ownership;
- for TrueMoney, reconcile the charge ID, source ID, exact amount, and
THBcurrency against the stored order; - reject an otherwise valid charge that belongs to another customer or order;
- store processed event and/or charge IDs under a unique constraint;
- make fulfillment transactional and idempotent because webhook deliveries can be duplicated or reordered;
- return a successful webhook response only after the durable transition is complete (or safely queued).
Errors
Omise::APIErrorretains the HTTP status, raw body, and Omisecode,location, andmessagefields when available.Omise::TransportErrorreports transport safety failures such as an oversized API response.Omise::ResponseErrorreports malformed or semantically inconsistent API/webhook JSON or a missing QR URI.Omise::VerificationErrorreports rejected webhook verification.Omise::SignatureVerificationErrorreports missing, malformed, stale, or mismatched webhook signatures.
Development
mise x crystal@1.19.2 -- crystal spec
No runtime or test shards are required.
License
MIT © Xavier Luis Ablaza. See LICENSE.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
main- Seen
- Aug 4, 2026
- Crystal
~> 1.19.2- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/hostari/omise-crystal
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 15, 2026
- Synced
- Aug 15, 2026
- Versions
- 1