Create a Case by Contract with the API

File an arbitration case with the contract, your argument, evidence, and the respondent in one call. The case is created in the awaiting signature state and you finish it on the web at the returned action_url: sign the terms, pay the filing fee, and verify your identity. Track what is waiting on you with the status endpoints, and view the decision online. For the consent-case guide, see Create a Case by Consent.

See the Case API respond in seconds

Run a schema-accurate demo and inspect the JSON immediately—no sign-in or API key required. Each response includes a unique URL where you can visit the generated demo case, but nothing is stored or added to an account.

POST /api/v1/cases stateless demo
Python
import os
import requests

response = requests.post(
    "https://www.decisionlayer.ai/api/v1/cases",
    headers={
        "Authorization": (
            "Bearer "
            + os.environ["DECISIONLAYER_API_KEY"]
        )
    },
    data={
        "question_for_arbitration": (
            "Should the sample project deposit be returned?"
        ),
        "argument": (
            "The contract required return within 30 days."
        ),
        "financial_demand_usd": "3500.00",
        "respondent_first_name": "Jordan",
        "respondent_last_name": "Chen",
        "respondent_email": (
            "api-playground-respondent@decisionlayer.ai"
        ),
        "respondent_street_address": "12 Example St",
        "respondent_city": "Austin",
        "respondent_state": "TX",
        "respondent_zipcode": "78701",
        "claimant_street_address": "800 Example Ave",
        "claimant_city": "Denver",
        "claimant_state": "CO",
        "claimant_zipcode": "80202",
        "claimant_affirmation": "true",
    },
    files={
        "contract_file": (
            "sample_contract.txt",
            b"Sample contract with an arbitration clause.",
            "text/plain",
        )
    },
    timeout=60,
)
response.raise_for_status()
print(response.json())
Response Ready
{
  "message": "Press Run to generate a demo API response.",
  "case": null
}

The Run button uses a safe demo endpoint. It never writes to the database, uploads a file, sends a notification, or files a case. Copy the Python example when you are ready to call the real API with your key.

Case filing is limited to approved accounts. Creating, listing, retrieving, and responding to cases are all live today; only filing needs an approved account. To request filing access contact us — casemanager@decisionlayer.ai

How it works

  1. 1

    Create the casePOST /api/v1/cases with the contract file, your opening argument, evidence, and the respondent's name, email, and mailing address. Your own details default to your account. The response is 201 with status: "awaiting_signature" and an action_url.

  2. 2

    Finish it on the web — open action_url while signed in to sign the arbitration terms, then pay the filing fee, then verify your identity. None of these are API calls. The case is filed and the respondent notified once they are done. Your dashboard lists every case that still needs a step from you.

  3. 3

    Track what is waiting on you — poll GET /api/v1/cases/{id} for status, action_required, next_action (sign_terms, pay_filing_fee, verify_identity, respond) and action_url. GET /api/v1/cases?action_required=true lists only the cases that need you.

  4. 4

    Respond in turns — while the case is awaiting_response, current_turn names who moves next. That party submits their round with POST /api/v1/cases/{id}/responses (argument, optional evidence files, and affirmation=true) or on the web at action_url; the server works out which round it is. Anyone else gets a 409 whose details say whose turn it is. A respondent must sign the arbitration terms on the web first — their next_action is sign_terms until then. Read the whole thread, oldest first, with GET /api/v1/cases/{id}/responses.

  5. 5

    View the answer online — when the case is decided, both claimant and respondent open the case's view_url to see the decision on the web. There is no decision endpoint.

Authenticate every request with a bearer token: Authorization: Bearer dvarb_your_key_here. Claimant and respondent each use their own key; parties without a key simply use the web dashboard instead.

create_real_case.py

"""DecisionLayer API: file an arbitration case by contract and track it.

The flow this script walks through:

1. Create the case with the contract, your opening argument, evidence, and
   the respondent's identity and address (one API call). The case lands in
   the ``awaiting_signature`` state.
2. Finish it on the web -- open the returned ``action_url`` to SIGN the
   arbitration terms, then PAY the filing fee, then VERIFY your identity.
   None of these are API calls. The case is filed and the respondent is
   notified once they are done.
3. Poll the case. ``action_required`` tells you whether the case is still
   waiting on you, ``next_action`` says what to do, and ``action_url`` is
   where to do it. Your dashboard shows the same list.
4. Respond in turns. When ``status`` is ``awaiting_response`` and
   ``current_turn`` is your role, ``POST /api/v1/cases/{id}/responses`` with
   your argument, optional evidence files, and ``affirmation=true``. The
   server knows which round you are on. ``GET .../responses`` returns the
   whole thread, oldest first. Posting when it is not your turn returns
   ``409`` with details saying whose turn it is.
5. When the case is decided, both parties view the answer ONLINE at the
   case's ``view_url`` (there is no decision endpoint).

Respondents use the same script with their own API key, MY_ROLE set to
"respondent", and CASE_ID set to the case from the notification email. Claim
the case and sign the arbitration terms on the web first; the script then
loads that case instead of creating a new one.

Claimants only need to change API_KEY below. Respondents also change MY_ROLE
and CASE_ID. Create a key at https://www.decisionlayer.ai/settings/api-keys

Then run:

    pip install requests
    python create_real_case.py
"""

