SDK migrations

Migrate to SDK 4.0 and the v3 session API

Move Python and PHP code from the v2 flow-session calls to v3, which sends the session id in a header.

The v2 session API reaches end of life on November 30, 2026

SDK 4.0.0 deprecates the v2 flow-session calls, such as create_flow_session, invoke_flow_response and poll_flow_response. The v2 session routes reach end of life on November 30, 2026. Move your code to the v3 calls on this page before then.

Version 4.0.0 of the FlowHunt Python and PHP SDKs adds the v3 flow-session API. Moving to it keeps your chat session ids out of URLs, and most of the work is renaming methods. This page shows what changed, which calls to rename, and the code before and after.

What changed#

v3 session calls take the session id in the X-FH-Session-Id header, never in the URL. The request and response bodies are the same as in v2. The v2 session routes are deprecated and reach end of life on November 30, 2026.

On v2, every chat call puts the session id in the path, for example /v2/flows/sessions/{session_id}/invoke. The session id is the only credential an anonymous chat session has, and URLs end up in access logs, proxy logs, browser history and Referer headers. A header doesn’t.

Install 4.0.0#

Python

The package on PyPI is flowhunt.

bash
pip install --upgrade "flowhunt>=4,<5"

PHP

The package on Packagist is flowhunt/flowhunt-php-sdk. It needs PHP 8.1 or newer.

bash
composer require flowhunt/flowhunt-php-sdk:^4.0

Migrate in four steps#

  1. Upgrade the package

    Install 4.0.0 as shown above. Your existing v2 calls keep working after the upgrade, so you can move one call at a time. Finish before November 30, 2026.

  2. Rename each session call

    Add _v3 to the Python method name or V3 to the PHP one. The method mapping lists every pair.

  3. Pass the session id as the header argument

    The session-id argument is now x_fh_session_id. In Python, pass it by keyword. In PHP, three methods take it second instead of first. See argument order changed for three methods.

  4. Check the removed methods

    If you call any of the methods removed in 4.0.0, switch to the replacement or remove the call.

Method mapping#

Python methods are on flowhunt.FlowsApi, and the same v3 methods also exist on flowhunt.V3Api. PHP methods are on FlowHunt\Api\FlowsApi.

Whatv2 HTTPv3 HTTP (session id in X-FH-Session-Id)Python 3.x → 4.0PHP 3.x → 4.0
Start flow session (API key)POST /v2/flows/sessions/from_flow/createPOST /v3/flows/sessions/from_flow/createcreate_flow_session → create_flow_session_v3createFlowSession → createFlowSessionV3
Start chatbot session (anonymous)POST /v2/flows/sessions/createPOST /v3/flows/sessions/createcreate_chatbot_session → create_chatbot_session_v3createChatbotSession → createChatbotSessionV3
Send a messagePOST /v2/flows/sessions/{id}/invokePOST /v3/flows/sessions/invokeinvoke_flow_response → invoke_flow_response_v3invokeFlowResponse → invokeFlowResponseV3
Poll for eventsPOST /v2/flows/sessions/{id}/invocation_response/{from_timestamp}POST /v3/flows/sessions/invocation_response/{from_timestamp}poll_flow_response → poll_flow_response_v3pollFlowResponse → pollFlowResponseV3
Cancel a runPOST /v2/flows/sessions/{id}/cancelPOST /v3/flows/sessions/cancelcancel_flow_session → cancel_flow_session_v3cancelFlowSession → cancelFlowSessionV3
Go back to previous anchorPOST /v2/flows/sessions/{id}/backPOST /v3/flows/sessions/backgo_back → go_back_v3goBack → goBackV3
Fire a chat hookPOST /v2/flows/sessions/{id}/hookPOST /v3/flows/sessions/hookfire_chat_hook → fire_chat_hook_v3fireChatHook → fireChatHookV3
Resume a waiting hookPOST /v2/flows/sessions/{id}/resume-hookPOST /v3/flows/sessions/resume-hookresume_hook → resume_hook_v3resumeHook → resumeHookV3
Set flow variablesPATCH /v2/flows/sessions/{id}/variablesPATCH /v3/flows/sessions/variablesupdate_session_variables → update_session_variables_v3updateSessionVariables → updateSessionVariablesV3
List attachmentsGET /v2/flows/sessions/{id}/attachmentsGET /v3/flows/sessions/attachmentsget_attachments → get_attachments_v3getAttachments → getAttachmentsV3
Upload attachmentPOST /v2/flows/sessions/{id}/attachmentsPOST /v3/flows/sessions/attachmentsupload_attachments → upload_attachments_v3uploadAttachments → uploadAttachmentsV3
Delete attachmentDELETE /v2/flows/sessions/{id}/attachments/{file_id}DELETE /v3/flows/sessions/attachments/{file_id}delete_attachment → delete_attachment_v3deleteAttachment → deleteAttachmentV3
Message feedbackPOST /v2/flows/sessions/{id}/feedbackPOST /v3/flows/sessions/feedbackcreate_message_feedback → create_message_feedback_v3createMessageFeedback → createMessageFeedbackV3
Read an artefactGET /v2/flows/sessions/{id}/artefacts/{artefact_id}GET /v3/flows/sessions/artefacts/{artefact_id}get_artefact_content → get_artefact_content_v3getArtefactContent → getArtefactContentV3
Search on type (new, no v2)nonePOST /v3/flows/sessions/searchinvoke_search_v3invokeSearchV3

