Errors
Error shapes that apply across the API, the auth failures every endpoint can return, and the per-endpoint 400s in one place.
Trailing slash required
Every route ends in / (e.g. /api/v1/checkout-sessions/).
- A
GETwithout the slash 301-redirects to the slashed URL. - A
POST/ write without the slash fails — the redirect can't preserve the request body.
Always include the trailing slash.
Error format
Field-specific validation errors return an object keyed by field name, with an array (or string) message:
{
"field_name": ["Error message."]
}General errors (auth failures, not found) return a detail key:
{
"detail": "Error message."
}A few endpoints that take no request body answer with a bare list instead — there is no field to key the message on. Cancel and resume are the two.
Authentication and KYC
Returned by every endpoint. See Authenticate your requests.
| Status | Trigger | Body |
|---|---|---|
401 | Key missing, invalid, expired, or the account behind it deactivated | {"detail": "Invalid API key."} |
403 | Owner not KYC-verified | {"status_code": "KYC_INCOMPLETE" | "KYC_VERIFICATION_PENDING", "message": "KYC verification needed to perform this action."} |
403 | Key not usable for this account | {"detail": "You do not have permission to perform this action."} |
The 401 detail distinguishes the cases: Authentication credentials were not provided. (no Authorization header, or one that does not start with Bearer ), Invalid API key. (no key matches), User inactive or deleted. (the account behind the key is deactivated — checked before expiry, so a valid key on a dead account says so), and API key has expired.
The two 403s are different shapes, so branch on the presence of detail, not on the status code. The KYC body has status_code and message and no detail; the other has detail alone. KYC is the one a new partner meets, and it is cleared in the dashboard rather than here.
GET /api/v1/products/ and every /webhooks/ route are the exceptions to the KYC 403: they work before KYC verification, so only the second shape occurs on them.
KYC is not enforced on the sandbox. An unverified account can exercise every endpoint there — see Test against the sandbox.
Rate limits
Only Create a checkout session is rate-limited. Every other endpoint is unlimited.
| Status | Trigger | Body |
|---|---|---|
429 | More than 20 sessions a minute on this account | {"detail": "Request was throttled. Expected available in 41 seconds."} |
The wait is named inside the detail string, not in a separate field. Retry after it.
Creating a checkout session
From Create a checkout session.
| Status | Trigger | Body |
|---|---|---|
400 | return_url missing, null or blank | {"return_url": ["A return URL is required."]} |
400 | items empty, or more than ten | Keyed on items |
400 | An item's pbp_id unknown / not visible | Keyed on items |
400 | customer_id not a customer on your account | Keyed on customer_id |
Two items resolving to the same product is not an error — the last one is kept and the other is dropped silently. See Items.
Adding or updating a customer
From Customers.
| Status | Trigger | Body |
|---|---|---|
400 | Phone is not a valid Nepali mobile number | Keyed on phone |
400 | Phone sent at a value other than its current one | {"phone": ["Phone cannot be changed."]} |
400 | Adding your own number as a customer | {"phone": ["You cannot add yourself as a customer."]} |
A phone you already hold is not an error: the customer is corrected and the response is 200 instead of 201.
Creating a subscription
From Create a subscription.
| Status | Trigger | Body |
|---|---|---|
400 | pbp_id unknown / not visible for this seller | {"pbp_id": "PlanBillingPeriod with public_id '...' does not exist or is not visible."} |
400 | Account has no API usage type | {"api_usage_type": "Your account does not have an API usage type configured."} |
400 | return_url missing | {"return_url": "A return URL is required for reminder-and-payment accounts."} |
400 | Client already holds a live subscription on another billing period of the same product | ["You already have a subscription to this product."] — a bare list, not a keyed object |
Managing a subscription
From Cancel or reschedule.
| Status | Trigger | Body |
|---|---|---|
400 | Date in the past | {"next_billing_cycle": ["next_billing_cycle must be in the future or today."]} |
400 | Subscription not found / not owned | {"subscription_id": "Subscription '...' does not exist."} |
400 | One-time purchase — nothing to cancel, resume or reschedule | ["This is a one-time purchase, so there is nothing to cancel."] |