API Reference
Solvd resolves fintech disputes with sub-second AI decisions. The REST API lets you submit disputes programmatically, query resolutions, and receive real-time events via webhooks.
Base URL: https://solvd-3.polsia.app
Get resolving in 3 steps
Generate an API key
Head to solvd-3.polsia.app/api-keys and create your first key. Copy it — it's shown only once. The key has the format sk_live_...
Submit your first dispute
Send a POST /api/disputes with your transaction details. By default, Solvd auto-resolves it instantly and returns the AI decision in the same response.
Receive live resolution events
Register a webhook endpoint with POST /api/webhooks to receive dispute_resolved events in real time. Use the X-Solvd-Signature header to verify authenticity.
# 1. Submit a dispute and get an instant AI resolution curl -X POST https://solvd-3.polsia.app/api/disputes \ -H "X-API-Key: sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "transaction_id": "txn_9f2a1b3c", "dispute_type": "unauthorized_charge", "customer_name": "Sarah Chen", "customer_email": "sarah@example.com", "amount": 149.99, "currency": "USD", "description": "I did not make this purchase" }'
import requests response = requests.post( "https://solvd-3.polsia.app/api/disputes", headers={ "X-API-Key": "sk_live_your_key_here", "Content-Type": "application/json", }, json={ "transaction_id": "txn_9f2a1b3c", "dispute_type": "unauthorized_charge", "customer_name": "Sarah Chen", "customer_email": "sarah@example.com", "amount": 149.99, "currency": "USD", "description": "I did not make this purchase", } ) dispute = response.json() print(dispute["dispute"]["resolution"]["decision"])
const response = await fetch("https://solvd-3.polsia.app/api/disputes", { method: "POST", headers: { "X-API-Key": "sk_live_your_key_here", "Content-Type": "application/json", }, body: JSON.stringify({ transaction_id: "txn_9f2a1b3c", dispute_type: "unauthorized_charge", customer_name: "Sarah Chen", customer_email: "sarah@example.com", amount: 149.99, currency: "USD", description: "I did not make this purchase", }), }); const { dispute } = await response.json(); console.log(dispute.resolution.decision);
Authentication
All API requests require an API key. Keys are generated at solvd-3.polsia.app/api-keys. The raw key is shown once at creation — store it securely. Solvd stores only the SHA-256 hash.
/api/* routes.Supported auth methods
curl https://solvd-3.polsia.app/api/disputes \ -H "X-API-Key: sk_live_abc123..."
curl https://solvd-3.polsia.app/api/disputes \ -H "Authorization: Bearer sk_live_abc123..."
Rate Limits
Each API key has a configurable rate limit (default: 100 requests/minute). Limits use a sliding window. Rate limit status is returned in response headers on every request.
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per minute for this key |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
Seconds until the window resets |
When the limit is exceeded, the API returns 429 Too Many Requests with a retry_after_seconds field in the response body.
Submit a Dispute
Creates a new dispute. By default (auto_resolve: true), Solvd immediately classifies and resolves it with an AI decision — median latency under 1 second.
High-value disputes (amount > $5,000) are automatically escalated to a human reviewer.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction_id | string | required | Your internal transaction identifier. Used for deduplication lookups. |
| dispute_type | string | optional | Type of dispute. See Dispute Types for valid values. The AI will also re-classify regardless of the value provided. Default: other |
| customer_name | string | optional | Full name of the disputing customer. |
| customer_email | string | optional | Customer email address. |
| customer_id | string | optional | Your internal customer identifier. |
| amount | number | optional | Disputed amount. Disputes exceeding $5,000 are automatically escalated. |
| currency | string | optional | 3-letter ISO 4217 currency code. Default: USD |
| merchant_name | string | optional | Merchant name associated with the transaction. |
| transaction_date | string | optional | ISO 8601 date of the original transaction (e.g. 2025-01-15T14:30:00Z). |
| description | string | optional | Customer's description of the dispute. More detail leads to higher confidence scores. |
| evidence | object | optional | Structured evidence object. Free-form key/value pairs (e.g. receipt URLs, merchant responses). |
| metadata | object | optional | Arbitrary metadata passed through and stored with the dispute. Useful for your internal IDs or tags. |
| external_id | string | optional | Your external reference ID for this dispute (e.g. from your CRM or ticketing system). |
| priority | string | optional | Priority tier: low, medium, high.Default: medium |
| auto_resolve | boolean | optional | Set false to submit without triggering AI resolution (useful for staging ingestion).Default: true |
{
"success": true,
"dispute": {
"id": 42,
"transaction_id": "txn_9f2a1b3c",
"dispute_type": "unauthorized_charge",
"status": "resolved",
"priority": "medium",
"customer_name": "Sarah Chen",
"customer_email": "sarah@example.com",
"amount": "149.99",
"currency": "USD",
"created_at": "2025-04-15T19:00:00.000Z",
"resolution": {
"decision": "approve_refund",
"classified_type": "unauthorized_charge",
"confidence_score": 0.94,
"decision_reasoning": "Transaction flagged as unauthorized...",
"customer_explanation": "We've reviewed your dispute and approved a full refund...",
"refund_amount": "149.99",
"processing_time_ms": 412
}
}
}
Returns a paginated list of disputes, optionally filtered by status or type. Default sort is newest first.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status: pending, resolved, denied, escalated, pending_info |
| type | string | Filter by dispute type. See Dispute Types for valid values. |
| limit | integer | Number of results to return. Max 200. Default: 50 |
| offset | integer | Number of results to skip for pagination. Default: 0 |
| sort | string | Sort field: created_at, amount, status, dispute_typeDefault: created_at |
| order | string | Sort direction: asc or descDefault: desc |
curl "https://solvd-3.polsia.app/api/disputes?status=resolved&limit=20&offset=0" \ -H "X-API-Key: sk_live_your_key"
import requests res = requests.get( "https://solvd-3.polsia.app/api/disputes", headers={"X-API-Key": "sk_live_your_key"}, params={"status": "resolved", "limit": 20} ) data = res.json() print(f"Total: {data['total']}, returned: {len(data['disputes'])}")
const params = new URLSearchParams({ status: "resolved", limit: 20 }); const res = await fetch(`https://solvd-3.polsia.app/api/disputes?${params}`, { headers: { "X-API-Key": "sk_live_your_key" } }); const { disputes, total } = await res.json();
{
"success": true,
"disputes": [ /* array of dispute objects */ ],
"total": 143,
"limit": 20,
"offset": 0
}
Returns a single dispute with its complete resolution history, including AI decision reasoning, confidence scores, and customer-facing explanation.
curl https://solvd-3.polsia.app/api/disputes/42 \ -H "X-API-Key: sk_live_your_key"
res = requests.get( "https://solvd-3.polsia.app/api/disputes/42", headers={"X-API-Key": "sk_live_your_key"} ) dispute = res.json()["dispute"]
const res = await fetch("https://solvd-3.polsia.app/api/disputes/42", { headers: { "X-API-Key": "sk_live_your_key" } }); const { dispute } = await res.json();
{
"success": true,
"dispute": {
"id": 42,
"transaction_id": "txn_9f2a1b3c",
"status": "resolved",
"amount": "149.99",
"created_at": "2025-04-15T19:00:00.000Z",
"resolutions": [
{
"id": 7,
"decision": "approve_refund",
"classified_type": "unauthorized_charge",
"confidence_score": 0.94,
"decision_reasoning": "Transaction occurred outside customer's typical usage pattern...",
"customer_explanation": "We've reviewed and approved a full refund of $149.99...",
"refund_amount": "149.99",
"processing_time_ms": 412,
"created_at": "2025-04-15T19:00:00.412Z"
}
]
}
}
Import up to 500 disputes in one request from a CSV file. Each row is validated individually, inserted into the database, and then asynchronously resolved by the AI engine. The API returns immediately with a summary of imported vs. failed rows.
multipart/form-data. The file field must be named file. Max file size: 10 MB. Max rows: 500.CSV Format
| Column | Required | Notes |
|---|---|---|
| transaction_id | required | Unique transaction identifier |
| dispute_type | optional | Must be a valid dispute type enum value |
| customer_name | optional | |
| customer_email | optional | Must be valid email format if provided |
| amount | optional | Numeric value (e.g. 149.99) |
| currency | optional | ISO 4217 code, defaults to USD |
| transaction_date | optional | ISO 8601 format |
| description | optional | Dispute description |
| priority | optional | low, medium, or high |
# CSV example (disputes.csv): # transaction_id,dispute_type,customer_name,amount # txn_001,unauthorized_charge,Alice Wong,89.00 # txn_002,duplicate_charge,Bob Kim,55.50 curl -X POST https://solvd-3.polsia.app/api/disputes/import \ -H "X-API-Key: sk_live_your_key" \ -F "file=@disputes.csv"
import requests with open("disputes.csv", "rb") as f: res = requests.post( "https://solvd-3.polsia.app/api/disputes/import", headers={"X-API-Key": "sk_live_your_key"}, files={"file": ("disputes.csv", f, "text/csv")} ) data = res.json() print(f"Imported {data['imported']}/{data['total']} disputes")
const fs = require("fs"); const FormData = require("form-data"); const form = new FormData(); form.append("file", fs.createReadStream("disputes.csv")); const res = await fetch("https://solvd-3.polsia.app/api/disputes/import", { method: "POST", headers: { "X-API-Key": "sk_live_your_key", ...form.getHeaders() }, body: form, }); const { imported, total, errors } = await res.json();
{
"success": true,
"total": 50,
"imported": 48,
"failed": 2,
"errors": [
{ "row": 12, "field": "customer_email", "message": "Invalid email format" },
{ "row": 37, "field": "amount", "message": "Amount must be numeric" }
]
}
Webhooks
Webhooks push dispute events to your server in real time. Solvd signs every payload with HMAC-SHA256 — verify the X-Solvd-Signature header to confirm authenticity.
Failed deliveries are retried up to 3 times with exponential backoff (5s → 20s → 80s).
Creates a new webhook. The secret is returned only once at creation — store it securely.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | required | HTTPS endpoint URL to receive events. Must be a valid URL. |
| events | string[] | required | Array of event types to subscribe to. Must contain at least one event. See Event Reference. |
| description | string | optional | Human-readable description for this webhook (e.g. "Production alerts endpoint"). |
curl -X POST https://solvd-3.polsia.app/api/webhooks \ -H "X-API-Key: sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/webhooks/solvd", "description": "Production dispute events", "events": ["dispute_resolved", "dispute_escalated"] }'
res = requests.post( "https://solvd-3.polsia.app/api/webhooks", headers={"X-API-Key": "sk_live_your_key"}, json={ "url": "https://yourapp.com/webhooks/solvd", "description": "Production dispute events", "events": ["dispute_resolved", "dispute_escalated"] } ) webhook = res.json()["webhook"] print("Store this secret:", webhook["secret"])
const res = await fetch("https://solvd-3.polsia.app/api/webhooks", { method: "POST", headers: { "X-API-Key": "sk_live_your_key", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://yourapp.com/webhooks/solvd", description: "Production dispute events", events: ["dispute_resolved", "dispute_escalated"], }), }); const { webhook } = await res.json(); console.log("Store this secret:", webhook.secret);
{
"success": true,
"webhook": {
"id": 3,
"url": "https://yourapp.com/webhooks/solvd",
"description": "Production dispute events",
"events": ["dispute_resolved", "dispute_escalated"],
"is_active": true,
"secret": "a3f8c2e1d9b7...", // returned ONCE — store it
"created_at": "2025-04-15T19:00:00.000Z"
}
}
Returns all registered webhooks with delivery statistics (total, successful, failed deliveries).
curl https://solvd-3.polsia.app/api/webhooks \ -H "X-API-Key: sk_live_your_key"
Returns configuration for a specific webhook. The secret is not returned after creation.
curl https://solvd-3.polsia.app/api/webhooks/3 \ -H "X-API-Key: sk_live_your_key"
Update webhook URL, subscribed events, description, or active status.
Request Body (all optional)
| Parameter | Type | Description |
|---|---|---|
| url | string | New endpoint URL |
| events | string[] | Updated event subscriptions |
| description | string | Updated description |
| is_active | boolean | Set to false to pause deliveries without deleting |
Permanently removes a webhook and all its delivery history. No request body needed.
curl -X DELETE https://solvd-3.polsia.app/api/webhooks/3 \ -H "X-API-Key: sk_live_your_key"
Event Reference
| Event | When it fires |
|---|---|
dispute_created |
A new dispute has been submitted via API or dashboard |
dispute_resolved |
The AI engine has issued a decision (approve_refund, partial_refund, deny_claim, or request_more_info) |
dispute_escalated |
Dispute exceeded $5,000 threshold or requires human review |
Webhook payloads contain the full dispute object including resolution details when available.
Verifying Signatures
Every webhook delivery includes an X-Solvd-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.
Always verify this before processing events.
import hmac, hashlib def verify_signature(payload_bytes, secret, signature): expected = hmac.new( secret.encode(), payload_bytes, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) # In your Flask/FastAPI handler: raw_body = request.get_data() sig = request.headers.get("X-Solvd-Signature") if not verify_signature(raw_body, WEBHOOK_SECRET, sig): abort(403, "Invalid signature")
const crypto = require("crypto"); function verifySignature(rawBody, secret, signature) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } // In your Express handler: app.post("/webhooks/solvd", express.raw({ type: "application/json" }), (req, res) => { const sig = req.headers["x-solvd-signature"]; if (!verifySignature(req.body, process.env.WEBHOOK_SECRET, sig)) { return res.status(403).send("Invalid signature"); } const event = JSON.parse(req.body); // handle event... res.status(200).send("OK"); });
Error Codes
All error responses follow the same format: { "success": false, "message": "..." }
| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Missing required field (transaction_id), invalid enum value, malformed JSON, invalid URL format |
| 401 | Unauthorized | Missing or invalid API key. Include X-API-Key header with a valid active key. |
| 403 | Forbidden | API key is revoked. Generate a new key at /api-keys. |
| 404 | Not Found | Dispute or webhook with the given ID does not exist. |
| 429 | Rate Limited | Exceeded your key's rate limit. Check X-RateLimit-Reset header and retry after that many seconds. |
| 500 | Server Error | Unexpected internal error. These are automatically logged. Contact support if recurring. |
Dispute Types
Valid values for the dispute_type field. The AI engine re-classifies regardless — this is your hint.
Decision Types
Possible values for resolution.decision in any dispute response.
| Decision | Description |
|---|---|
approve_refund | Full refund granted. Clear evidence of unauthorized or erroneous transaction. |
partial_refund | Partial refund granted. Some merit to the claim, mitigating factors considered. |
deny_claim | Claim denied. Transaction appears legitimate or insufficient evidence provided. |
escalate_to_human | Dispute routed to human reviewer — high value (>$5K), legal risk, or account takeover suspected. |
request_more_info | Additional documentation needed from the customer before a decision can be made. |
Ready to integrate?
Start your free pilot — submit your first 100 disputes at no cost and see AI resolution in action.
Start your free pilot →