SP Service Authorization API

Service Authorization API

Send a Georgia DFCS Service Authorization PDF, get structured JSON back — case details, every client on the authorization, and every authorized service line with units and dollar amounts.

Handles all four SA formats: the generated referral forms, the typed manual form, and the handwritten scanned form. Handwritten pages are read from the rendered image, so they work the same as any other.

Getting started

  1. Create an account — it comes with a small starting credit.
  2. Create an API key on your account page. It is shown once.
  3. POST a PDF to /api/v1/extract.php.
  4. Watch billing.balance_usd on each response, or call /api/v1/balance.php.

Prefer to click around first?

Download Postman collection View as JSON

Every endpoint, ready to run. In Postman: Import → drop the file in, then open the collection's Variables tab and paste your key into api_key. base_url is already set to https://sp330.getnums.com. On Extract a document, choose a PDF for the file row on the Body tab — Postman cannot save a file inside a collection, so that row arrives empty.

Authentication

Send your key on every request:

Authorization: Bearer sp260_your_key_here

Or X-API-Key: sp260_... if a bearer header is inconvenient. Issue and revoke your own keys on your account page.

Extract a document

POST https://sp330.getnums.com/api/v1/extract.php

Upload a PDF directly

curl -X POST https://sp330.getnums.com/api/v1/extract.php \
  -H "Authorization: Bearer sp260_your_key_here" \
  -F "[email protected]" \
  --form-string "model=@cf/meta/llama-4-scout-17b-16e-instruct"

model is optional — drop that last line and @cf/meta/llama-4-scout-17b-16e-instruct is used anyway, since it is the default. Every model id is listed under Choosing a model.

Note the --form-string. Model ids begin with @, and curl reads -F "model=@..." as an instruction to upload a local file of that name — it fails with "Failed to open/read local data" before the request is even sent. --form-string sends the value literally. Use plain -F for the file itself, where the @ is what you want.
Scanned or handwritten PDFs. These have no text layer, so their pages must be turned into images before anything can read them. The server does this for you when you post a PDF — just send the file. The response reports rendered_by when it happened.

You can still send your own images (an array of data URIs, one per page) if you have already rendered them, which saves the server the work. On the rare host with no PDF renderer installed, that becomes the only option and the API says so with needs_page_images — you are not charged for that reply.

Send text and page images

POST /api/v1/extract.php
Content-Type: application/json

{
  "filename": "service-authorization.pdf",
  "text":     "<the PDF text layer, or an empty string>",
  "images":   ["data:image/jpeg;base64,...", "..."],
  "page_count": 8,
  "model":    "@cf/meta/llama-4-scout-17b-16e-instruct"   // optional
}

Response

Shown in full under Response format. In outline:

{
  "success": true,
  "message": "SA file analyzed successfully! Found case information.",
  "data": { /* case, services, referred_persons - see below */ },
  "file_path": "sa_files/SA_1785414778_authorization.pdf",
  "billing": { "price_usd": 0.00259, "price_display": "$0.00259",
               "balance_usd": 1.9974 }
}

Native envelope (format=v1)

{
  "ok": true,
  "api_version": 1,
  "request_id": "req_1042",
  "document": { "filename": "...", "sa_type": "type3",
                "read_as": "vision", "page_count": 8, "pages_processed": 8,
                "model": "@cf/meta/llama-4-scout-17b-16e-instruct",
                "model_name": "Llama 4 Scout 17B" },
  "case": {
    "case_id": "18756136",  "service_auth_id": "19324975",
    "first_name": "Warrnette", "last_name": "Ward", "county": "Barrow",
    "service_start": "02/06/2026", "service_end": "03/06/2026", ...
  },
  "case_manager": { "name": "Kim Mcdaniel", "supervisor": "Sarah Landers" },

  // Every person the authorization covers. One SA often lists several
  // siblings who share a single pool of units.
  "clients": [
    { "first_name": "Nyla",     "last_name": "Ward", "person_id": "21132979",
      "service_start": "02/06/2026", "service_end": "03/06/2026" },
    { "first_name": "Zion",     "last_name": "Ward", "person_id": "20709178", ... },
    { "first_name": "Kingston", "last_name": "Ward", "person_id": "21440002", ... }
  ],

  "services": [
    { "uas": "518", "code": "47E", "description": "Behavioral Aide",
      "units": 16, "rate": 38.5, "amount": 616, "end_date": "03/06/2026",
      // true when these units cover ALL the clients above, not one of them
      "shared_pool": true,
      "needs_review": false, "review_note": null }
  ],

  "totals": { "client_count": 3, "service_count": 3,
              "total_authorized": 1146.64, "rows_needing_review": 0 },
  "review": { "flags": {}, "warnings": [ ... ] },
  "billing": { "price_usd": 0.0107, "price_display": "$0.0107",
               "currency": "USD", "processing_ms": 21400 }
}
Shared units are never divided. When one SA covers several clients, the units are the authorized total for the whole case — shared_pool: true marks those rows. A pool of 32 across 5 children has no correct per-child value, so the API reports the total rather than inventing a split.

