DodoPayments Schemas - TundraSoft/tundra-connect GitHub Wiki
DodoPayments Schemas
Public Guardian schemas, exported from
@tundraconnect/dodo-payments/schemas.
import {
CreatePaymentRequestSchemaObject,
PaymentSchemaObject,
SubscriptionSchemaObject,
} from '@tundraconnect/dodo-payments/schemas';
| Export | Used for |
|---|---|
CreatePaymentRequestSchemaObject |
POST /payments body. |
CreatePaymentResponseSchemaObject |
POST /payments result. |
PaymentSchemaObject |
GET /payments/{id} record. |
PaymentListItemSchemaObject |
One entry of GET /payments. |
PaymentListSchemaObject |
A GET /payments page. |
CreateRefundRequestSchemaObject |
POST /refunds body. |
RefundItemRequestSchemaObject |
One items entry of a refund request. |
RefundSchemaObject |
A refund record. |
RefundListItemSchemaObject |
One entry of GET /refunds. |
RefundListSchemaObject |
A GET /refunds page. |
RefundStatusSchemaObject |
Refund status enum. |
CreateSubscriptionRequestSchemaObject |
POST /subscriptions body. |
CreateSubscriptionResponseSchemaObject |
POST /subscriptions result. |
SubscriptionSchemaObject |
A subscription record. |
SubscriptionListSchemaObject |
A GET /subscriptions page. |
ProductCartItemSchemaObject |
One product line. |
BillingAddressSchemaObject |
Billing address on a record. |
BillingAddressRequestSchemaObject |
Billing address in a request (strict). |
CustomerRequestSchemaObject |
The customer field of a create request. |
CustomerDetailsSchemaObject |
The embedded customer summary. |
CustomerSchemaObject |
A full customer record (getCustomer). |
CustomerPortalSessionSchemaObject |
createCustomerPortalSession result. |
ChangePlanRequestSchemaObject |
POST /subscriptions/{id}/change-plan body. |
ChangePlanResponseSchemaObject |
changePlan result. |
ProrationBillingModeSchemaObject |
Plan-change billing mode enum. |
CreateProductRequestSchemaObject |
POST /products body. |
UpdateProductRequestSchemaObject |
PATCH /products/{id} body. |
ProductSchemaObject |
A product record. |
ProductListItemSchemaObject |
One entry of GET /products. |
ProductListSchemaObject |
A GET /products page. |
ProductPriceRequestSchemaObject |
The price of a product being created. |
PriceSchemaObject |
Any price Dodo returns. |
OneTimePriceSchemaObject |
A one-time price. |
RecurringPriceSchemaObject |
A recurring price. |
UsageBasedPriceSchemaObject |
A usage-based price (read-only). |
ProductMetadataSchemaObject |
Product metadata. |
CreateDiscountRequestSchemaObject |
POST /discounts body. |
UpdateDiscountRequestSchemaObject |
PATCH /discounts/{id} body. |
DiscountCurrencyOptionRequestSchemaObject |
One currency_options entry of a request. |
DiscountSchemaObject |
A discount record. |
DiscountCurrencyOptionSchemaObject |
One currency_options entry of a record. |
DiscountListSchemaObject |
A GET /discounts page. |
DiscountCustomerSchemaObject |
One allow-list entry. |
DiscountCustomerListSchemaObject |
An allow-list page. |
DiscountTypeSchemaObject |
Discount type enum. |
DiscountCustomerEligibilitySchemaObject |
Customer eligibility enum. |
SubscriptionDiscountSchemaObject |
A discount applied to a subscription. |
TaxCategorySchemaObject |
Product tax category enum. |
IntentStatusSchemaObject |
Payment status enum. |
SubscriptionStatusSchemaObject |
Subscription status enum. |
TimeIntervalSchemaObject |
Billing recurrence unit. |
ErrorResponseSchemaObject |
The { code, message } failure envelope. |
Constants: INTENT_STATUSES, SUBSCRIPTION_STATUSES, TIME_INTERVALS,
TAX_CATEGORIES, PRORATION_BILLING_MODES, PLAN_CHANGE_EFFECTIVE_AT,
PLAN_CHANGE_ON_PAYMENT_FAILURE, DISCOUNT_TYPES,
DISCOUNT_CUSTOMER_ELIGIBILITIES, REFUND_STATUSES,
REFUND_NETWORK_REFERENCE_TYPES.
Prices
PriceSchema is a union discriminated on type, so checking type
narrows it:
import { PriceSchemaObject } from '@tundraconnect/dodo-payments/schemas';
const [, price] = PriceSchemaObject.safeParse({
type: 'recurring_price',
price: 1500,
currency: 'USD',
payment_frequency_count: 1,
payment_frequency_interval: 'Month',
subscription_period_count: 20,
subscription_period_interval: 'Year',
});
if (price?.type === 'recurring_price') {
console.log(price.payment_frequency_interval); // 'Month'
}
Amounts are integers in the currency's smallest unit, and currency is an
uppercase ISO 4217 code. A request price may be one-time or recurring; a
response may also be usage-based, whose meter fields pass through untyped.
Discounts
The discount request schemas are strict: an unknown field is an error, not a dropped key, and numbers and booleans are not coerced from strings. They also check Dodo's cross-field rules, so a code that could never be created fails before it is sent.
import { CreateDiscountRequestSchemaObject } from '@tundraconnect/dodo-payments/schemas';
const [error] = CreateDiscountRequestSchemaObject.safeParse({
type: 'percentage',
amount: 2000,
usageLimit: 5, // typo
});
error?.message; // "Unknown property 'usageLimit' is not allowed in strict mode"
On a discount record, type and customer_eligibility are typed as
string, so a legacy flat_per_unit discount or a new eligibility value
still reads. Discount metadata has the same shape as product metadata.
SubscriptionDiscountSchemaObject reads both shapes Dodo uses for a
subscription's discounts: the full one from getSubscription and the
short one from listSubscriptions. Only discount_id is required.
Refunds
CreateRefundRequestSchemaObject is strict, like the discount
requests: an unknown field is an error, and numbers and booleans are not
coerced. An items list must name at least one line, each once, with a
positive integer amount when it has one.
On a refund record, status is the four-value RefundStatusSchema;
network_reference_type is typed as string, so a new kind still reads.
metadata is optional on the record although Dodo documents it as always
present, because refunds only gained metadata in November 2025.
Product metadata
Product metadata values are strings, numbers or booleans, and keep their
type through validation: 5 stays a number and '5' stays a string. This
differs from subscription and customer metadata, which Dodo returns as
strings. tax_category on a product read is typed as string, so a
category Dodo adds later does not fail the read.
Status enums
IntentStatusSchema has eleven values and only succeeded means money
moved. processing and the requires_* family are explicitly not
success — see API.
TimeIntervalSchema is case-sensitive and capitalized: Day, Week,
Month, Year. 'month' is rejected.
CustomerSchema vs CustomerDetailsSchema
Two different customer shapes, and it matters which you are holding:
CustomerDetailsSchemais the summary embedded in a payment or subscription —customer_id,email,name, optional phone/metadata.CustomerSchemais the full record fromgetCustomer, addingbusiness_id,created_atand the blocklist fields.
blocked_at is resolved only by the single-customer route; the vendor
leaves it empty on list responses, so absent means "not reported", not
"not blocked".
CustomerRequestSchema
A union, matching the vendor: either { customer_id } or
{ email, name?, phone_number? }. Passing neither is rejected locally
rather than producing an opaque 422.
Request schemas are strict
Every request schema rejects an unknown field instead of dropping it, and
refuses a number or boolean sent as a string (quantity: '1',
payment_link: 'true'). This covers payments, subscriptions, plan
changes, products, discounts and refunds, and their nested objects (cart
lines, customer, billing address, prices, addons). A typo like
discountCodes would otherwise be dropped silently and the request sent
without it — a full-price subscription with no error.
import { CreateSubscriptionRequestSchemaObject } from '@tundraconnect/dodo-payments/schemas';
const [error] = CreateSubscriptionRequestSchemaObject.safeParse({
product_id: 'prd_monthly',
quantity: 1,
customer: { customer_id: 'cus_1' },
billing: { country: 'US' },
discountCodes: ['WELCOME20'], // typo for discount_codes
});
error?.message; // "Unknown property 'discountCodes' is not allowed in strict mode"
The customer union is strict too, so { customer_id, email } matches
neither form and is rejected: pass the id alone for an existing customer.
Response schemas pass unknown fields through
PaymentSchema and SubscriptionSchema both use .passthrough(). Dodo's
payment object carries settlement, dispute, refund and discount families,
and its subscription object carries addons, meters and credit
entitlements — all outside this connect's scope and all actively growing.
A closed shape would turn any additive vendor change into a
RESPONSE_ERROR on a status check for a payment that already succeeded —
the worst possible moment to start failing.
status on a payment is optional and nullable, because the vendor
types it that way. Treat a missing status as unknown, never as success.
List pages normalize defensively
PaymentListSchema and SubscriptionListSchema wrap
{ items: [...] }, with two normalizations applied in a single top-level
Guardian.preprocess:
- An absent
itemsbecomes[]— "this customer has no payments" is an ordinary answer, not a failure. - A bare array body is treated as the items themselves, so results are never silently dropped in favour of an empty page if the vendor returns a naked array.
A scalar body is still rejected.
The normalization is one top-level
preprocesswrapping the whole object, never a per-field one: aGuardian.preprocessused as aGuardian.object()field's value silently skips its transform. SeeCONVENTIONS.md.
Billing
billing.country is required on every create call — Dodo is a merchant of
record and derives tax treatment from it. The rest of the address is
optional and accepts explicit nulls, which is how the vendor returns
unset fields.