Error Codes
Complete reference of Bag API error codes with HTTP statuses, causes, and how to fix them.
Error Codes
Every error response from the Bag API follows the same shape:
{
"status": "error",
"message": "Human-readable description of what went wrong",
"code": "MACHINE_READABLE_CODE"
}The message field is safe to display to end users. The code field is stable and safe to match against in your code. Some errors include an additional hint field with specific guidance.
Authentication errors
UNAUTHORIZED
| HTTP status | 401 |
| When it happens | Missing, malformed, expired, or revoked API key |
{
"status": "error",
"message": "Invalid or revoked API key",
"code": "UNAUTHORIZED",
"hint": "Include a valid API key in the Authorization header: Bearer bag_live_sk_..."
}Common causes and fixes:
- Missing
Authorizationheader — AddAuthorization: Bearer bag_test_sk_...to your request headers. - Malformed key — Keys must start with
bag_live_sk_orbag_test_sk_. Check for trailing whitespace or truncation. - Key was revoked — Generate a new key from the Bag Dashboard under Developer Settings.
- Expired key — If your key has an expiration date, create a new one.
- Too many failed attempts — After 10 failed auth attempts from the same IP within 5 minutes, further attempts are blocked. Wait and retry.
FORBIDDEN
| HTTP status | 403 |
| When it happens | Valid credentials but insufficient permissions for the requested action |
{
"status": "error",
"message": "Cannot delete files outside your directory",
"code": "FORBIDDEN"
}Common causes and fixes:
- Accessing another merchant's resources — You can only access resources belonging to your merchant account.
- Deleting files outside your directory — The upload delete endpoint restricts deletions to your own merchant directory.
- Using a test key for live-only operations — Some operations require a live API key.
Validation errors
BAD_REQUEST
| HTTP status | 400 |
| When it happens | Request body is malformed, missing required fields, or contains invalid values |
{
"status": "error",
"message": "amount: Amount must be a positive number up to 999,999,999.99; network: Invalid enum value",
"code": "BAD_REQUEST"
}The message field contains semicolon-separated validation errors in the format field: reason. When using the SDK, these are parsed into structured field errors.
Common causes and fixes:
- Missing required fields — Check the API reference for which fields are required. For example,
POST /api/payment-linksrequiresname,amount, andnetwork. - Invalid enum value — Fields like
networkandtokenonly accept specific values. See the endpoint documentation for valid options. - Amount out of range — Amounts must be between
0.01and999,999,999.99. - Invalid wallet address — Wallet addresses must be 26–128 alphanumeric characters.
- Invalid transaction hash — Transaction hashes must be 32–128 hex characters, optionally prefixed with
0x. - Invalid URL — Webhook URLs must be valid HTTPS URLs under 2048 characters.
- Invalid email — Email addresses must be valid and under 320 characters.
- Key-network mismatch — Test keys (
bag_test_sk_*) can only be used with testnet networks (base_sepolia,eth_sepolia,solana_devnet). Live keys (bag_live_sk_*) can only be used with mainnet networks (base,ethereum,polygon,solana). - Mode mismatch — A transaction's payment link must be in the same mode as the API key being used. You cannot record a transaction against a live payment link using a test key, or vice versa.
- Testnet settlement — Settlements are not available for testnet networks. Only live-mode transactions can be settled.
Payment errors
CONFLICT
| HTTP status | 409 |
| When it happens | Duplicate resource or state conflict |
{
"status": "error",
"message": "Transaction already recorded",
"code": "CONFLICT"
}Common causes and fixes:
- Duplicate transaction hash — The
txHashyou submitted is already associated with another transaction. Each on-chain transaction can only be recorded once. If you're retrying after a timeout, checkGET /api/transactionsfirst to see if the original request succeeded. - Duplicate KYB submission — You can only submit one KYB application. Use
PATCH /api/kybto update an existing application. - Duplicate checkout submission — The session already has a transaction hash attached. Check the session status with
GET /api/checkout/session/{id}.
NOT_FOUND
| HTTP status | 404 |
| When it happens | The requested resource does not exist or has been deleted |
{
"status": "error",
"message": "Not found",
"code": "NOT_FOUND"
}Common causes and fixes:
- Invalid ID — Double-check the resource ID in your request path or query parameter. IDs are UUIDs (e.g.,
d4e5f6a7-b8c9-4d0e-a1f2-b3c4d5e6f7a8). - Resource was deleted — Payment links, webhook endpoints, and API keys can be permanently deleted.
- Expired checkout session — Sessions expire 30 minutes after creation. Create a new session.
- Wrong environment — Resources created with a test key are not visible with a live key, and vice versa.
Rate limiting
RATE_LIMITED
| HTTP status | 429 |
| When it happens | Too many requests in the current time window |
{
"status": "error",
"message": "Too many requests. Please slow down.",
"retryAfter": 10
}The retryAfter field indicates how many seconds to wait before retrying. The response also includes rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp (ms) when the window resets |
How to fix: Implement exponential backoff with jitter. See the Rate Limits page for detailed retry guidance and code examples.
Server errors
INTERNAL_ERROR
| HTTP status | 500 |
| When it happens | Unexpected server-side failure |
{
"status": "error",
"message": "Internal server error",
"code": "INTERNAL_ERROR"
}How to fix: This is a Bag-side issue. Retry the request after a short delay (1–2 seconds). If the error persists, check the Bag status page or contact support.
TIMEOUT
| HTTP status | 504 |
| When it happens | An upstream service (e.g., tax calculation) did not respond in time |
{
"status": "error",
"message": "Tax calculation timed out. Please retry.",
"code": "TIMEOUT"
}How to fix: Retry the request after 2–5 seconds. Timeouts are typically transient. If the tax quote endpoint consistently times out, the Numeral tax service may be experiencing issues.
Handling errors in code
import { Bag, BagError } from "@getbagsapp/sdk";
const bag = new Bag({ apiKey: process.env.BAG_API_KEY! });
try {
const link = await bag.paymentLinks.create({
name: "Pro Plan",
amount: 29.99,
network: "base",
});
} catch (err) {
if (err instanceof BagError) {
switch (err.code) {
case "UNAUTHORIZED":
console.error("Check your API key");
break;
case "BAD_REQUEST":
console.error("Validation failed:", err.message);
break;
case "RATE_LIMITED":
const retryAfter = err.retryAfter ?? 10;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
break;
default:
console.error("API error:", err.code, err.message);
}
}
}from bag import Bag, BagError
import time
bag = Bag(api_key=os.environ["BAG_API_KEY"])
try:
link = bag.payment_links.create(
name="Pro Plan",
amount=29.99,
network="base",
)
except BagError as e:
if e.code == "UNAUTHORIZED":
print("Check your API key")
elif e.code == "BAD_REQUEST":
print(f"Validation failed: {e.message}")
elif e.code == "RATE_LIMITED":
retry_after = getattr(e, "retry_after", 10)
time.sleep(retry_after)
else:
print(f"API error: {e.code} - {e.message}")Error code summary
| Code | HTTP Status | Description |
|---|---|---|
BAD_REQUEST | 400 | Invalid request body or parameters |
UNAUTHORIZED | 401 | Missing or invalid authentication |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource does not exist |
CONFLICT | 409 | Duplicate resource or state conflict |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Unexpected server error |
TIMEOUT | 504 | Upstream service timeout |