import mimetypes
import os
import time

import requests

# 1. Paste your API key here (from /settings/api-keys).
API_KEY = "PASTE_YOUR_KEY_HERE"

# 2. Base URL of the DecisionLayer API. Use http://localhost:8000 locally.
BASE_URL = "https://www.decisionlayer.ai"

HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# 3. Set your role. Respondents must also paste their claimed case ID below.
MY_ROLE = "claimant"
CASE_ID = ""


def write_sample_file(name: str, text: str) -> str:
    """Create a small attachment so the demo is self-contained."""
    with open(name, "w") as handle:
        handle.write(text)
    return name


def create_case() -> dict:
    """File the case: contract + argument + evidence + respondent."""
    write_sample_file(
        "sample_contract.txt", "Sample contract for the DecisionLayer demo.\n"
    )
    write_sample_file("sample_evidence.txt", "Sample evidence: deposit receipt.\n")

    # Your own name, email, and address default to your account and the
    # claimant address below (filing_capacity=personal). Mailing addresses
    # are required so the respondent can be formally notified.
    data = {
        "question_for_arbitration": "Respondent kept a $3,500 deposit.",
        "argument": (
            "The contract required return of the deposit within 30 days. "
            "It has been 90 days and the deposit has not been returned."
        ),
        "financial_demand_usd": "3500.00",
        "respondent_first_name": "Jordan",
        "respondent_last_name": "Chen",
        "respondent_email": "jordan.chen@example.com",
        "respondent_street_address": "12 Main St",
        "respondent_city": "Austin",
        "respondent_state": "TX",
        "respondent_zipcode": "78701",
        "claimant_street_address": "800 Oak Ave",
        "claimant_city": "Denver",
        "claimant_state": "CO",
        "claimant_zipcode": "80202",
        # Affirm the information you are submitting is accurate and complete.
        "claimant_affirmation": "true",
    }

    with (
        open("sample_contract.txt", "rb") as contract,
        open("sample_evidence.txt", "rb") as evidence,
    ):
        files = [
            ("contract_file", ("sample_contract.txt", contract, "text/plain")),
            ("evidence", ("sample_evidence.txt", evidence, "text/plain")),
        ]
        response = requests.post(
            f"{BASE_URL}/api/v1/cases",
            headers=HEADERS,
            data=data,
            files=files,
            timeout=60,
        )

    if response.status_code != 201:
        print(f"Create failed ({response.status_code}):")
        print(response.json())
        raise SystemExit(1)

    result = response.json()
    case = result["case"]
    print(f"Created case: {case['id']} (status: {case['status']})")
    print(f"Next step: {case['next_action']}")
    print(f"Finish it here: {result['action_url']}")
    print("Sign the terms there; payment and identity verification follow.")
    return case


def get_case(case_id: str) -> dict:
    """Fetch the case: status, next action, current_turn, and view_url."""
    response = requests.get(
        f"{BASE_URL}/api/v1/cases/{case_id}", headers=HEADERS, timeout=60
    )
    response.raise_for_status()
    return response.json()


def list_cases_needing_action() -> list:
    """Every case that is waiting on you (unfinished filings, your turn, ...)."""
    response = requests.get(
        f"{BASE_URL}/api/v1/cases",
        headers=HEADERS,
        params={"action_required": "true"},
        timeout=60,
    )
    response.raise_for_status()
    return response.json()


def list_responses(case_id: str) -> list:
    """The case's response thread, oldest first (both parties see the same)."""
    response = requests.get(
        f"{BASE_URL}/api/v1/cases/{case_id}/responses", headers=HEADERS, timeout=60
    )
    response.raise_for_status()
    return response.json()


