Getting Started

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

1

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_...

2

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.

3

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);
Security

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.

ℹ️
Bootstrap mode: If no API keys have been created yet, all endpoints are open. As soon as the first key is created, authentication is enforced on all /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.

HeaderDescription
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.

Disputes API

Submit a Dispute

POST /api/disputes Submit and auto-resolve 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

ParameterTypeRequiredDescription
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
200 OK Response
{
  "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
    }
  }
}
GET /api/disputes List all disputes

Returns a paginated list of disputes, optionally filtered by status or type. Default sort is newest first.

Query Parameters

ParameterTypeDescription
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_type
Default: created_at
order string Sort direction: asc or desc
Default: 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();
200 OK Response
{
  "success": true,
  "disputes": [ /* array of dispute objects */ ],
  "total": 143,
  "limit": 20,
  "offset": 0
}
GET /api/disputes/:id Get dispute with full resolution history

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();
200 OK Response
{
  "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"
      }
    ]
  }
}
POST /api/disputes/import Batch import via CSV

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.

⚠️
This endpoint accepts multipart/form-data. The file field must be named file. Max file size: 10 MB. Max rows: 500.

CSV Format

ColumnRequiredNotes
transaction_idrequiredUnique transaction identifier
dispute_typeoptionalMust be a valid dispute type enum value
customer_nameoptional
customer_emailoptionalMust be valid email format if provided
amountoptionalNumeric value (e.g. 149.99)
currencyoptionalISO 4217 code, defaults to USD
transaction_dateoptionalISO 8601 format
descriptionoptionalDispute description
priorityoptionallow, 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();
200 OK Response
{
  "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 API

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).

POST /api/webhooks Register a webhook endpoint

Creates a new webhook. The secret is returned only once at creation — store it securely.

Request Body

ParameterTypeRequiredDescription
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);
201 Created Response
{
  "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"
  }
}
GET /api/webhooks List all webhooks

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"
GET /api/webhooks/:id Get webhook details

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"
PUT /api/webhooks/:id Update webhook

Update webhook URL, subscribed events, description, or active status.

Request Body (all optional)

ParameterTypeDescription
urlstringNew endpoint URL
eventsstring[]Updated event subscriptions
descriptionstringUpdated description
is_activebooleanSet to false to pause deliveries without deleting
DELETE /api/webhooks/:id Delete webhook

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

EventWhen 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");
});
Reference

Error Codes

All error responses follow the same format: { "success": false, "message": "..." }

StatusMeaningCommon 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.

unauthorized_charge failed_transfer duplicate_charge refund_request account_takeover billing_error service_not_received other

Decision Types

Possible values for resolution.decision in any dispute response.

DecisionDescription
approve_refundFull refund granted. Clear evidence of unauthorized or erroneous transaction.
partial_refundPartial refund granted. Some merit to the claim, mitigating factors considered.
deny_claimClaim denied. Transaction appears legitimate or insufficient evidence provided.
escalate_to_humanDispute routed to human reviewer — high value (>$5K), legal risk, or account takeover suspected.
request_more_infoAdditional 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 →