← Back to blog

Using Zyfy with ChatGPT, Gemini, and your own AI agents

Zyfy already has an MCP server for Claude, Cursor, and other MCP-compatible clients — install it and you're done, no code required. ChatGPT and Gemini take a different approach, connecting to third-party APIs as tools the model can call directly. Here's how to add Zyfy to each using your own API key, fully under your control.

ChatGPT — Custom GPT Actions

  1. In ChatGPT, go to Explore GPTs → Create (or edit an existing GPT) and open the Configure tab.

  2. Under Actions, click Create new action.

  3. Click Import from URL and paste in the Zyfy OpenAPI spec:

    https://zyfy.uk/openapi/v1.json
    

    This pulls in both endpoints — postcode lookup and vehicle lookup — with full schemas already defined.

  4. Click Authentication and set:

    • Authentication Type: API Key
    • Auth Type: Bearer
    • API Key: your Zyfy key from the dashboard (looks like ea_live_...)

    A quick note on why "Bearer" and not a custom header: ChatGPT's built-in API Key auth only supports sending the credential as Authorization: Bearer <key> or Authorization: Basic <key> — it can't send arbitrary header names. Zyfy accepts the same key either way, so this just works.

  5. Save, then test it in the GPT's preview pane — try "What's the flood risk for SW1A 2AA?" or "Is AB12CDE ULEZ compliant?"

That's it — no OAuth, no shared key, no waiting on a store review. This GPT is private to you until you choose to share or publish it.

Gemini — function calling

Gemini doesn't have a no-code Actions builder — function calling here means giving the model a tool schema in your own code, running the lookup yourself, and handing the result back. Same idea as ChatGPT, just a few more lines since there's no configuration UI. We'll use the zyfy Python client for the lookups themselves rather than hand-rolling HTTP calls.

  1. Install both packages.

    pip install google-genai zyfy
    
  2. Declare both tools. One schema per Zyfy endpoint you want the model to be able to call.

    from google import genai
    
    client = genai.Client()  # reads GEMINI_API_KEY from env
    
    lookup_postcode_declaration = {
        "type": "function",
        "name": "lookup_postcode",
        "description": "Look up UK postcode intelligence — crime rate, flood risk, "
                        "property price, deprivation index, and more.",
        "parameters": {
            "type": "object",
            "properties": {
                "postcode": {"type": "string", "description": "A UK postcode, e.g. 'SW1A 2AA'"},
            },
            "required": ["postcode"],
        },
    }
    
    lookup_vehicle_declaration = {
        "type": "function",
        "name": "lookup_vehicle",
        "description": "Look up UK vehicle intelligence — DVLA details, MOT history, "
                        "odometer trend, and ULEZ compliance.",
        "parameters": {
            "type": "object",
            "properties": {
                "registration": {"type": "string", "description": "A UK vehicle registration, e.g. 'AB12CDE'"},
            },
            "required": ["registration"],
        },
    }
    
    tools = [lookup_postcode_declaration, lookup_vehicle_declaration]
    
  3. Wire up the tool functions using the Zyfy client. Same client, same pattern, for both — no manual headers, no manual JSON parsing, and lookups that are still pending enrichment are retried automatically.

    from dataclasses import asdict
    from zyfy import Zyfy
    
    zyfy = Zyfy()  # reads ZYFY_API_KEY from env
    
    def lookup_postcode(postcode: str) -> dict:
        return asdict(zyfy.postcode.lookup(postcode))
    
    def lookup_vehicle(registration: str) -> dict:
        return asdict(zyfy.vehicle.lookup(registration))
    
    dispatch = {"lookup_postcode": lookup_postcode, "lookup_vehicle": lookup_vehicle}
    
  4. Call the model with the tools attached, execute any function call, and feed the result back.

    interaction = client.interactions.create(
        model="gemini-3-flash-preview",
        input="Is SW1A 2AA a high flood risk area, and is AB12CDE ULEZ compliant?",
        tools=tools,
    )
    
    fc_step = next((s for s in interaction.steps if s.type == "function_call"), None)
    
    if fc_step:
        result = dispatch[fc_step.name](**fc_step.arguments)
    
        final_interaction = client.interactions.create(
            model="gemini-3-flash-preview",
            input=[{
                "type": "function_result",
                "name": fc_step.name,
                "call_id": fc_step.id,
                "result": [{"type": "text", "text": json.dumps(result)}],
            }],
            tools=tools,
            previous_interaction_id=interaction.id,
        )
        print(final_interaction.output_text)
    else:
        print(interaction.output_text)
    

    A model may ask for one tool call at a time — for a prompt that needs both lookups, loop the function-call step until interaction.steps has no more function_call entries, resubmitting each result with previous_interaction_id chained to the last interaction. This is also exactly how you'd wire Zyfy into any other agent framework with tool-calling support (Bedrock, a self-hosted model, your own orchestration code) — define the schema once, call the Zyfy client, feed the result back.

Getting an API key

Every path above runs on your own Zyfy key — get one free at zyfy.uk/signup (100 requests/month, no card required). Full endpoint and signal reference is in the docs.

Try Zyfy free

100 free lookups per month. No credit card required.

Get started free →