DecisionLayer
API guide

API Sandbox

Try every call in the contract-case flow before you write any code against it. One public key, no account, and the same sample answers every time: create a case, check its status, fetch the other party's response and evidence, submit your own, and read the award. Nothing is stored, emailed or filed.

The sandbox key
dvarb_sandbox_public_key
Send it as Authorization: Bearer dvarb_sandbox_public_key to the usual base URL, https://www.decisionlayer.ai/api/v1. The key alone switches a request to the sandbox, so your code is the code you will ship: only the key and the case ids change.

Every sandbox response carries the header X-DecisionLayer-Sandbox: true, and every sandbox case has "test": true, so a sample answer cannot be mistaken for a real case.

§ 01

Three kinds of key

KeyWhat it doesWhat you need
Sandbox
dvarb_sandbox_public_key
Returns the fixed sample cases on this page. Requests are checked, then answered the same way every time. Nothing
Test
dvarb_test_…
Creates cases on your account that move as you act on them, with signing, payment and identity verification skipped. Each party uses its own test key. An account, then Settings › Test API Keys
Production
dvarb_…
Files real cases. An account approved to file, then Settings › API Keys
§ 02

Five fixed cases

A sandbox answer never changes, so a single case cannot show a dispute moving forward. There are five instead, all the same sample dispute (an unpaid $20,000 invoice balance), each frozen at one point in its life.

Case idYou areFrozen atUse it for
case_00000000000000000000000001 Claimant Just created, awaiting_signature What POST /cases returns
case_00000000000000000000000002 Claimant Filed; the respondent has not answered Status when it is not your turn
case_00000000000000000000000003 Claimant The respondent answered with evidence; your reply is due Fetching their response and files, and submitting yours
case_00000000000000000000000004 Respondent A claim was filed against you; your first response is due The same calls from the other side
case_00000000000000000000000005 Claimant decided Reading the award
§ 03

Step 1: create a case

POST /api/v1/cases takes the contract, your opening argument, evidence, and the respondent's name and address as multipart/form-data. The sandbox checks the request the way the live API does, then returns the first fixed case. It does not echo what you sent. A 201 means your request had every required field.

POST /api/v1/cases
curl · returns 201
curl -X POST https://www.decisionlayer.ai/api/v1/cases \
  -H "Authorization: Bearer dvarb_sandbox_public_key" \
  -F "question_for_arbitration=Respondent kept a \$3,500 deposit." \
  -F "argument=The contract required return of the deposit within 30 days." \
  -F "financial_demand_usd=3500.00" \
  -F "respondent_first_name=Jordan" \
  -F "respondent_last_name=Chen" \
  -F "respondent_email=jordan.chen@example.com" \
  -F "respondent_street_address=12 Main St" \
  -F "respondent_city=Austin" \
  -F "respondent_state=TX" \
  -F "respondent_zipcode=78701" \
  -F "claimant_street_address=800 Oak Ave" \
  -F "claimant_city=Denver" \
  -F "claimant_state=CO" \
  -F "claimant_zipcode=80202" \
  -F "claimant_affirmation=true" \
  -F "contract_file=@services_agreement.pdf"

In the response, read case.id, then case.next_action (sign_terms) and case.next_step, which says what comes next and who has to do it. Leave fields out and you get the 422 the live API sends, with every problem listed at once:

POST /api/v1/cases
curl · returns 422
curl -X POST https://www.decisionlayer.ai/api/v1/cases \
  -H "Authorization: Bearer dvarb_sandbox_public_key" \
  -F "question_for_arbitration=Respondent kept a \$3,500 deposit." \
  -F "argument=The contract required return of the deposit within 30 days." \
  -F "respondent_first_name=Jordan" \
  -F "respondent_email=jordan.chen@example.com"
§ 04

Step 2: check the status of a case

