Checkout metadata
Create-time checkout context, validation boundaries, response fields, and immutable read surfaces.
Checkout metadata
Use checkoutMetadata to attach non-sensitive merchant context to a checkout when you create it. FloPay copies the map into later billing records so you can correlate API reads and webhook events with your own order or workflow.
checkoutMetadata is separate from every existing metadata field. Product, provider, transaction, subscription, and legacy checkout metadata keep their current meanings and shapes.
Checkout metadata is created once. There is no public operation to edit it after session creation.
DTO contract
| Surface | Field | Contract |
|---|---|---|
CreateCheckoutSessionBodyDto | checkoutMetadata | Optional request field: Record<string, string> | null. |
CheckoutSessionResponseDto | data.checkoutMetadata | Optional response field: Record<string, string>. It is inside data and is never null. |
CheckoutSessionRecordDto | checkoutMetadata | Optional response field on a stored checkout-session record: Record<string, string>. It is never null. |
PaymentDto | checkoutMetadata | Optional response field on the raw payment DTO: Record<string, string>. It is never null. |
UserSubscriptionDto | checkoutMetadata | Optional response field on the raw subscription DTO: Record<string, string>. It is never null. |
The request type is Record<string, string> | null and the field is optional. The response type is Record<string, string>; it is optional and never null.
The request field is nullable because explicit null has the same stored result as omission. Response and read DTOs are optional and non-null: when no snapshot exists, the key is absent.
Create a checkout session
POST /v1/checkouts/sessions accepts the optional request field on CreateCheckoutSessionBodyDto.
POST /v1/checkouts/sessions
Content-Type: application/json
Idempotency-Key: checkout-order-123{
"clientId": "00000000-0000-0000-0000-000000000000",
"currency": "USD",
"products": [{ "code": "starter", "quantity": 1 }],
"accountData": {
"userId": "buyer-123",
"email": "buyer@example.com"
},
"successUrl": "https://merchant.example/success",
"cancelUrl": "https://merchant.example/checkout",
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}A 201 Created response is a CheckoutSessionResponseDto. The snapshot is at data.checkoutMetadata, not at the response root:
{
"data": {
"uuid": "c01a59cf-a07d-4f2a-9afd-09630cd4ea94",
"nonce": "checkout-session-token-from-response",
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}
}Omission, null, and an empty map
| Create request | Stored and returned behavior |
|---|---|
| Field omitted | checkoutMetadata is absent from reads and webhooks. |
"checkoutMetadata": null | Same as omission. The response key is absent, not null. |
"checkoutMetadata": {} | The empty snapshot is preserved and returned as {}. |
| A populated map | The string map is copied and returned on relevant snapshots. |
Omitted input and null both leave the response field absent. An explicit {} remains {}.
Omitted and explicit null values also preserve the legacy checkout-create idempotency fingerprint. A populated map or {} participates in the fingerprint using canonical key ordering. This means an Idempotency-Key replay with the same pairs in a different object-key order is the same request, while a material metadata change is a different request and conflicts with the original key.
Validation
The backend is the runtime authority for validation. A value is valid only when all of these boundaries are satisfied:
| Boundary | Limit |
|---|---|
| Entries | At most 50 own string key-value pairs. |
| Key length | At most 40 Unicode code points per key. |
| Value length | At most 500 Unicode code points per value. |
| Serialized size | At most 8,192 UTF-8 bytes for the complete JSON.stringify representation, including braces, separators, quotes, and escaping. |
Values exactly at every limit are accepted. The map must be a plain, non-array object and every value must be a string. Empty keys and empty values are valid. U+0000 and unmatched UTF-16 surrogates are rejected in both keys and values.
An invalid shape or an exceeded boundary returns 400 Bad Request. The standard validation response includes the merged validator message:
{
"message": [
"checkoutMetadata must contain at most 50 string pairs; keys and values must not contain U+0000 or unmatched UTF-16 surrogates; keys may contain up to 40 Unicode characters, values up to 500 Unicode characters, and the serialized size may be up to 8192 bytes"
],
"error": "Bad Request",
"statusCode": 400
}SDK callers receive the same backend message through a FloPayError with type: "api_error", the HTTP status available as statusCode, and the backend error code when one is present. The SDK does not apply a second set of client-side metadata limits.
Read the snapshot
Checkout session
GET /v1/checkouts/sessions/{id} returns CheckoutSessionResponseDto, so read data.checkoutMetadata. The checkout-session token remains required for this checkout-page read.
Client-authenticated stored checkout-session reads use CheckoutSessionRecordDto. The list response contains records inside its existing pagination envelope, and the stored-record by-id response returns the raw DTO. In both cases, checkoutMetadata is an optional field on each record.
Payment
GET /v1/payments/{id} returns PaymentDto as the raw DTO, not wrapped in data:
{
"id": "3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0",
"status": "succeeded",
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}The existing payment metadata: Record<string, unknown> | null field is unrelated and remains unchanged.
Subscription
GET /v1/subscriptions/{id} returns UserSubscriptionDto as the raw DTO, not wrapped in data:
{
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"state": "active",
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}The existing subscription metadata: Record<string, unknown> | null field is unrelated and remains unchanged.