Every row is checked before you get it

Response format

This is what every call returns — there is no parameter to set. success / message / data, ISO YYYY-MM-DD dates, money as 2-decimal strings, and the service list flattened to one row per client per service.

These key names are fixed. Anything added later arrives as extra keys alongside them — never renamed, never restructured, so nothing below has to be remapped on your side. Today the extras are request_id, api_version and billing.
{
  "success": true,
  "message": "SA file analyzed successfully! Found case information.",
  "data": {
    "case_name": "Daisy Crowder", "case_id": "18916067",
    "first_name": "Daisy", "last_name": "Crowder",
    "service_authorization_id": "90225829",
    "payment_county": "Pickens", "legal_county": "Pickens",
    "date_referral_sent": "2026-01-27",
    "effective_date": "2026-02-01", "case_start_date": "2026-02-01",
    "expiry_date": "2026-03-01",
    "provider_name": "Ally consultants",

    // Flat: one row per client per service.
    "services": [
      { "person_id": "18348646", "service_base_code": "518", "service_type_code": "47e",
        "code": "518-47e", "uas": "518", "service_full_code": "518-47e",
        "name": "Behavioral Aide", "type": "Behavioral Aide",
        "units": 16, "rate": "38.50", "total_amount": "616.00",
        "start_date": "2026-02-01", "end_date": "2026-03-01",
        "should_ignore_units": false, "id": null, "person_name": null }
    ],

    "referred_persons": [
      { "first_name": "Daisy", "last_name": "Crowder", "person_id": "18348646",
        "address": "131 Lois Parks Dr.", "city": "Ellijay", "state": "Georgia", "zip": "30540",
        "dob": "", "medicaid_id": "", "state_id": null,
        "selected_services": [ /* that client's rows */ ] }
    ],

    "assigned_case_manager": { "first_name": "Ulsi", "last_name": "Watson",
                               "full_name": "Ulsi Watson" },
    "dfcs_cm_email": "[email protected]", "dfcs_cm_phone": "(706) 301-8997",
    "dfcs_cm_id": null, "pay_county_id": null,

    "pdf_type": "manual", "confidence_score": 95,
    "extraction_date": "2026-07-30T11:40:22.032109Z",
    "raw_text_preview": "Intake: [ ] ...",
    "rates": [], "qualifications": [], "contact_info": [],
    "validation_errors": []
  },
  "file_path": "sa_files/SA_1785411571_authorization.pdf",
  "debug_info": { "extraction_method": "...", "processing_time": "...",
                  "file_uploaded": "yes", "extracted_fields": [ "..." ] }
}

Several clients on one authorization

When referred_persons has more than one entry, every client gets a full copy of the services and units / total_amount come back null with should_ignore_units: true. The authorization grants a pool for the whole family and the form does not record how it splits — 32 units across 5 children has no correct per-child value, so you assign it, not us.

Clients after the first rarely have a Person ID printed. Theirs is manual-2, manual-3 and so on: addressable, and obviously not a real Person ID. A lone ID printed beside several names stays on the first client only.

Failures use the same shape

A rejected call answers with success: false, a plain-English message and data: null — including the ones that fail before a model runs, like a bad key or an empty wallet. Branch on success; the HTTP status agrees with it. error.code is also present for programmatic handling.

Fields that are always null

id on a service, state_id, dfcs_cm_id and pay_county_id are primary keys in your database, not values on the PDF. Filling them with a guess would attach a record to the wrong case manager or county, so they are left null. Join on what the form actually prints: service_full_code (e.g. 518-47e), payment_county, and dfcs_cm_email.

Need the older native envelope? Send format=v1. It is kept for anything already built against it and is not the default.

Two more honest limits. type repeats the short service name rather than the long qualification paragraph — that text is not on the authorization. service_authorization_id is null on manual forms, because they do not carry one; only the generated forms print it.

Choosing a model

Send model with the exact id from the table below. Leave it out and the default is used, which is the right choice for almost everything — it reads scans and handwriting and is the cheapest model that does.