GET /api/v1/cases/{id} returns the case as your key sees it. Three fields answer "do I have to do anything?": action_required, next_action and next_step. waiting_on and status_summary say where the case stands whoever has to act. This case is waiting on the respondent, so nothing is asked of you:

GET /api/v1/cases/case_00000000000000000000000002
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000002 \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

This one is waiting on you. action_required is true, next_step.api names the call to make, next_round is 2, and accepted_fields lists what that round takes:

GET /api/v1/cases/case_00000000000000000000000003
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003 \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

To find every case that needs you without checking each one, filter the list:

GET /api/v1/cases?action_required=true
curl · returns 200
curl "https://www.decisionlayer.ai/api/v1/cases?action_required=true" \
  -H "Authorization: Bearer dvarb_sandbox_public_key"
§ 05

Step 3: fetch the other party's response and evidence

Find out that they answered. Poll GET /api/v1/events. Each event says what changed, where its case stands now, and the next step with who has to take it. Store next_cursor from every response and send it back as cursor; you are then given each event once. A case's own history is at GET /api/v1/cases/{id}/events:

GET /api/v1/cases/case_00000000000000000000000003/events
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003/events \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

Near the end is case.response_submitted from the respondent, and last is case.review_completed, which passes the turn to you with a deadline.

Read what they said. GET /api/v1/cases/{id}/responses returns the thread, oldest first. Each round has its argument, what the party asked you to produce (evidence_demands), and its evidence_files:

GET /api/v1/cases/case_00000000000000000000000003/responses
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003/responses \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

List every file on the case. GET /api/v1/cases/{id}/files returns the contract, the claim's evidence and each submitted round's files, in the order they were filed. step and submitted_by say whose each one is:

GET /api/v1/cases/case_00000000000000000000000003/files
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003/files \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

Download one. Every file has a download_url. It takes the same Authorization header; the URL alone opens nothing. This is the first file the respondent attached:

GET /api/v1/cases/case_00000000000000000000000003/files/file_a6cbcf39a3fcb26a44803706
curl · returns the file
curl -OJ https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003/files/file_a6cbcf39a3fcb26a44803706 \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

File names are generated. A file sent through the API is stored under a generated name, such as case_00000000000000000000000003_respondent_supporting_doc_1_66806c1c.pdf. That is the name you get back, not the name it was uploaded under, and the live API does the same. A file the other party uploaded on the website may keep its upload name. Names are not unique, so refer to a file by its id.

§ 06

Step 4: submit your response and evidence

POST /api/v1/cases/{id}/responses submits your turn. You never say which round it is; the server knows from the case. argument and affirmation=true are always required, and a field the open round does not accept is a 422. Files go in evidence; a file that answers the other party's request goes in evidence_response_files.

POST /api/v1/cases/case_00000000000000000000000003/responses
curl · returns 201
curl -X POST https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000003/responses \
  -H "Authorization: Bearer dvarb_sandbox_public_key" \
  -F "argument=The accepted purchase order sets the price." \
  -F "evidence_response=The record you asked for is attached." \
  -F "affirmation=true" \
  -F "evidence=@service_report.pdf" \
  -F "evidence_response_files=@allocation_record.pdf"

The answer is the recorded response and the case as it now stands for you: in_review, waiting on DecisionLayer, nothing required of you. In the sandbox the recorded round is the sample's, not what you sent, and the case itself does not move: read it again and it is still your turn.

Respond when it is not your turn and the answer is a 409 whose details say whose turn it is:

POST /api/v1/cases/case_00000000000000000000000002/responses
curl · returns 409
curl -X POST https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000002/responses \
  -H "Authorization: Bearer dvarb_sandbox_public_key" \
  -F "argument=Too soon." \
  -F "affirmation=true"
§ 07

Step 5: read the award

When status is decided, GET /api/v1/cases/{id}/decision returns the award text and, for 30 days after it is published, a pdf_url. Before then the same call is a 404 with error.reason not_published.

