omise-crystal
Omise PromptPay for Crystal
A minimal, dependency-free Crystal client for creating and verifying Omise PromptPay charges. It pins the Omise API contract to 2019-05-29 and 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 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.
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
Parse the raw webhook body, then retrieve and verify the charge through Omise:
event = Omise::Event.parse(request.body.not_nil!.gets_to_end)
verified_charge = Omise::Verifier.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.
The 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".
Webhook security and idempotency
Omise webhooks do not carry a cryptographic signature. Never trust the webhook body by itself. Always retrieve the charge from Omise before trusting paid or status; Omise::Verifier does this. Retrieval proves what Omise currently reports, but does not prove that the charge belongs to the order named by your route or session.
Your application must additionally:
- reconcile the verified charge ID, source ID, exact amount, and
THBcurrency against values stored for the expected order when the charge was created; - 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.
Development
mise x crystal@1.19.2 -- crystal spec
No runtime or test shards are required.
License
MIT © Xavier Luis Ablaza. See LICENSE.
omise-crystal
- 0
- 0
- 0
- 0
- 0
- about 1 hour ago
- August 2, 2026
MIT License
Sun, 02 Aug 2026 15:17:08 GMT