def print_thread(case_id: str) -> None:
    """Show every submitted round on the case."""
    thread = list_responses(case_id)
    print(f"{len(thread)} response(s) on the case:")
    for entry in thread:
        names = ", ".join(f["name"] for f in entry["evidence_files"]) or "no files"
        argument = entry["argument"] or "no argument"
        print(
            f"  Round {entry['round']} by the {entry['submitted_by']} "
            f"at {entry['submitted_at']}: {argument[:60]} [{names}]"
        )


def submit_response(case_id: str, argument: str, evidence_paths=()) -> dict | None:
    """Submit your turn: the argument, optional evidence files, and affirmation.

    The server works out which round you are submitting from the case state.
    A 409 means the case is not waiting on you: the details say whose turn it
    is, or what to do first (a respondent must sign the terms on the web).
    A 422 means a field was rejected; the details name it.
    """
    data = {"argument": argument, "affirmation": "true"}
    handles = [open(path, "rb") for path in evidence_paths]
    try:
        files = [
            (
                "evidence",
                (
                    os.path.basename(path),
                    handle,
                    mimetypes.guess_type(path)[0] or "application/octet-stream",
                ),
            )
            for path, handle in zip(evidence_paths, handles)
        ]
        response = requests.post(
            f"{BASE_URL}/api/v1/cases/{case_id}/responses",
            headers=HEADERS,
            data=data,
            files=files or None,
            timeout=60,
        )
    finally:
        for handle in handles:
            handle.close()

    if response.status_code == 409:
        print("Not your turn yet:")
        for detail in response.json()["error"]["details"]:
            print(f"  - {detail}")
        return None
    if response.status_code == 422:
        print("The response was rejected:")
        for detail in response.json()["error"]["details"]:
            print(f"  - {detail}")
        return None
    response.raise_for_status()

    result = response.json()
    submitted = result["response"]
    print(f"Response submitted. Round {submitted['round']} is on record.")
    case = result["case"]
    if case["action_required"]:
        print(f"Waiting on you: {case['next_action']} -> {case['action_url']}")
    return result


def main() -> None:
    if MY_ROLE not in {"claimant", "respondent"}:
        print('MY_ROLE must be either "claimant" or "respondent".')
        raise SystemExit(1)

    case_id = CASE_ID.strip()
    if MY_ROLE == "respondent" and not case_id:
        print(
            "Respondent mode requires CASE_ID. Claim the case from your "
            "notification email, sign the terms on the web, then paste its "
            "case_... ID into CASE_ID."
        )
        raise SystemExit(1)

    if case_id:
        case = get_case(case_id)
        if case["role"] != MY_ROLE:
            print(
                f"MY_ROLE is {MY_ROLE!r}, but this API key is the "
                f"{case['role']!r} on case {case_id}."
            )
            raise SystemExit(1)
        print(f"Using existing case: {case_id} (status: {case['status']})")
    else:
        case = create_case()
        case_id = case["id"]

    # Poll for the case state. In practice you'd check back after finishing
    # the web steps; polling here just demonstrates the read surface.
    for _ in range(5):
        case = get_case(case_id)
        status, turn = case["status"], case["current_turn"]
        print(f"Status: {status}, turn: {turn}")

        if case["action_required"]:
            print(f"Waiting on you: {case['next_action']} -> {case['action_url']}")
        if status == "decided":
            print(f"The decision is ready. View it online: {case['view_url']}")
            return
        if status == "awaiting_response" and turn == case["role"]:
            reply_evidence = write_sample_file(
                "sample_reply_evidence.txt", "Sample evidence: the signed addendum.\n"
            )
            submitted = submit_response(
                case_id,
                "Section 4.2 does not apply; the termination was mutual.",
                evidence_paths=[reply_evidence],
            )
            if submitted:
                print_thread(case_id)
        time.sleep(5)

    pending = list_cases_needing_action()
    print(f"You have {len(pending)} case(s) waiting on you.")
    print("Still in progress. Re-run later, or check your dashboard.")
    print(f"You can always view the case online: {case['view_url']}")


if __name__ == "__main__":
    main()

Understanding errors

Every error returns a consistent JSON shape with a list of actionable details. Validation problems are reported together so one retry fixes them all:

{
  "error": {
    "status": 422,
    "message": "Your request could not be processed.",
    "details": [
      "Field 'contract_file' is required. Attach the governing contract (.pdf, .docx, .rtf, or .txt) that contains the arbitration clause.",
      "Field 'respondent_city' is required. Provide the respondent's mailing address; e.g. respondent_city='Austin'."
    ]
  }
}

A 403 means your account is not yet approved to file cases; a 404 means the case does not exist or your key does not belong to a party on it; a 409 on a response means the case is not waiting on you — the details say whose turn it is or what to finish on the web first.