razorpay_dart 0.1.0
razorpay_dart: ^0.1.0 copied to clipboard
A Dart port of the official razorpay-node SDK for the Razorpay API.
razorpay_dart #
A Dart port of the official razorpay-node
SDK, tracking release 2.9.8. Pure Dart, no Flutter dependency.
⚠️ This belongs on a server, not in an app #
This SDK authenticates with your account's key_secret, which grants full
access to your Razorpay account: creating orders, issuing refunds, moving
settlements.
Never ship it inside a Flutter app or a web bundle. Anything on a device can
be decompiled, so a secret placed there is a published secret. Use it only from
a server you control — dart_frog, shelf, serverpod, a Cloud Function, a
CLI. Your app should talk to your backend, and your backend talks to Razorpay.
For collecting payments in a Flutter app, use Razorpay's own razorpay_flutter checkout plugin instead, and verify the resulting signature on your server with this package.
Install #
dependencies:
razorpay_dart: ^0.1.0
dart pub add razorpay_dart
Quick start #
import 'dart:io';
import 'package:razorpay_dart/razorpay_dart.dart';
Future<void> main() async {
final razorpay = Razorpay(
keyId: Platform.environment['RAZORPAY_KEY_ID'],
keySecret: Platform.environment['RAZORPAY_KEY_SECRET'],
);
// Amounts are in the smallest currency unit: 50000 paise = ₹500.
final order = await razorpay.orders.create(
params: const RazorpayOrderCreateRequestBody(
amount: 50000,
receipt: 'receipt#1',
notes: {'purpose': 'demo'},
),
);
print('${order.id} — ${order.amount} ${order.currency}');
}
Every method resolves to the entity itself, matching razorpay-node. There is
no response wrapper and no HTTP client type in any signature.
Authentication #
An API key pair:
final razorpay = Razorpay(keyId: '...', keySecret: '...');
Or, as a platform partner, an OAuth access token:
final razorpay = Razorpay(oauthToken: '...');
Partners acting on a submerchant's behalf can add the account header:
final razorpay = Razorpay(
keyId: '...',
keySecret: '...',
headers: {'X-Razorpay-Account': 'acc_XXXXXXXX'},
);
Other headers are ignored, so a stray one cannot displace authentication.
Verifying signatures #
This is the part that matters for security. A razorpay_payment_id arriving at
your endpoint proves nothing on its own — anyone can post one. Only the
signature does.
// In your Checkout callback handler.
final valid = Razorpay.validatePaymentVerification(
params: {
'order_id': orderId,
'payment_id': paymentId,
},
signature: razorpaySignature,
secret: keySecret,
);
if (!valid) {
// Treat the payment as not made. Do not fulfil the order.
}
For webhooks, pass the raw request body:
final valid = Razorpay.validateWebhookSignature(
body: rawRequestBody, // exactly as received, byte for byte
signature: request.headers['x-razorpay-signature']!,
secret: webhookSecret,
);
Do not decode and re-encode the body first. Re-serialising JSON reorders keys and changes whitespace, which changes the HMAC and makes every legitimate webhook look forged.
RazorpaySignature exposes the same functions, plus
RazorpaySignature.paymentPayload for inspecting exactly what is being signed.
Comparison is constant-time, and every function is pinned by tests to vectors
generated from razorpay-node itself.
Error handling #
Errors are typed, so you can tell a rejected request from one that never arrived:
try {
await razorpay.orders.create(params: params);
} on RazorpayApiException catch (e) {
// Razorpay replied with a non-2xx. Your request was the problem.
print('${e.statusCode} ${e.code}: ${e.description}');
print('field: ${e.field}');
} on RazorpayNetworkException catch (e) {
// Nothing reached Razorpay: timeout, DNS, TLS. Safe to retry.
print(e.message);
} on RazorpaySerializationException catch (e) {
// The response did not fit the model. This is a bug in this package --
// please report it, with e.responseBody.
print(e);
}
All three extend RazorpayException, so catch that to handle any SDK failure.
Pagination #
List endpoints return RazorpayList<T>:
final page = await razorpay.orders.all(
params: const RazorpayOrderQuery(count: 25, skip: 0),
);
for (final order in page.items) {
print(order.id);
}
count and skip are omitted from the request when you do not set them, so
Razorpay's own defaults (count: 10, skip: 0) apply.
Date filters accept whichever form you have:
await razorpay.orders.all(
params: RazorpayOrderQuery(
from: DateTime.now().subtract(const Duration(days: 7)),
to: 1700000000, // epoch seconds
),
);
// or an ISO 8601 string: from: '2023-11-14T22:13:20Z'
Uploading files #
File endpoints take a RazorpayFile, so you never need to depend on the HTTP
client:
await razorpay.documents.create(
purpose: 'dispute_evidence',
file: RazorpayFile.fromPath('evidence.pdf'),
);
await razorpay.accounts.uploadAccountDoc(
accountId: 'acc_XXXXXXXX',
documentType: 'business_proof_url',
file: RazorpayFile.fromBytes(bytes, filename: 'proof.pdf'),
);
OAuth for platform partners #
final oauth = OAuthTokenClient();
// 1. Send the merchant here to grant access.
final url = oauth.generateAuthUrl(
params: const InitiateAuthorisationRequest(
client_id: 'XXXXXXXXXXXXXX',
response_type: 'code',
redirect_uri: 'https://example.com/callback',
scope: ['read_write'],
state: 'a-random-nonce-you-check-on-return',
),
);
// 2. Exchange the code your callback receives.
final token = await oauth.getAccessToken(
params: const OAuthTokenRequest(
client_id: 'XXXXXXXXXXXXXX',
client_secret: '...',
redirect_uri: 'https://example.com/callback',
code: '...',
),
);
// 3. Act on the merchant's behalf.
final razorpay = Razorpay(oauthToken: token.access_token);
// Later, before it expires:
final refreshed = await oauth.refreshToken(
params: OAuthTokenRequest(
client_id: 'XXXXXXXXXXXXXX',
client_secret: '...',
refresh_token: token.refresh_token,
),
);
Malformed input throws ArgumentError rather than being sent, so a bad
redirect_uri fails at the call site instead of as a redirect that never
arrives.
Resources #
Reached from the Razorpay instance:
razorpay.accounts |
razorpay.addons |
razorpay.cards |
razorpay.customers |
razorpay.disputes |
razorpay.documents |
razorpay.fundAccount |
razorpay.iins |
razorpay.invoices |
razorpay.items |
razorpay.orders |
razorpay.paymentLink |
razorpay.payments |
razorpay.plans |
razorpay.products |
razorpay.qrCode |
razorpay.refunds |
razorpay.settlements |
razorpay.stakeholders |
razorpay.subscriptions |
razorpay.tokens |
razorpay.transfers |
razorpay.virtualAccounts |
razorpay.webhooks |
Method names match razorpay-node, so its
documentation
and the Razorpay API reference both apply.
Arguments are named rather than positional.
Testing against this package #
Point a client at your own stub server:
final razorpay = Razorpay(
keyId: 'test',
keySecret: 'test',
baseUrl: 'http://127.0.0.1:8080',
);
test/support/mock_api.dart in this repository is a recording stub server you
can crib from.
How closely this matches razorpay-node #
Different layers are verified differently, so it's worth being precise:
| Layer | How it's verified |
|---|---|
| Endpoints — verb, API version, path | Machine-checked. All 148 endpoints across 142 methods match razorpay-node@2.9.8 exactly. Enforced in CI. |
| Signature algorithms | Verified by execution. Test vectors were produced by running razorpay-node's own razorpay-utils.js, and are committed as fixtures. |
| Request bodies and query strings | Asserted against a recording stub server in the resource tests, but not diffed against razorpay-node. |
| Response models | Hand-ported from razorpay-node's .d.ts. Least verified — see the caveat below. |
tool/node_endpoints.json is a manifest of razorpay-node's endpoint surface,
extracted by running the official package against a recording stub
(tool/extract_node_endpoints.js). tool/check_parity.py compares this package
against it and runs in CI, so drift fails the build:
python3 tool/check_parity.py --verbose
Two differences are deliberate and normalised away, both because razorpay-node bakes into a path what belongs in a query string, or leaves a stray trailing slash. The script's docstring explains each.
Caveat on response models #
The models were ported from razorpay-node's TypeScript declarations, and those
declarations are wrong in places — RazorpayCard marks number and cvv
required, though Razorpay never returns either. Cases like that were fixed where
found, but the models have not been validated field-by-field against live
API responses. If a response fails to decode you'll get a
RazorpaySerializationException naming the field; please
open an issue with it.
About the pub.flutter-io.cn score #
One line item is worth explaining rather than leaving a mystery: pub.flutter-io.cn's
"pass static analysis" score does not use this package's analysis_options.yaml.
It substitutes its own fixed rule set when computing that score, and that
substitution ignores this repository's analyzer: exclude for generated code
and its analyzer: errors: {non_constant_identifier_names: ignore} override.
The practical effect: every field this package exposes as snake_case — because
that's the wire format Razorpay's JSON API uses (account_id, created_at,
payment_link_id, and so on throughout every model) — gets flagged by pub.flutter-io.cn's
scorer in both the hand-written model file and its generated .freezed.dart /
.g.dart counterparts, once per field. That's the overwhelming majority of the
static-analysis point loss, and it is a naming-convention observation, not a
defect: dart analyze --fatal-infos against this repository's own
(non-overridden) rule set reports zero issues, and that is what CI enforces.
Renaming every field to camelCase with a @JsonKey(name: '...') mapping back
to the wire format would close this gap, at the cost of touching every model in
the package for a purely cosmetic score improvement. Not planned for now; happy
to revisit if it turns out to matter to users choosing between packages.
Contributing #
dart pub get
dart run build_runner build --delete-conflicting-outputs
dart analyze --fatal-infos
dart test
python3 tool/check_parity.py
Generated files (*.freezed.dart, *.g.dart) are committed, and CI fails if
they differ from what the builders produce — so run build_runner and commit
its output with any model change.
To bump the tracked razorpay-node release:
git clone --depth 1 https://github.com/razorpay/razorpay-node.git /tmp/rzp-node
node tool/extract_node_endpoints.js /tmp/rzp-node
python3 tool/check_parity.py # shows what changed upstream
License #
MIT. See LICENSE.