GET /api/v1/cases/case_00000000000000000000000005/decision
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000005/decision \
  -H "Authorization: Bearer dvarb_sandbox_public_key"
§ 08

If you are the respondent

A respondent uses the same calls with their own key. On this case the sandbox key is the respondent: role is respondent, the claim's contract and evidence are on the case object, and round 1 is open.

GET /api/v1/cases/case_00000000000000000000000004
curl · returns 200
curl https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000004 \
  -H "Authorization: Bearer dvarb_sandbox_public_key"

Round 1 also takes evidence_demands (what you ask the claimant to produce) and the counterclaim fields. After a live first response the respondent's next step is identity verification on the web, which is what the returned case shows:

POST /api/v1/cases/case_00000000000000000000000004/responses
curl · returns 201
curl -X POST https://www.decisionlayer.ai/api/v1/cases/case_00000000000000000000000004/responses \
  -H "Authorization: Bearer dvarb_sandbox_public_key" \
  -F "argument=The price in our records is lower." \
  -F "evidence_demands=Your credit allocation record." \
  -F "affirmation=true" \
  -F "evidence=@receiving_log.pdf"

On a live case a respondent first claims it on the web, with the link or the code from their notice, and signs the arbitration terms. Those two steps are not in the sandbox; this case starts after them.

§ 09

What the sandbox does and does not do

  • 01 Same shape as the live API. Sandbox answers are built by the code that builds live ones, so the fields, status names, error envelopes and file downloads are the ones you will get in production.
  • 02 Fixed answers. A valid request always gets the same response. Nothing you send is kept, the dates never move, and no case changes because you called it.
  • 03 Requests are checked. Missing or invalid fields return the live 422. Idempotency-Key is accepted and has no effect.
  • 04 Web links lead nowhere. action_url and view_url have the live form, but the sample cases have no pages on the web.
  • 05 Contract cases only. Signing, claiming, large-file upload tickets, consent cases and simulations answer 403 with a list of what the sandbox covers. Use a test key for those.
§ 10

sandbox_walkthrough.py

Every step on this page as one Python script. It runs as it is; to call the live API, change the key and use your own case ids.

"""DecisionLayer API sandbox: every contract-case call, with fixed answers.

Nothing to set up. The key below is the public sandbox key. It needs no
account, and every call returns the same sample data each time: five fixed
cases, each frozen at one point in its life. Nothing is stored, emailed or
filed, so run this as often as you like.

What it walks through:

1. Create a case (POST /cases). The request is checked like a real one,
   then the fixed "just created" case comes back.
2. Check the status of a case (GET /cases/{id}): one that is waiting on the
   other party, and one that is waiting on you.
3. Fetch the other party's response and evidence: the event that says they
   answered, their response, the list of files, and one download.
4. Submit your response and evidence (POST /cases/{id}/responses), and see
   the 409 a response gets when it is not your turn.
5. Read the award on a decided case (GET /cases/{id}/decision).
6. The same calls from the respondent's side.

To move from the sandbox to real filings, change API_KEY to your own key
and use the case ids the API gives you. Everything else stays the same.

Run it:

    pip install requests
    python sandbox_walkthrough.py
"""

import os
import tempfile

import requests

# The public sandbox key. Swap in your own key to call the live API.
API_KEY = "dvarb_sandbox_public_key"

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

API = f"{BASE_URL}/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
TIMEOUT = 60

# The five fixed sandbox cases.
CASE_CREATED = "case_00000000000000000000000001"  # what POST /cases returns
CASE_WAITING = "case_00000000000000000000000002"  # waiting on the respondent
CASE_REPLY_DUE = "case_00000000000000000000000003"  # they answered; your turn
CASE_RESPONDENT = "case_00000000000000000000000004"  # you are the respondent
CASE_DECIDED = "case_00000000000000000000000005"  # decided; award published


def show(title: str) -> None:
    print(f"\n=== {title} ===")