# form-data
curl -X POST https://sp330.getnums.com/api/v1/extract.php \
  -H "Authorization: Bearer sp260_your_key" \
  -F "[email protected]" \
  --form-string "model=@cf/meta/llama-4-scout-17b-16e-instruct"
// or JSON
{ "filename": "authorization.pdf", "text": "...", "model": "@cf/meta/llama-4-scout-17b-16e-instruct" }

The response echoes what actually ran, so you can log it:

"document": { "read_as": "vision",
              "model": "@cf/meta/llama-4-scout-17b-16e-instruct",
              "model_name": "Llama 4 Scout 17B", ... }
Model namePass this as modelReads
Llama 3.2 11B Vision @cf/meta/llama-3.2-11b-vision-instruct scans & handwriting
Gemma 4 26B @cf/google/gemma-4-26b-a4b-it scans & handwriting
Llama 4 Scout 17B default @cf/meta/llama-4-scout-17b-16e-instruct scans & handwriting
Mistral Small 3.1 24B @cf/mistralai/mistral-small-3.1-24b-instruct scans & handwriting
Gemini 2.5 Flash google/gemini-2.5-flash scans & handwriting
Kimi K2.7 @cf/moonshotai/kimi-k2.7-code scans & handwriting
Kimi K2.6 @cf/moonshotai/kimi-k2.6 scans & handwriting
Qwen 3 30B @cf/qwen/qwen3-30b-a3b-fp8 text-layer PDFs only
GLM 4.7 Flash @cf/zai-org/glm-4.7-flash text-layer PDFs only
GPT-OSS 20B @cf/openai/gpt-oss-20b text-layer PDFs only
GPT-OSS 120B @cf/openai/gpt-oss-120b text-layer PDFs only
Llama 3.3 70B @cf/meta/llama-3.3-70b-instruct-fp8-fast text-layer PDFs only

A text-only model cannot read a scanned or handwritten authorization. Send one a document with no text layer and the call fails with needs_page_images rather than returning a guess. GET https://sp330.getnums.com/api/v1/models.php returns this same list as JSON, so you can validate a model id before sending. An unknown id returns unknown_model; one that is not enabled returns model_locked.

Pricing

Billed per request. billing.price_display on every response is what that call cost — no subscription, no minimum commitment.

ModelPer pageTypical 8-page scanReads
Llama 3.2 11B Vision
@cf/meta/llama-3.2-11b-vision-instruct
$0.00040 $0.00319 scans & handwriting
Gemma 4 26B
@cf/google/gemma-4-26b-a4b-it
$0.00049 $0.00392 scans & handwriting
Llama 4 Scout 17Bdefault
@cf/meta/llama-4-scout-17b-16e-instruct
$0.00134 $0.0107 scans & handwriting
Mistral Small 3.1 24B
@cf/mistralai/mistral-small-3.1-24b-instruct
$0.00157 $0.0126 scans & handwriting
Gemini 2.5 Flash
google/gemini-2.5-flash
$0.00195 $0.0156 scans & handwriting
Kimi K2.7
@cf/moonshotai/kimi-k2.7-code
$0.00500 $0.0400 scans & handwriting
Kimi K2.6
@cf/moonshotai/kimi-k2.6
$0.00500 $0.0400 scans & handwriting
Qwen 3 30B
@cf/qwen/qwen3-30b-a3b-fp8
$0.00031 — text-layer PDFs only
GLM 4.7 Flash
@cf/zai-org/glm-4.7-flash
$0.00036 — text-layer PDFs only
GPT-OSS 20B
@cf/openai/gpt-oss-20b
$0.00089 — text-layer PDFs only
GPT-OSS 120B
@cf/openai/gpt-oss-120b
$0.00163 — text-layer PDFs only
Llama 3.3 70B
@cf/meta/llama-3.3-70b-instruct-fp8-fast
$0.00185 — text-layer PDFs only

The figures above are for a typical authorization page. What you are actually charged is measured from the work the document required, so a one-page referral costs a fraction of a long scan rather than a flat fee. Failed calls that never reach a model are not billed.

Balance and usage

GET https://sp330.getnums.com/api/v1/balance.php — balance, spend and recent activity.

curl https://sp330.getnums.com/api/v1/balance.php -H "Authorization: Bearer sp260_your_key"
{
  "ok": true, "api_version": 1, "currency": "USD",
  "balance": { "amount_usd": 4.9793, "display": "$4.9793", "low": false },
  "usage":   { "window_days": 30, "requests": 2, "pages": 15,
               "spend_usd": 0.0207, "spend_display": "$0.0207" },
  "recent": [
    { "at": "2026-07-30 11:04:22", "type": "charge", "amount": -0.0107,
      "display": "-$0.0107", "balance_after": 4.9793, "note": "extract sa-19324975.pdf" },
    { "at": "2026-07-30 10:58:01", "type": "topup",  "amount": 5.0,
      "display": "+$5.0000", "balance_after": 5.0,    "note": "Admin top-up" }
  ]
}