Downloading an attachment (GET /v2/flows/sessions/{id}/attachments/{file_id}) has no v3 version. It is a signed-in dashboard call and isn’t part of this migration.

Only the two create calls need your API key. from_flow/create starts a session for a flow in your workspace and requires the key. create is the anonymous chatbot session that the embedded chat widget uses, so it takes no key. Both return session_id and created_at with HTTP 201. Every other v3 session call needs only the X-FH-Session-Id header.

Upload attachments over HTTP for now

In 4.0.0, upload_attachments_v3 and uploadAttachmentsV3 can’t send a file. The API returns 422 “Expected UploadFile”, and the Python method rejects bytes before the request is sent. The 3.x upload methods have the same problem. Until it is fixed, send the file as multipart/form-data to POST /v3/flows/sessions/attachments with the X-FH-Session-Id header. The HTTP tab below shows the call.

Before and after#

This example starts a session for a published flow, sends “Hello” and prints the flow’s answer. Each tab shows the 3.x code first, then the 4.0 code we ran against https://api.flowhunt.io. The 4.0 code reads the API key, workspace id and flow id from the environment variables FLOWHUNT_API_KEY, FLOWHUNT_WORKSPACE_ID and FLOWHUNT_FLOW_ID.

Python

Python, SDK 3.x
session = flows.create_flow_session(workspace_id, flowhunt.FlowSessionCreateFromFlowRequest(flow_id=FLOW_ID))
invocation = flows.invoke_flow_response(session.session_id, flowhunt.FlowSessionInvokeRequest(message="Hello"))
events = flows.poll_flow_response(session.session_id, from_timestamp)
Python, SDK 4.0
import os
import time

import flowhunt

config = flowhunt.Configuration(host="https://api.flowhunt.io")
config.api_key["APIKeyHeader"] = os.environ["FLOWHUNT_API_KEY"]

with flowhunt.ApiClient(config) as client:
    flows = flowhunt.FlowsApi(client)

    session = flows.create_flow_session_v3(
        workspace_id=os.environ["FLOWHUNT_WORKSPACE_ID"],
        flow_session_create_from_flow_request=flowhunt.FlowSessionCreateFromFlowRequest(
            flow_id=os.environ["FLOWHUNT_FLOW_ID"]
        ),
    )
    session_id = session.session_id  # keep secret, never log it

    invocation = flows.invoke_flow_response_v3(
        x_fh_session_id=session_id,
        flow_session_invoke_request=flowhunt.FlowSessionInvokeRequest(message="Hello"),
    )

    since, seen, answered = int(invocation.created_at), set(), False
    for _ in range(60):
        time.sleep(2)
        events = flows.poll_flow_response_v3(
            x_fh_session_id=session_id, from_timestamp=since
        )
        new_events = [e for e in events if e.event_id not in seen]
        for event in new_events:
            seen.add(event.event_id)
            since = max(since, int(float(event.created_at_timestamp)) - 1)
            if event.action_type == "failed":
                raise RuntimeError("The flow run failed")
            if event.event_type == "ai" and event.action_type == "message":
                print(event.metadata.to_dict()["message"])
                answered = True
        if answered and not new_events:
            break

PHP

PHP, SDK 3.x
$session = $flows->createFlowSession($workspaceId, $request);
$invocation = $flows->invokeFlowResponse($session->getSessionId(), $invokeRequest);
$events = $flows->pollFlowResponse($session->getSessionId(), $fromTimestamp);
PHP, SDK 4.0
<?php
require 'vendor/autoload.php';

$config = FlowHunt\Configuration::getDefaultConfiguration()
    ->setHost('https://api.flowhunt.io')
    ->setApiKey('Api-Key', getenv('FLOWHUNT_API_KEY'));
$flows = new FlowHunt\Api\FlowsApi(new GuzzleHttp\Client(), $config);

$session = $flows->createFlowSessionV3(
    getenv('FLOWHUNT_WORKSPACE_ID'),
    new FlowHunt\Model\FlowSessionCreateFromFlowRequest(['flow_id' => getenv('FLOWHUNT_FLOW_ID')])
);
$sessionId = $session->getSessionId(); // keep secret, never log it

$invocation = $flows->invokeFlowResponseV3(
    $sessionId,
    new FlowHunt\Model\FlowSessionInvokeRequest(['message' => 'Hello'])
);

