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.
pip install --upgrade "flowhunt>=4,<5"PHP
The package on Packagist is flowhunt/flowhunt-php-sdk. It needs PHP 8.1 or newer.
composer require flowhunt/flowhunt-php-sdk:^4.0Migrate in four steps#
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.
Rename each session call
Add
_v3to the Python method name orV3to the PHP one. The method mapping lists every pair.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.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.
| What | v2 HTTP | v3 HTTP (session id in X-FH-Session-Id) | Python 3.x → 4.0 | PHP 3.x → 4.0 |
|---|---|---|---|---|
| Start flow session (API key) | POST /v2/flows/sessions/from_flow/create | POST /v3/flows/sessions/from_flow/create | create_flow_session → create_flow_session_v3 | createFlowSession → createFlowSessionV3 |
| Start chatbot session (anonymous) | POST /v2/flows/sessions/create | POST /v3/flows/sessions/create | create_chatbot_session → create_chatbot_session_v3 | createChatbotSession → createChatbotSessionV3 |
| Send a message | POST /v2/flows/sessions/{id}/invoke | POST /v3/flows/sessions/invoke | invoke_flow_response → invoke_flow_response_v3 | invokeFlowResponse → invokeFlowResponseV3 |
| Poll for events | POST /v2/flows/sessions/{id}/invocation_response/{from_timestamp} | POST /v3/flows/sessions/invocation_response/{from_timestamp} | poll_flow_response → poll_flow_response_v3 | pollFlowResponse → pollFlowResponseV3 |
| Cancel a run | POST /v2/flows/sessions/{id}/cancel | POST /v3/flows/sessions/cancel | cancel_flow_session → cancel_flow_session_v3 | cancelFlowSession → cancelFlowSessionV3 |
| Go back to previous anchor | POST /v2/flows/sessions/{id}/back | POST /v3/flows/sessions/back | go_back → go_back_v3 | goBack → goBackV3 |
| Fire a chat hook | POST /v2/flows/sessions/{id}/hook | POST /v3/flows/sessions/hook | fire_chat_hook → fire_chat_hook_v3 | fireChatHook → fireChatHookV3 |
| Resume a waiting hook | POST /v2/flows/sessions/{id}/resume-hook | POST /v3/flows/sessions/resume-hook | resume_hook → resume_hook_v3 | resumeHook → resumeHookV3 |
| Set flow variables | PATCH /v2/flows/sessions/{id}/variables | PATCH /v3/flows/sessions/variables | update_session_variables → update_session_variables_v3 | updateSessionVariables → updateSessionVariablesV3 |
| List attachments | GET /v2/flows/sessions/{id}/attachments | GET /v3/flows/sessions/attachments | get_attachments → get_attachments_v3 | getAttachments → getAttachmentsV3 |
| Upload attachment | POST /v2/flows/sessions/{id}/attachments | POST /v3/flows/sessions/attachments | upload_attachments → upload_attachments_v3 | uploadAttachments → uploadAttachmentsV3 |
| Delete attachment | DELETE /v2/flows/sessions/{id}/attachments/{file_id} | DELETE /v3/flows/sessions/attachments/{file_id} | delete_attachment → delete_attachment_v3 | deleteAttachment → deleteAttachmentV3 |
| Message feedback | POST /v2/flows/sessions/{id}/feedback | POST /v3/flows/sessions/feedback | create_message_feedback → create_message_feedback_v3 | createMessageFeedback → createMessageFeedbackV3 |
| Read an artefact | GET /v2/flows/sessions/{id}/artefacts/{artefact_id} | GET /v3/flows/sessions/artefacts/{artefact_id} | get_artefact_content → get_artefact_content_v3 | getArtefactContent → getArtefactContentV3 |
| Search on type (new, no v2) | none | POST /v3/flows/sessions/search | invoke_search_v3 | invokeSearchV3 |
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
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)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:
breakPHP
$session = $flows->createFlowSession($workspaceId, $request);
$invocation = $flows->invokeFlowResponse($session->getSessionId(), $invokeRequest);
$events = $flows->pollFlowResponse($session->getSessionId(), $fromTimestamp);<?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.
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\"}"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"}'curl -X POST "https://api.flowhunt.io/v3/flows/sessions/invocation_response/$FROM_TIMESTAMP" \
-H "X-FH-Session-Id: $SESSION_ID"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_timestampis milliseconds since the epoch. The poll returns the events whosecreated_at_timestampis strictly greater.- Start from the
created_atof the invoke response, which is a millisecond value sent as a string, so convert it to an integer. Use0to get the whole session. Don’t use thecreated_atof the create response, which is a date and time, not milliseconds. - After each poll, move
from_timestampto the newest event’screated_at_timestampminus 1 and skip events whoseevent_idyou 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,humanorsystem), anaction_type(message,loading,tool_call,failed,hook_waiting_for_inputand others) andmetadata. The answer text is inmetadata.messageof theaimessageevents.
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.
| v2 | v3 |
|---|---|
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 method | Endpoint | What to do |
|---|---|---|
AgentGridsApi.get_agent_grid_preview | GET /v2/agent_grids/{agent_grid_id}/preview | Remove the call. |
CustomModelsApi.get_available_models | GET /v2/custom-models/available | Remove 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_recharge | PUT /v2/workspaces/{workspace_id}/auto-recharge | Remove the call. |
FlowBatchApi.export_csv | GET /v2/flows/{flow_id}/batch/{batch_run_id}/export-csv | Call 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.