Resources / Field guide

Calling AI APIs from FileMaker: the two bugs everyone hits

FileMaker is a great front end for AI work — push a PDF or some text at a model, get structured data back. But two predictable bugs stop almost everyone on their first try: an "invalid base64" error, and a request body the API rejects as malformed JSON. Both are one-line fixes once you know them.

The setup

The pattern is simple: build a JSON request in FileMaker, send it with Insert from URL, parse the JSON that comes back. It's the same flow as any API call (see consuming an API end to end) — but two FileMaker-specific details bite when an AI model is on the other end, especially when you're sending a PDF. I hit both shipping a document-extraction feature; here's each one and the fix.

Bug #1 — "Invalid base64 data"

To send a PDF (or image) to a model's vision/document path, you base64-encode the file and put it in the request. The obvious function is Base64Encode — and it produces base64 the API rejects:

# ❌ Base64Encode follows RFC 2045: it inserts a CRLF every 76 characters.
#    The API sees those line breaks and returns:
#    "messages.0.content.0.document.source.base64: Invalid base64 data"
Set Variable [ $pdf ; Value: Base64Encode ( Document::PDF_Container ) ]

RFC 2045 (the MIME flavor) wraps lines at 76 characters. APIs that want a single unbroken base64 string choke on the embedded newlines. FileMaker has a second function that lets you pick the RFC — use RFC 4648, which has no line breaks:

# ✅ RFC 4648 = one unbroken base64 string, exactly what the API wants
Set Variable [ $pdf ; Value: Base64EncodeRFC ( 4648 ; Document::PDF_Container ) ]

That's the entire fix. The same applies to anything else you base64 for an API — auth headers, JWTs, image payloads.

Bug #2 — the request body isn't valid JSON

The second wall is building the body. Two tempting approaches both fail:

  • String concatenation. The moment a value contains a quote, a newline, or a backslash, you've produced invalid JSON — and prompts and extracted text are full of those.
  • JSONFormatElements. It only pretty-prints JSON that's already valid. Hand it text it can't parse and it returns ? — not an error you can catch, just a silent question mark that the API then rejects.

Build the body with JSONSetElement instead. It quotes and escapes values for you, and the type constant tells it how to encode each one:

# JSONString  → quotes + escapes text safely (prompts, extracted text)
# JSONNumber  → real numeric (wrap maybe-empty fields in GetAsNumber)
# JSONRaw     → insert already-built JSON without re-escaping it
Set Variable [ $body ; Value: JSONSetElement ( "{}" ;
    [ "model"             ; "claude-sonnet-4-6"            ; JSONString ] ;
    [ "max_tokens"        ; GetAsNumber ( Settings::MaxTokens ) ; JSONNumber ] ;
    [ "system"            ; $systemPrompt                  ; JSONString ] ;
    [ "messages[0].role"  ; "user"                         ; JSONString ] ;
    [ "messages[0].content" ; $content                     ; JSONRaw ]
) ]

The key move is JSONRaw for messages[0].content: you build the content array separately (next section) and drop it in as-is. If you used JSONString there, the whole array would be escaped into a single string and the API would reject it.

Watch your token ceiling. A default max_tokens like 2000 silently truncates long structured output — a 16-line invoice or PO comes back cut off mid-JSON. Make it a field you can raise per document type (4000+ for big tables) rather than a hardcoded constant.

Building the content array (text + PDF)

An AI message's content is an array of typed blocks. For "here's a PDF, extract this," you send a document block plus a text block. Build the array with JSONSetElement using array paths:

Set Variable [ $pdf ; Value: Base64EncodeRFC ( 4648 ; Document::PDF_Container ) ]

Set Variable [ $content ; Value: JSONSetElement ( "[]" ;
    # block 0 — the instruction
    [ "[0].type" ; "text" ; JSONString ] ;
    [ "[0].text" ; $promptText ; JSONString ] ;
    # block 1 — the PDF itself
    [ "[1].type"               ; "document"        ; JSONString ] ;
    [ "[1].source.type"        ; "base64"          ; JSONString ] ;
    [ "[1].source.media_type"  ; "application/pdf"  ; JSONString ] ;
    [ "[1].source.data"        ; $pdf              ; JSONString ]
) ]

The resulting JSON is exactly the shape the API expects:

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 4000,
  "system": "Extract the fields as JSON…",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "Extract the line items." },
        { "type": "document",
          "source": { "type": "base64", "media_type": "application/pdf", "data": "JVBERi0x…" } }
      ]
    }
  ]
}

Sending it

Send with Insert from URL, putting the headers in the cURL options. For Claude that's the API key, the API version, and a JSON content type:

Set Variable [ $curl ; Value:
    "-X POST" &
    " -H \"x-api-key: " & Settings::AI_ApiKey & "\"" &
    " -H \"anthropic-version: 2023-06-01\"" &
    " -H \"content-type: application/json\"" &
    " --data @$body" ]

Insert from URL [ Select ; With dialog: Off ;
    Target: $$AI_Response ; "https://api.anthropic.com/v1/messages" ;
    Verify SSL Certificates ; cURL options: $curl ]

# Pull the model's text back out
Set Variable [ $text ; Value: JSONGetElement ( $$AI_Response ; "content[0].text" ) ]

One prompt, two paths. Cheap PDFs have a real text layer you can extract locally and send as text; scanned ones don't and need the document/vision block above. Write your system prompt to be path-agnostic ("the document is supplied either as extracted text or as a PDF — handle both the same way") so a single prompt serves both routes and you can switch per document.

The takeaway

FileMaker is genuinely capable as an AI client — the blockers aren't conceptual, they're two encoding details. Use Base64EncodeRFC(4648; …) for binary, build every request body with JSONSetElement (and JSONRaw for nested arrays), and give yourself a generous, configurable token limit. Get those right and the rest is ordinary API plumbing.

Want AI features inside your FileMaker solution?

Document extraction, semantic search, AI-assisted data entry — built into the system your team already uses. I do this work directly. Let's talk about what you're trying to automate.

Work with me