def check(response: requests.Response, expected: int) -> None:
    """Stop with the API's own explanation if a call did not go as expected."""
    if response.status_code != expected:
        raise SystemExit(
            f"{response.request.method} {response.url} returned "
            f"{response.status_code}, expected {expected}:\n{response.text}"
        )


def post_form(path: str, fields: dict, attachments: list) -> requests.Response:
    """POST multipart/form-data, the only body the write calls accept.

    ``attachments`` is a list of (field, filename, text). Text fields are
    sent as parts too, so the request is multipart even with no files.
    """
    parts = [(name, (None, value)) for name, value in fields.items()]
    parts += [
        (field, (filename, text, "text/plain")) for field, filename, text in attachments
    ]
    return requests.post(f"{API}{path}", headers=HEADERS, files=parts, timeout=TIMEOUT)


def describe(case: dict) -> None:
    step = case["next_step"]
    print(f"  {case['id']}: {case['status']} (you are the {case['role']})")
    print(f"  {case['status_summary']}")
    print(f"  action_required={case['action_required']}, next: {step['description']}")
    if step["api"]:
        print(f"  call: {step['api']}")


# --- 1. Create a case ---------------------------------------------------------

show("1. Create a case")
contract = (
    "Sample services agreement. Any dispute arising under this agreement "
    "will be resolved by binding arbitration administered by DecisionLayer."
)
created = post_form(
    "/cases",
    {
        "question_for_arbitration": "Respondent kept a $3,500 deposit.",
        "argument": "The contract required return of the deposit within 30 days.",
        "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",
        "claimant_affirmation": "true",
    },
    [("contract_file", "services_agreement.txt", contract)],
)
check(created, 201)
print(f"  sandbox header: {created.headers.get('X-DecisionLayer-Sandbox')}")
print(f"  {created.json()['message']}")
describe(created.json()["case"])

# The request is checked before the fixed answer is returned. Leave out the
# contract and the affirmation and you get the 422 the live API would send.
rejected = post_form(
    "/cases",
    {
        "question_for_arbitration": "Respondent kept a $3,500 deposit.",
        "argument": "The contract required return of the deposit within 30 days.",
        "respondent_first_name": "Jordan",
        "respondent_email": "jordan.chen@example.com",
    },
    [],
)
check(rejected, 422)
print("  A filing with fields missing is a 422 that lists every problem:")
for detail in rejected.json()["error"]["details"][:3]:
    print(f"    - {detail}")

# --- 2. Check the status of a case --------------------------------------------

show("2. Check status")
for case_id in (CASE_WAITING, CASE_REPLY_DUE):
    case = requests.get(f"{API}/cases/{case_id}", headers=HEADERS, timeout=TIMEOUT)
    check(case, 200)
    describe(case.json())

needs_me = requests.get(
    f"{API}/cases",
    headers=HEADERS,
    params={"action_required": "true"},
    timeout=TIMEOUT,
)
check(needs_me, 200)
print(f"  cases waiting on you: {[case['id'] for case in needs_me.json()]}")

# --- 3. Fetch the other party's response and evidence -------------------------

show("3. The other party responded")
# Poll GET /events with the cursor from your last call. Each event says what
# changed and where its case stands now, so there is nothing to compare.
feed = requests.get(f"{API}/events", headers=HEADERS, timeout=TIMEOUT)
check(feed, 200)
page = feed.json()
answered = [
    event
    for event in page["events"]
    if event["resource_id"] == CASE_REPLY_DUE
    and event["type"] in ("case.response_submitted", "case.review_completed")
]
# The last two: the respondent's response, then the turn passing to you.
for event in answered[-2:]:
    print(f"  {event['occurred_at']}  {event['type']}: {event['summary']}")
print(f"  store this and send it as ?cursor= next time: {page['next_cursor']}")

