Errors
Error handling, error codes, and error classes
All errors follow a consistent shape with UPPER_SNAKE_CASE error codes. The SDK exports constants and a helper so you never need to string-match:
1 import { ErrorCode, isGwopError, ErrorResponse } from "@gwop/sdk/errors"; 2 3 try { 4 await gwop.invoices.create({ body: { amountUsdc: -1 } }); 5 } catch (err) { 6 // Match on error code — access statusCode, headers, body 7 if (isGwopError(err, ErrorCode.ValidationError)) { 8 console.log(err.statusCode); // 400 9 } 10 11 // Access structured error fields — use instanceof ErrorResponse 12 if (err instanceof ErrorResponse) { 13 console.log(err.error.code); // "VALIDATION_ERROR" 14 console.log(err.error.message); // "amount_usdc must be at least 1 (0.000001 USDC)" 15 } 16 }
Build your error handling against codes, not HTTP status codes or message strings. Codes are stable across SDK versions.
| Code | Constant | Status | Meaning | Recommended Action |
|---|---|---|---|---|
UNAUTHORIZED | ErrorCode.Unauthorized | 401 | Invalid, revoked, or missing API key | Check GWOP_MERCHANT_API_KEY. Do not retry |
FORBIDDEN | ErrorCode.Forbidden | 403 | Valid key but merchant account not active | Contact Gwop support. Do not retry |
VALIDATION_ERROR | ErrorCode.ValidationError | 400 | Request body failed validation | Fix request payload. Use instanceof ErrorResponse to access err.error.details |
INVOICE_NOT_FOUND | ErrorCode.InvoiceNotFound | 404 | Invoice doesn’t exist or not visible to this merchant | Verify invoice ID |
INVOICE_CANCEL_NOT_ALLOWED | ErrorCode.InvoiceCancelNotAllowed | 400 | Cannot cancel — invoice is not OPEN | Check invoice status before canceling |
AUTH_INTENT_NOT_SETTLED | ErrorCode.AuthIntentNotSettled | 402 | Agent hasn’t paid the auth challenge yet | Poll with backoff until paid or expired |
AUTH_INTENT_NOT_FOUND | ErrorCode.AuthIntentNotFound | 404 | Auth intent doesn’t exist | Verify intent ID or create a new one |
AUTH_INTENT_EXPIRED | ErrorCode.AuthIntentExpired | 409 | Auth intent TTL exceeded | Create a new auth intent |
AUTH_INTENT_USED | ErrorCode.AuthIntentUsed | 409 | Auth intent already exchanged for a JWT | Use the JWT from the first exchange |
SESSION_NOT_FOUND | ErrorCode.SessionNotFound | 404 | Session doesn’t exist | Session may have been revoked or expired |
IDEMPOTENCY_CONFLICT | ErrorCode.IdempotencyConflict | 409 | Idempotency key reused with different parameters | Use a fresh crypto.randomUUID() |
RATE_LIMITED | ErrorCode.RateLimited | 429 | Too many requests | Back off. Check Retry-After header |
The SDK provides two ways to handle errors:
1 import { ErrorCode, isGwopError, ErrorResponse } from "@gwop/sdk/errors"; 2 3 try { 4 await gwop.invoices.create({ body: { amountUsdc: 1_000_000 } }); 5 } catch (err) { 6 // Recommended — works across all SDK methods 7 if (isGwopError(err, ErrorCode.ValidationError)) { 8 err.statusCode; // 400 9 } 10 11 if (isGwopError(err, ErrorCode.RateLimited)) { 12 err.headers.get("retry-after"); // When to retry 13 } 14 15 // Manual access — when you need the full ErrorResponse type 16 if (err instanceof ErrorResponse) { 17 err.error.code; // UPPER_SNAKE_CASE error code 18 err.error.message; // Human-readable description 19 err.statusCode; // HTTP status code 20 } 21 22 // Any SDK HTTP error 23 if (isGwopError(err)) { 24 err.statusCode; // HTTP status code 25 err.headers; // Response headers 26 err.body; // Raw response body 27 } 28 }
1 import { ErrorCode, isGwopError } from "@gwop/sdk/errors"; 2 3 async function createInvoiceWithRetry(amount: number, maxRetries = 3) { 4 const idempotencyKey = crypto.randomUUID(); 5 6 for (let attempt = 0; attempt < maxRetries; attempt++) { 7 try { 8 return await gwop.invoices.create({ 9 idempotencyKey, 10 body: { amountUsdc: amount }, 11 }); 12 } catch (err) { 13 if (isGwopError(err, ErrorCode.RateLimited)) { 14 const retryAfter = err.headers.get("retry-after") ?? "60"; 15 await new Promise((r) => setTimeout(r, Number(retryAfter) * 1000)); 16 continue; 17 } 18 throw err; // Don't retry client errors 19 } 20 } 21 throw new Error("Max retries exceeded"); 22 }
1 import { ErrorCode, isGwopError } from "@gwop/sdk/errors"; 2 3 try { 4 await gwop.authIntents.exchange({ authIntentId, idempotencyKey: crypto.randomUUID() }); 5 } catch (err) { 6 if (isGwopError(err, ErrorCode.AuthIntentNotSettled)) { 7 // Agent hasn't paid yet — poll and retry 8 } else if (isGwopError(err, ErrorCode.AuthIntentNotFound)) { 9 // Invalid intent ID — create a new one 10 } else if (isGwopError(err, ErrorCode.AuthIntentExpired)) { 11 // TTL exceeded — create a new auth intent 12 } else { 13 throw err; 14 } 15 }