$since = (int) $invocation->getCreatedAt();
$seen = [];
$answered = false;
for ($i = 0; $i < 60; $i++) {
    sleep(2);
    // Note the order: timestamp first, session id second.
    $events = $flows->pollFlowResponseV3($since, $sessionId);
    $newEvents = array_filter($events, fn ($e) => !isset($seen[$e->getEventId()]));
    foreach ($newEvents as $event) {
        $seen[$event->getEventId()] = true;
        $since = max($since, (int) $event->getCreatedAtTimestamp() - 1);
        if ($event->getActionType() === 'failed') {
            throw new RuntimeException('The flow run failed');
        }
        if ($event->getEventType() === 'ai' && $event->getActionType() === 'message') {
            echo $event->getMetadata()->getMessage(), PHP_EOL;
            $answered = true;
        }
    }
    if ($answered && !$newEvents) {
        break;
    }
}

HTTP

Only the create call sends the API key. Every call after it sends the session id in the X-FH-Session-Id header.

Start a session (API key)
curl -X POST "https://api.flowhunt.io/v3/flows/sessions/from_flow/create?workspace_id=$WORKSPACE_ID" \
  -H "Api-Key: $FLOWHUNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"flow_id\": \"$FLOW_ID\"}"
Send a message
curl -X POST "https://api.flowhunt.io/v3/flows/sessions/invoke" \
  -H "X-FH-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'
Poll for events
curl -X POST "https://api.flowhunt.io/v3/flows/sessions/invocation_response/$FROM_TIMESTAMP" \
  -H "X-FH-Session-Id: $SESSION_ID"
Upload an attachment
curl -X POST "https://api.flowhunt.io/v3/flows/sessions/attachments" \
  -H "X-FH-Session-Id: $SESSION_ID" \
  -F "file=@invoice.jpg;type=image/jpeg"

Before, the session id was part of each path, for example /v2/flows/sessions/$SESSION_ID/invoke. A v3 call without the header returns 401.

How polling works#

  • from_timestamp is milliseconds since the epoch. The poll returns the events whose created_at_timestamp is strictly greater.
  • Start from the created_at of the invoke response, which is a millisecond value sent as a string, so convert it to an integer. Use 0 to get the whole session. Don’t use the created_at of the create response, which is a date and time, not milliseconds.
  • After each poll, move from_timestamp to the newest event’s created_at_timestamp minus 1 and skip events whose event_id you have already seen. Events created in the same millisecond can arrive in different polls. The FlowHunt chat widget polls the same way.
  • Each event has an event_type (ai, human or system), an action_type (message, loading, tool_call, failed, hook_waiting_for_input and others) and metadata. The answer text is in metadata.message of the ai message events.

FlowHunt doesn’t send a separate “run finished” event. When we ran the samples against a one-step flow, the poll returned a system loading event, then one ai message with the answer, and nothing after that. That is why the samples stop at the first poll that brings no new events after an answer arrived, and raise on failed. If your flow sends several messages or waits for input with hook_waiting_for_input, decide your own stop condition.

Argument order changed for three methods#

Reorder these three calls

The v3 methods put path parameters before the session-id header. In PHP, and in Python when you pass arguments by position, these three calls fail with a type error or quietly send the wrong values unless you reorder them.

v2v3
pollFlowResponse($session_id, $from_timestamp)pollFlowResponseV3($from_timestamp, $x_fh_session_id)
deleteAttachment($session_id, $file_id)deleteAttachmentV3($file_id, $x_fh_session_id)
getArtefactContent($session_id, $artefact_id)getArtefactContentV3($artefact_id, $x_fh_session_id)

In Python, pass keyword arguments everywhere, for example x_fh_session_id=session_id, and the order no longer matters. Python 4.0 also checks argument types, so from_timestamp must be an int. Passing the string "0" raises a pydantic ValidationError.

Other breaking changes in 4.0.0#

These methods were in 3.19.1 and are gone in 4.0.0, because the API endpoints behind them were removed or changed. PHP method names are the camelCase form of the Python ones, for example get_available_models becomes getAvailableModels.

Removed methodEndpointWhat to do
AgentGridsApi.get_agent_grid_previewGET /v2/agent_grids/{agent_grid_id}/previewRemove the call.
CustomModelsApi.get_available_modelsGET /v2/custom-models/availableRemove the call.
FineTuningsApi.create_image_ft, delete_image_ft, search_image_fts, update_image_ft/v2/photo_ai/images/...Remove the calls.
WorkspacesApi.update_workspace_auto_rechargePUT /v2/workspaces/{workspace_id}/auto-rechargeRemove the call.
FlowBatchApi.export_csvGET /v2/flows/{flow_id}/batch/{batch_run_id}/export-csvCall start_export_csv (POST, same path), then get_export_csv_status (GET .../export-csv/{task_id}).

The methods for the Zendesk channel, Shopify billing, Replicate and SERP pingback webhooks were removed too. Those are incoming webhooks for FlowHunt’s own servers, not calls your code makes.

Keep the session id secret#

The session id is a credential

Anyone who has a session id can read and continue that conversation. Don’t log it, don’t put it in URLs or query strings, and don’t send it to analytics. Treat it like a short-lived password.

What about v2?#

The v2 session routes are deprecated as of SDK 4.0.0 and reach end of life on November 30, 2026. Until then they keep working, and they are marked deprecated in the API reference. SDK 4.0.0 still includes the v2 methods. In Python, calling one raises a DeprecationWarning. In PHP, they are marked @deprecated in the code, which your IDE or static analysis will flag.