From 299645b04a25f7b37d0a5c18fbd86d5787751fb7 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Wed, 8 Apr 2026 13:48:35 -0700 Subject: [PATCH 01/10] Document S2S player session endpoint v2/identify (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add API documentation for the server-to-server identify endpoint covering request payload (game_id, secret_key, user_id, timezone, client_ip, deep_link), success response (201 with player data, opt-out states, player properties, templated deep links), and error responses (403, 404). ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "Take a look at ../server-api-docs and sketch out the request payload and response payload for this new API call form." → Claude: Explored the server-api-docs repo structure (Antora/AsciiDoc), read existing endpoint docs for conventions, then sketched out request and response payloads in markdown for review. 2. "Go ahead and update the docs." → Claude: Created modules/ROOT/pages/other/v2_identify.adoc following the existing AsciiDoc conventions (metadata table, parameter tables, nested response field tables, error response partials). Added nav entry. 3. "Commit your work." → Claude: Committed the documentation changes. ### Key decisions made: - Human guided: write the docs in server-api-docs repo following existing conventions - Claude discovered: existing docs use AsciiDoc/Antora with specific table formatting, response partials, and xref cross-references; 404 error format differs from other endpoints (uses {error:{code,message}} not {status,errors}) 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/nav.adoc | 1 + modules/ROOT/pages/other/v2_identify.adoc | 123 ++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 modules/ROOT/pages/other/v2_identify.adoc diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 8edb393..c4ab5c6 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -11,4 +11,5 @@ * xref:page$other/v2_push_subscription.adoc[Push Subscription Management] * xref:page$other/v2_desktop_subscription.adoc[Desktop Web Push Subscription Management] * xref:page$other/v2_exclusions.adoc[Exclusion Management] +* xref:page$other/v2_identify.adoc[Server to Server Player Sessions] * xref:page$other/v2_users.adoc[Player Removal] diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc new file mode 100644 index 0000000..84593d1 --- /dev/null +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -0,0 +1,123 @@ += Server to Server Player Sessions +:endpoint: v2/identify + +The POST `{endpoint}` endpoint creates or resumes a player session from a server to server context. This is useful for games that manage their own server infrastructure and want to track player sessions, populate player properties for use in audience targeting and message personalization, and resolve deep links with player-specific data. + +When a new player is identified for the first time, Teak will create the player record. Subsequent calls for the same player will resume their session. + +== Creating or Resuming a Player Session +[cols="1h,3a", frame="none", grid="none"] +|=== +| Endpoint +| https://api.gocarrot.com/{endpoint} +| Request Type +| POST +| Content-Type +| application/json or application/x-www-form-urlencoded +|=== + +=== Required Parameters +[cols="1,3a", stripes="even"] +|=== +|Name | Description + +| game_id +| Your Teak App ID +| secret_key +| Your Teak Server Secret +| user_id +| The Game Assigned Player ID. This must match the value provided by the game client's Teak SDK integration. +|=== + +=== Optional Parameters +[cols="1,3a", stripes="even"] +|=== +|Name | Description + +| timezone +| The player's timezone as a UTC offset (e.g. `-7.0` for US Pacific Daylight Time). Used for timezone-aware message scheduling. +| client_ip +| The player's IP address. Used for GeoIP country resolution. When omitted, the IP address of the requesting server is used, which will not produce meaningful GeoIP data for your players. +| deep_link +| A deep link to return in the response. If the link scheme begins with `teak`, xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] is applied and xref:ROOT:user-guide:page$player-properties.adoc[Player Property, window=_blank] values can be substituted using `\{{property_name}}` syntax. Non-teak scheme links are returned as-is. +|=== + +=== Example Request +[source, json] +---- +{ + "game_id": "your_teak_app_id", + "secret_key": "your_server_secret", + "user_id": "player_123", + "timezone": -7.0, + "client_ip": "203.0.113.42", + "deep_link": "teak12345:///reward?level={{player_level}}" +} +---- + +=== Success Response +[cols="1,3a"] +|=== +| Status Code +| 201 +| Response Body +| JSON dictionary. +[cols="1,3a", stripes="even"] +!=== +! id +! Teak's internal numeric ID for the player. +! game_id +! The numeric ID of the game. +! country_code +! Two-letter ISO country code derived from GeoIP lookup of `client_ip` (or the request IP if `client_ip` is not provided). Empty string if GeoIP lookup fails. +! opt_out_states +! A dictionary of channel opt-out states for this player. Contains keys `email`, `push`, and `sms`, each with a `state` value indicating the player's current opt-out preference for that channel. +! user_profile +! The player's current xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank]. A dictionary containing: + +'string_attributes' is a map of string property names to their current values. + +'number_attributes' is a map of number property names to their current values. + +'context' is an opaque string for internal use. +! deep_link +! _(Only present if `deep_link` was provided in the request.)_ The deep link after Liquid template rendering. Player Property values are substituted for `\{{property_name}}` placeholders in links with a `teak` scheme. Non-teak scheme links are returned unmodified. +!=== +| Example +| [source, json] +---- +{ + "id": 12345, + "game_id": 678, + "country_code": "US", + "opt_out_states": { + "email": {"state": "opted_in"}, + "push": {"state": "opted_in"}, + "sms": {"state": "opted_out_no_data"} + }, + "user_profile": { + "string_attributes": { + "player_level": "elite" + }, + "number_attributes": { + "high_score": 9001.0 + }, + "context": "..." + }, + "deep_link": "teak12345:///reward?level=elite" +} +---- +|=== + +=== Error Responses + +==== Forbidden + +:response-code: 403 +:response-body: JSON dictionary with 'status' and 'errors' keys. 'status' will be 'error'. 'errors' will be a dictionary containing keys indicating which parameters were invalid, with values being an array of human readable error messages. +:response-example: {"status":"error","errors":{"authorization":["is invalid"]}} +include::partial$response.adoc[] + +==== Not Found + +:response-code: 404 +:response-body: JSON dictionary with an 'error' key containing a dictionary with 'code' and 'message' keys. +:response-example: {"error":{"code":404,"message":"Could not find game with id unknown_app_id"}} +include::partial$response.adoc[] From 25ab74599260d7f362620d21a30c71b4223d41c8 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Wed, 8 Apr 2026 14:51:53 -0700 Subject: [PATCH 02/10] Remove internal IDs and user_profile from S2S identify response docs (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip id, game_id, and user_profile fields from the documented response to match the implementation change avoiding exposure of internal numeric IDs and internal data structures. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "Feedback on the API: I'd rather not return numeric game id/user id... Let's also cut user_profile from the login s2s response." → Claude: Updated docs to remove id, game_id, user_profile from response fields and example JSON. 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index 84593d1..ab681e0 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -64,19 +64,10 @@ When a new player is identified for the first time, Teak will create the player | JSON dictionary. [cols="1,3a", stripes="even"] !=== -! id -! Teak's internal numeric ID for the player. -! game_id -! The numeric ID of the game. ! country_code ! Two-letter ISO country code derived from GeoIP lookup of `client_ip` (or the request IP if `client_ip` is not provided). Empty string if GeoIP lookup fails. ! opt_out_states ! A dictionary of channel opt-out states for this player. Contains keys `email`, `push`, and `sms`, each with a `state` value indicating the player's current opt-out preference for that channel. -! user_profile -! The player's current xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank]. A dictionary containing: + -'string_attributes' is a map of string property names to their current values. + -'number_attributes' is a map of number property names to their current values. + -'context' is an opaque string for internal use. ! deep_link ! _(Only present if `deep_link` was provided in the request.)_ The deep link after Liquid template rendering. Player Property values are substituted for `\{{property_name}}` placeholders in links with a `teak` scheme. Non-teak scheme links are returned unmodified. !=== @@ -84,23 +75,12 @@ When a new player is identified for the first time, Teak will create the player | [source, json] ---- { - "id": 12345, - "game_id": 678, "country_code": "US", "opt_out_states": { "email": {"state": "opted_in"}, "push": {"state": "opted_in"}, "sms": {"state": "opted_out_no_data"} }, - "user_profile": { - "string_attributes": { - "player_level": "elite" - }, - "number_attributes": { - "high_score": 9001.0 - }, - "context": "..." - }, "deep_link": "teak12345:///reward?level=elite" } ---- From 2737069b53fcd98587fffa03eb74cf6b418d1a9e Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Wed, 8 Apr 2026 19:18:37 -0700 Subject: [PATCH 03/10] Add session_id, rate limiting, and opt-out docs to S2S identify (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add session_id to response fields and example - Document 200 req/sec rate limit with 429 error response - Add NOTE about permanent opt-out behavior (session created but no tokens stored) ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "Let's include session_id in the output, and update the docs accordingly. Let's rate limit to 200 req/sec, but only for S2S calls. Let's document the permanent opt out behavior (it should be documented elsewhere but overcommunicating and putting information at the point of need makes for a better experience)." → Claude: Added session_id to response, :rate_limit: 200 attribute, rate limiting table row, 429 error section, and permanent opt-out NOTE. 2-7. [Rate limiting DRY-up, metrics, and commit discussions - see carrot repo commit for full log] ### Key decisions made: - Human guided: Overcommunicate opt-out behavior at point of need even if documented elsewhere 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index ab681e0..b4ee066 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -1,10 +1,13 @@ = Server to Server Player Sessions :endpoint: v2/identify +:rate_limit: 200 The POST `{endpoint}` endpoint creates or resumes a player session from a server to server context. This is useful for games that manage their own server infrastructure and want to track player sessions, populate player properties for use in audience targeting and message personalization, and resolve deep links with player-specific data. When a new player is identified for the first time, Teak will create the player record. Subsequent calls for the same player will resume their session. +NOTE: If a player has been permanently opted out via data removal, the session will still be created but no push tokens, email addresses, or advertising identifiers will be stored or updated for the player. + == Creating or Resuming a Player Session [cols="1h,3a", frame="none", grid="none"] |=== @@ -14,6 +17,8 @@ When a new player is identified for the first time, Teak will create the player | POST | Content-Type | application/json or application/x-www-form-urlencoded +| Rate Limiting +| {rate_limit} requests per second |=== === Required Parameters @@ -66,6 +71,8 @@ When a new player is identified for the first time, Teak will create the player !=== ! country_code ! Two-letter ISO country code derived from GeoIP lookup of `client_ip` (or the request IP if `client_ip` is not provided). Empty string if GeoIP lookup fails. +! session_id +! An identifier for this player session. Present when a session is successfully created. ! opt_out_states ! A dictionary of channel opt-out states for this player. Contains keys `email`, `push`, and `sms`, each with a `state` value indicating the player's current opt-out preference for that channel. ! deep_link @@ -76,6 +83,7 @@ When a new player is identified for the first time, Teak will create the player ---- { "country_code": "US", + "session_id": "12345", "opt_out_states": { "email": {"state": "opted_in"}, "push": {"state": "opted_in"}, @@ -101,3 +109,10 @@ include::partial$response.adoc[] :response-body: JSON dictionary with an 'error' key containing a dictionary with 'code' and 'message' keys. :response-example: {"error":{"code":404,"message":"Could not find game with id unknown_app_id"}} include::partial$response.adoc[] + +==== Rate Limit Response + +:response-code: 429 +:response-body: JSON dictionary with 'status' and 'errors' keys. 'status' will be 'rate_limit'. 'errors' will contain the key 'rate_limit' +:response-example: {"status":"rate_limit","errors":{"rate_limit":["/{endpoint} may only be called {rate_limit} times per second. Please wait a second and try again"]}} +include::partial$response.adoc[] From b975082c52d5216db753631126bbf92c759bc69d Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Wed, 8 Apr 2026 20:56:51 -0700 Subject: [PATCH 04/10] Document email parameter for S2S identify (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add email as an optional parameter for player session creation. Include in example request payload. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "Great! Now, implement the feature and update the docs." → Claude: Added email param to optional parameters table and example request in v2_identify.adoc. 2. "Commit and push your changes to both repos." → Claude: Staged and committed docs changes. ### Key decisions made: - Claude: Documented permanent opt-out interaction with email storage 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index b4ee066..b2e8dbe 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -43,6 +43,8 @@ NOTE: If a player has been permanently opted out via data removal, the session w | The player's timezone as a UTC offset (e.g. `-7.0` for US Pacific Daylight Time). Used for timezone-aware message scheduling. | client_ip | The player's IP address. Used for GeoIP country resolution. When omitted, the IP address of the requesting server is used, which will not produce meaningful GeoIP data for your players. +| email +| An email address for this player. When provided, Teak will store or update the player's email address for use in email marketing campaigns. If the player has been permanently opted out, the email address will not be stored. | deep_link | A deep link to return in the response. If the link scheme begins with `teak`, xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] is applied and xref:ROOT:user-guide:page$player-properties.adoc[Player Property, window=_blank] values can be substituted using `\{{property_name}}` syntax. Non-teak scheme links are returned as-is. |=== @@ -56,6 +58,7 @@ NOTE: If a player has been permanently opted out via data removal, the session w "user_id": "player_123", "timezone": -7.0, "client_ip": "203.0.113.42", + "email": "player@example.com", "deep_link": "teak12345:///reward?level={{player_level}}" } ---- From 1f1f16a8faa3f670a17950a3d54b8bed5affbc32 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Thu, 9 Apr 2026 12:53:02 -0700 Subject: [PATCH 05/10] Replace timezone with timezone_id, document ad ID and validation behavior (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S2S identify only accepts IANA timezone names (timezone_id), not numeric UTC offsets. Updated optional params and example request accordingly. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1-2. [See carrot repo commit for full timezone/ad ID/client_ip discussion] 6. "Correction: Since timezone name is supported, we apparently ONLY want to support that. No offsets. Update the code and docs accordingly." → Claude: Replaced timezone + timezone_id params with just timezone_id in docs and example ### Key decisions made: - Human guided: Only timezone_id (IANA names) supported for S2S, not numeric offsets 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index b2e8dbe..bef9907 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -39,8 +39,8 @@ NOTE: If a player has been permanently opted out via data removal, the session w |=== |Name | Description -| timezone -| The player's timezone as a UTC offset (e.g. `-7.0` for US Pacific Daylight Time). Used for timezone-aware message scheduling. +| timezone_id +| The player's IANA timezone name (e.g. `America/Los_Angeles`). Used for timezone-aware message scheduling. When omitted, UTC is assumed. | client_ip | The player's IP address. Used for GeoIP country resolution. When omitted, the IP address of the requesting server is used, which will not produce meaningful GeoIP data for your players. | email @@ -56,7 +56,7 @@ NOTE: If a player has been permanently opted out via data removal, the session w "game_id": "your_teak_app_id", "secret_key": "your_server_secret", "user_id": "player_123", - "timezone": -7.0, + "timezone_id": "America/Los_Angeles", "client_ip": "203.0.113.42", "email": "player@example.com", "deep_link": "teak12345:///reward?level={{player_level}}" From c49f7d4454e652bcfe17a5cbce5baca5294e5f96 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Thu, 9 Apr 2026 13:23:17 -0700 Subject: [PATCH 06/10] Add 400 Bad Request error response documentation (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document parameter validation errors for S2S identify. All validation errors are returned in a single response. Example shows timezone_id and client_ip errors together. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 4. "Should the docs be updated to reflect the error handling changes?" → Claude: Added 400 Bad Request section with multi-error example. 5. "Change that documentation example to use an error for timezone_id, and client_ip not timezone and client_ip." → Claude: Updated example to show timezone_id + client_ip errors. 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index bef9907..5539d33 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -99,6 +99,13 @@ NOTE: If a player has been permanently opted out via data removal, the session w === Error Responses +==== Bad Request + +:response-code: 400 +:response-body: JSON dictionary with 'status' and 'errors' keys. 'status' will be 'error'. 'errors' will be a dictionary containing keys for each invalid parameter, with values being an array of human readable error messages. All validation errors are returned at once. +:response-example: {"status":"error","errors":{"timezone_id":["Mars/Olympus_Mons is not a recognized IANA timezone name"],"client_ip":["not-an-ip is not a valid IP address"]}} +include::partial$response.adoc[] + ==== Forbidden :response-code: 403 From 6274b19861dee990b2462126cbc0aab25d22b40d Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Thu, 9 Apr 2026 13:48:17 -0700 Subject: [PATCH 07/10] Update modules/ROOT/pages/other/v2_identify.adoc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- modules/ROOT/pages/other/v2_identify.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index 5539d33..415f21e 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -46,7 +46,7 @@ NOTE: If a player has been permanently opted out via data removal, the session w | email | An email address for this player. When provided, Teak will store or update the player's email address for use in email marketing campaigns. If the player has been permanently opted out, the email address will not be stored. | deep_link -| A deep link to return in the response. If the link scheme begins with `teak`, xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] is applied and xref:ROOT:user-guide:page$player-properties.adoc[Player Property, window=_blank] values can be substituted using `\{{property_name}}` syntax. Non-teak scheme links are returned as-is. +| A deep link to return in the response. If the link scheme begins with `teak`, xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] is applied and xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank] values can be substituted using `\{{property_name}}` syntax. Non-teak scheme links are returned as-is. |=== === Example Request From 0ac96d6142b3db623eb35ff200621c678bb6e62d Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Thu, 9 Apr 2026 13:48:45 -0700 Subject: [PATCH 08/10] Update modules/ROOT/pages/other/v2_identify.adoc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- modules/ROOT/pages/other/v2_identify.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index 415f21e..621405d 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -88,9 +88,9 @@ NOTE: If a player has been permanently opted out via data removal, the session w "country_code": "US", "session_id": "12345", "opt_out_states": { - "email": {"state": "opted_in"}, - "push": {"state": "opted_in"}, - "sms": {"state": "opted_out_no_data"} + "email": {"state": "opt_in"}, + "push": {"state": "opt_in"}, + "sms": {"state": "opt_out"} }, "deep_link": "teak12345:///reward?level=elite" } From 5fb098975f6be9a9f1011123f9e9a2202a773228 Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Fri, 10 Apr 2026 20:05:44 -0700 Subject: [PATCH 09/10] Address PR #5 feedback: rewrite intro, fix deep_link docs (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite intro per Alex's feedback: "records that a player has started a session", not "creates or resumes". Each call is a new session. Drives Played event; first-time also tracks Installed. - NOTE block: "exclusion" not "data removal", "tracked" not "created", cross-link to v2_exclusions. - Section heading: "Recording a Player Session" not "Creating or Resuming". - deep_link param/response: document that Teak managed links (custom scheme or Teak link domain) are resolved to a custom scheme deep link with Liquid templating applied. Other links returned as-is. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "We're going to work on this branch of carrot, and the corresponding server-api-docs branch (feat/c-583-server-to-server-login-api). There's feedback on server-api-docs PR #5, and we need to update both repos accordingly." → Claude: Fetched PR #5 feedback from AlexSc and MrJoy, summarized all comments. 2. "Question: Does the universal link resolution behavior described in the docs match what the controller is doing?" → Claude: Analyzed controller vs docs, identified 4 mismatches around launch_link/deep_link handling. 3. "In terms of universal link resolution, the docs are authoritative and the controller needs to match the docs. Let's update the docs for 1 and 2, and update the tests to match the docs." → Claude: Updated docs intro/NOTE (items 1&2), removed launch_link-based S2S tests, added deep_link resolution tests. 4. "Doesn't Alex expect the deep_link parameter to do deep link resolution?" → Claude: Re-read Alex's comments — he's asking for resolution through deep_link, not launch_link. Updated docs and tests. 5. "Are you sure the 'applies Liquid templating after resolving a universal link' test matches the documentation?" → Claude: Recognized the test was testing internal pipeline, not API contract. Removed it. 6. "Clarification from Alex: 'If it's a Teak managed link — custom scheme or universal link — we're going to give you back a custom scheme URL with templating applied.'" → Claude: Updated docs to describe two cases (Teak managed → custom scheme + templating, other → as-is). Re-added Liquid-after-resolution test since it IS the documented behavior. 7. "Update the docs and tests." → Claude: Made final docs and test updates matching Alex's clarification. 8. "Commit the changes in both repos, then update the code to pass the tests." → Claude: Committing. ### Key decisions made: - Human guided: docs are authoritative, controller must match - Human guided: Alex's clarification unified resolution + templating into one behavior for Teak managed links - Human caught: Liquid-after-resolution test was correct behavior, not just implementation detail - Claude discovered: launch_link is an SDK-internal param, not part of S2S API surface 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index 621405d..8eddf41 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -2,13 +2,13 @@ :endpoint: v2/identify :rate_limit: 200 -The POST `{endpoint}` endpoint creates or resumes a player session from a server to server context. This is useful for games that manage their own server infrastructure and want to track player sessions, populate player properties for use in audience targeting and message personalization, and resolve deep links with player-specific data. +The POST `{endpoint}` endpoint records that a player has started a session and returns data about the player. This is useful for games that cannot integrate the Teak SDK. -When a new player is identified for the first time, Teak will create the player record. Subsequent calls for the same player will resume their session. +Each call to this endpoint tracks a new session. This drives the xref:ROOT:user-guide:page$audiences.adoc#_played[Played, window=_blank] event in the Audience builder. If Teak has not seen this player before, it also tracks an xref:ROOT:user-guide:page$audiences.adoc#_installed[Installed, window=_blank] event. -NOTE: If a player has been permanently opted out via data removal, the session will still be created but no push tokens, email addresses, or advertising identifiers will be stored or updated for the player. +NOTE: If a player has been permanently opted out via xref:page$other/v2_exclusions.adoc[exclusion], the session will still be tracked but no push tokens, email addresses, or advertising identifiers will be stored or updated for the player. -== Creating or Resuming a Player Session +== Recording a Player Session [cols="1h,3a", frame="none", grid="none"] |=== | Endpoint @@ -46,7 +46,7 @@ NOTE: If a player has been permanently opted out via data removal, the session w | email | An email address for this player. When provided, Teak will store or update the player's email address for use in email marketing campaigns. If the player has been permanently opted out, the email address will not be stored. | deep_link -| A deep link to return in the response. If the link scheme begins with `teak`, xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] is applied and xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank] values can be substituted using `\{{property_name}}` syntax. Non-teak scheme links are returned as-is. +| A deep link to return in the response. If the link is a Teak managed link — either a `teak` custom scheme URL or a URL matching a Teak link domain — it is resolved to a custom scheme deep link with xref:ROOT:user-guide:page$custom-tags.adoc[Liquid templating, window=_blank] applied. xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank] values can be substituted using `\{{property_name}}` syntax. Other links are returned as-is. |=== === Example Request @@ -75,11 +75,11 @@ NOTE: If a player has been permanently opted out via data removal, the session w ! country_code ! Two-letter ISO country code derived from GeoIP lookup of `client_ip` (or the request IP if `client_ip` is not provided). Empty string if GeoIP lookup fails. ! session_id -! An identifier for this player session. Present when a session is successfully created. +! An identifier for this player session. Present when a session is successfully tracked. ! opt_out_states ! A dictionary of channel opt-out states for this player. Contains keys `email`, `push`, and `sms`, each with a `state` value indicating the player's current opt-out preference for that channel. ! deep_link -! _(Only present if `deep_link` was provided in the request.)_ The deep link after Liquid template rendering. Player Property values are substituted for `\{{property_name}}` placeholders in links with a `teak` scheme. Non-teak scheme links are returned unmodified. +! _(Only present if `deep_link` was provided in the request.)_ The processed deep link. Teak managed links — `teak` custom scheme URLs and Teak link domain URLs — are resolved to a custom scheme deep link with Liquid templating applied. xref:ROOT:user-guide:page$player-properties.adoc[Player Properties, window=_blank] values are substituted for `\{{property_name}}` placeholders. Other links are returned unmodified. !=== | Example | [source, json] From 3911c4c812b23b98f5e50423feac95806c3b403c Mon Sep 17 00:00:00 2001 From: Jon Frisby Date: Fri, 10 Apr 2026 21:40:52 -0700 Subject: [PATCH 10/10] Document client_ip/country_code geolocation requirement (C-583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Either client_ip or country_code must be provided. client_ip takes priority. New country_code param for when IP is unavailable. Updated response country_code description and 400 error example. If neither value is known, pass 127.0.0.1 for client_ip. ## Human-Claude Interaction Log ### Human prompts (VERBATIM): 1. "Next up, we need to reconcile the geolocation functionality with another S2S API we're making on a different branch. The documented behavior there, which we'd like to have parity with here is to use the first one of these that is available: 1. `client_ip` 2. `country_code` (we may need to resolve this to a `location_id` via Parsnip?) 3. The player's location from their most recent SDK session What does it take to get us there?" → Claude: Traced geolocation flow, found Parsnip endpoints, identified gaps. 2. "Actually, we're going to skip #3 here. We're also going to make a change so that we don't fall back to using the server IP if neither `client_ip` nor `country_code` is provided. One of the two MUST be provided. We need to update the docs appropriately. In the event that neither value is known or they do not want to provide it, they should pass 127.0.0.1 for `client_ip`. Let's get the docs and specs updated, shall we?" → Claude: Updated docs and tests. 3. "Great! Now, let's get the controller updated." → Claude: Implemented controller changes. 4. "Commit and push your changes to both repos." → Claude: Committing. ### Key decisions made: - Human guided: skip #3 (fallback to SDK session location), require one of client_ip/country_code - Human guided: 127.0.0.1 as the "don't know" sentinel value 🤖 Generated with Claude Code Co-Authored-By: Claude --- modules/ROOT/pages/other/v2_identify.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ROOT/pages/other/v2_identify.adoc b/modules/ROOT/pages/other/v2_identify.adoc index 8eddf41..b39d01c 100644 --- a/modules/ROOT/pages/other/v2_identify.adoc +++ b/modules/ROOT/pages/other/v2_identify.adoc @@ -42,7 +42,9 @@ NOTE: If a player has been permanently opted out via xref:page$other/v2_exclusio | timezone_id | The player's IANA timezone name (e.g. `America/Los_Angeles`). Used for timezone-aware message scheduling. When omitted, UTC is assumed. | client_ip -| The player's IP address. Used for GeoIP country resolution. When omitted, the IP address of the requesting server is used, which will not produce meaningful GeoIP data for your players. +| The player's IP address. Used for GeoIP country resolution. Either `client_ip` or `country_code` must be provided. If both are provided, `client_ip` takes priority. If neither the player's IP address nor country are known, pass `127.0.0.1`. +| country_code +| Two-letter ISO country code (e.g. `US`, `DE`). Used when the player's IP address is not available but their country is known. Either `client_ip` or `country_code` must be provided. If both are provided, `client_ip` takes priority. | email | An email address for this player. When provided, Teak will store or update the player's email address for use in email marketing campaigns. If the player has been permanently opted out, the email address will not be stored. | deep_link @@ -73,7 +75,7 @@ NOTE: If a player has been permanently opted out via xref:page$other/v2_exclusio [cols="1,3a", stripes="even"] !=== ! country_code -! Two-letter ISO country code derived from GeoIP lookup of `client_ip` (or the request IP if `client_ip` is not provided). Empty string if GeoIP lookup fails. +! Two-letter ISO country code. When `client_ip` is provided, this is derived from GeoIP lookup. When `country_code` is provided instead, this echoes the provided value. Empty string if GeoIP lookup fails. ! session_id ! An identifier for this player session. Present when a session is successfully tracked. ! opt_out_states @@ -103,7 +105,7 @@ NOTE: If a player has been permanently opted out via xref:page$other/v2_exclusio :response-code: 400 :response-body: JSON dictionary with 'status' and 'errors' keys. 'status' will be 'error'. 'errors' will be a dictionary containing keys for each invalid parameter, with values being an array of human readable error messages. All validation errors are returned at once. -:response-example: {"status":"error","errors":{"timezone_id":["Mars/Olympus_Mons is not a recognized IANA timezone name"],"client_ip":["not-an-ip is not a valid IP address"]}} +:response-example: {"status":"error","errors":{"timezone_id":["Mars/Olympus_Mons is not a recognized IANA timezone name"],"location":["either client_ip or country_code must be provided"]}} include::partial$response.adoc[] ==== Forbidden