Every extraction response already carries your balance after the charge, so you rarely need a separate call. Poll this before a large batch.

GET https://sp330.getnums.com/api/v1/usage.php?days=30 — per-request history and a daily breakdown.
GET https://sp330.getnums.com/api/v1/models.php — models you may pass as model, with prices.

Wallet

The API is prepaid. Each call is charged to your balance and the response tells you what is left.

A complete example

A scanned authorization covering three siblings — the hardest case the API handles.

curl -X POST https://sp330.getnums.com/api/v1/extract.php \
  -H "Authorization: Bearer sp260_your_key" \
  -F "[email protected]" \
  -F "images[]=data:image/jpeg;base64,..." \
  -F "page_count=8" \
  --form-string "model=@cf/meta/llama-4-scout-17b-16e-instruct"
{
  "ok": true, "api_version": 1, "request_id": "req_1042",

  "document": { "filename": "SA-Ally-Jan2026.pdf", "sa_type": "type3",
                "read_as": "vision", "page_count": 8, "pages_processed": 8,
                "model": "@cf/meta/llama-4-scout-17b-16e-instruct",
                "model_name": "Llama 4 Scout 17B" },

  "case": { "case_id": "18756136", "service_auth_id": "",
            "first_name": "Warrnette", "last_name": "Ward", "county": "Barrow",
            "service_start": "02/06/2026", "service_end": "03/06/2026",
            "provider_name": "Ally Consultants", "date_referral": "02/01/2026",
            "street": "4115 Bridlegate Way", "city": "Snellville",
            "state": "Georgia", "zip": "30039", "phone": "(678) 458-8367" },

  "case_manager": { "name": "Kim Mcdaniel", "supervisor": "Sarah Landers" },

  // One authorization, three children. Each carries the service window.
  "clients": [
    { "first_name": "Nyla",     "last_name": "Ward", "person_id": "21132979",
      "service_start": "02/06/2026", "service_end": "03/06/2026" },
    { "first_name": "Zion",     "last_name": "Ward", "person_id": "20709178",
      "service_start": "02/06/2026", "service_end": "03/06/2026" },
    { "first_name": "Kingston", "last_name": "Ward", "person_id": "21440002",
      "service_start": "02/06/2026", "service_end": "03/06/2026" }
  ],

  "services": [
    { "uas": "518", "code": "47E",  "description": "Behavioral Aide",
      "units": 16, "rate": 38.5,  "amount": 616,    "end_date": "03/06/2026",
      "shared_pool": true, "needs_review": false, "review_note": null },
    { "uas": "518", "code": "56A",  "description": "WRAP Transporting/Escorting",
      "units": 12, "rate": 29.48, "amount": 353.76, "end_date": "03/06/2026",
      "shared_pool": true, "needs_review": false, "review_note": null },
    { "uas": "518", "code": "56AA", "description": "WRAP Transporting (Without Child)",
      "units": 12, "rate": 14.74, "amount": 176.88, "end_date": "03/06/2026",
      "shared_pool": true, "needs_review": false, "review_note": null }
  ],

  "totals": { "client_count": 3, "service_count": 3,
              "total_authorized": 1146.64, "rows_needing_review": 0 },

  "review": { "flags": {}, "warnings": [
      "This authorization covers 3 clients who share one pool of units..." ] },

  "billing": { "price_usd": 0.0107, "price_display": "$0.0107", "currency": "USD",
               "processing_ms": 21400,
               "balance_usd": 4.9686, "balance_display": "$4.9686" }
}

How to read it

Errors

Failures use the same envelope; ok tells them apart.

{ "ok": false, "api_version": 1,
  "error": { "code": "needs_page_images", "message": "..." },
  "billing": { "price_usd": 0, "price_display": "$0.00", "currency": "USD" } }
CodeHTTPMeaning
unauthorized401Missing, unknown or revoked key.
rate_limited429Too many requests this minute. See Retry-After.
not_a_pdf415The upload is not a PDF.
file_too_large413Over the size limit.
needs_page_images422Scanned PDF sent without page images. Not billed.
unknown_model422model is not one from /models.php.
insufficient_credit402Balance too low to start. Not billed.
extraction_failed422Could not read the document. Billed — the model still ran.