thread = requests.get(
    f"{API}/cases/{CASE_REPLY_DUE}/responses", headers=HEADERS, timeout=TIMEOUT
)
check(thread, 200)
for round_ in thread.json():
    print(
        f"  round {round_['round']} by the {round_['submitted_by']}: "
        f"{len(round_['argument'])} characters of argument, "
        f"{len(round_['evidence_files'])} files"
    )
    if round_["evidence_demands"]:
        print(f"  they ask you to produce: {round_['evidence_demands']}")

files = requests.get(
    f"{API}/cases/{CASE_REPLY_DUE}/files", headers=HEADERS, timeout=TIMEOUT
)
check(files, 200)
for item in files.json():
    print(f"  {item['step']:<20} {item['kind']:<11} {item['name']}")

# Every file has a download_url. It takes the same Authorization header.
theirs = next(item for item in files.json() if item["submitted_by"] == "respondent")
download = requests.get(
    theirs["download_url"], headers=HEADERS, stream=True, timeout=TIMEOUT
)
check(download, 200)
saved = os.path.join(tempfile.mkdtemp(prefix="decisionlayer-"), theirs["name"])
with open(saved, "wb") as handle:
    for chunk in download.iter_content(chunk_size=65536):
        handle.write(chunk)
print(f"  saved {os.path.getsize(saved)} bytes to {saved}")

# --- 4. Submit your response and evidence -------------------------------------

show("4. Submit your response")
case = requests.get(
    f"{API}/cases/{CASE_REPLY_DUE}", headers=HEADERS, timeout=TIMEOUT
).json()
print(f"  round {case['next_round']} accepts: {', '.join(case['accepted_fields'])}")
reply = post_form(
    f"/cases/{CASE_REPLY_DUE}/responses",
    {
        "argument": "The accepted purchase order sets the price.",
        "evidence_response": "The record you asked for is attached.",
        "affirmation": "true",
    },
    [
        ("evidence", "service_report.txt", "Sample evidence."),
        ("evidence_response_files", "allocation_record.txt", "Sample record."),
    ],
)
check(reply, 201)
submitted = reply.json()
print(
    f"  recorded {submitted['response']['id']} with "
    f"{len(submitted['response']['evidence_files'])} files"
)
describe(submitted["case"])

# A response when it is not your turn is a 409 that says whose turn it is.
too_soon = post_form(
    f"/cases/{CASE_WAITING}/responses",
    {"argument": "Too soon.", "affirmation": "true"},
    [],
)
check(too_soon, 409)
print(f"  not your turn: {too_soon.json()['error']['details'][0]}")

# --- 5. Read the award --------------------------------------------------------

show("5. Read the award")
decision = requests.get(
    f"{API}/cases/{CASE_DECIDED}/decision", headers=HEADERS, timeout=TIMEOUT
)
check(decision, 200)
award = decision.json()
print(f"  published {award['published_at']}, PDF at {award['pdf_url']}")
print(f"  {award['text'].split('AWARD:')[-1].strip().splitlines()[0]}")

# Until a case is decided, the same call is a 404 with reason not_published.
pending = requests.get(
    f"{API}/cases/{CASE_REPLY_DUE}/decision", headers=HEADERS, timeout=TIMEOUT
)
check(pending, 404)
print(f"  before the award: reason={pending.json()['error']['reason']}")

# --- 6. The respondent's side -------------------------------------------------

show("6. If you are the respondent")
mine = requests.get(f"{API}/cases/{CASE_RESPONDENT}", headers=HEADERS, timeout=TIMEOUT)
check(mine, 200)
describe(mine.json())
answer = post_form(
    f"/cases/{CASE_RESPONDENT}/responses",
    {
        "argument": "The price in our records is lower.",
        "evidence_demands": "Your credit allocation record.",
        "affirmation": "true",
    },
    [("evidence", "receiving_log.txt", "Sample evidence.")],
)
check(answer, 201)
describe(answer.json()["case"])

print("\nDone. Every call above returns the same answer each time you run it.")