Org Web Adapter

hungryroot/v1-endpoint-deprecation-gate.org

ID
9786b791-9b2f-4976-9404-96d09a81c44e

TDD: V1 Endpoint Deprecation Gate

Overview

Two-layer DynamicConfig gate enabling no-code, no-deploy disabling of V1

endpoints as clients migrate. Global kill-switch for final teardown. Default

off — zero behavior change until explicitly configured.

V1 endpoints live at =/api/<action>/= (40 total, defined in =app/urls/apiv1.py=).

All have V2/V3 equivalents. Technical Roadmap '26 lists "v1 deprecation" as Q2+.

Status

IMPLEMENTED. Both layers default off.

Problem

Clients (mobile apps, internal services) migrate at different rates. Need to:

- Disable individual V1 endpoints as each client confirms migration — no deploy

- Observe remaining V1 traffic per endpoint before and after disabling

- Roll back within 5 minutes if a client breaks

- Kill all V1 at once when migration is complete

People

| Role | Name |

|------------------+--------------|

| Technical Author | Colin Powell |

Design

Two-Layer Gate

| Layer | Config key | Type | Default | Purpose |

|----------------------------+-------------------------------------+-----------+---------+---------------------------------------|

| 1 — Global kill-switch | =api.v1_endpoints.disabled= | Boolean | ="0"= | 410 ALL V1 endpoints. Emergency/teardown |

| 2 — Per-endpoint gates | =api.v1_endpoints.endpoint_gates= | JSON dict | ={}= | 410 individually named endpoints |

Per-Endpoint Gate Format

#+BEGIN_SRC json

{

"api-filter-pairings": "enabled",

"api-get-tag-group": "enabled",

"api-get-promo-data": "disabled"

}

#+END_SRC

- ="enabled"= → endpoint returns HTTP 410 Gone

- ="disabled"= or absent → passes through normally

- Unknown URL names → logged as warnings, ignored (typo protection)

Alternatives Rejected

| Approach | Reason |

|-------------------------+-------------------------------------------------------------------------|

| Per-endpoint bool flags | 40 DB rows, 40 enum members, 40 admin entries — overkill |

| Middleware | Fragile path-prefix matching; can't log resolved URL name |

| URL routing gate | =path(...)= evaluated at startup, not per-request |

| 410 vs 404 | 410 = "permanently gone" (RFC 9110); redirect impossible (different contract) |

| Decorator on each view | 40+ edits; gate applied in =apiv1.py= URL conf instead |

Response Body

#+BEGIN_SRC json

{"detail": "This V1 API endpoint has been deprecated. Please migrate to the V2/V3 API."}

#+END_SRC

Uses =detail= key per DRF house convention.

Implementation

Files Changed

| File | Change |

|--------------------------------------------+-----------------------------------------------------------------|

| =app/datatypes/dynamic_config.py= | Added =V1EndpointGates(RootModel[dict[str, str]])= Pydantic model |

| =app/constants/dynamic_config.py= | Added =V1_ENDPOINTS_DISABLED= to =APIDoomsdayConfig=; new =V1DeprecationConfig= |

| =app/views/api.py= | Added =_V1_URL_NAMES=, =_v1_gone_response=, =v1_endpoint_gate= |

| =app/urls/apiv1.py= | All 40 views wrapped with =v1_endpoint_gate(...)= at URL registration |

| =app/tests/views/test_apiv1_gate.py= | New test file — 9 tests, all passing |

Dynamic Config Definitions

#+BEGIN_SRC python

# In APIDoomsdayConfig:

V1_ENDPOINTS_DISABLED = (

"api.v1_endpoints.disabled",

(

"When enabled, ALL V1 /api/<action>/ endpoints return HTTP 410 Gone."

" Use for final teardown or emergencies once all clients have migrated."

" For per-endpoint control, use api.v1_endpoints.endpoint_gates instead."

" Remember to disable this if you need to roll back!"

),

"0",

StringValidationTypeEnum.BOOLEAN,

)

# New class:

@_register_configs

class V1DeprecationConfig(dc_datatypes.BaseDynamicConfig, enum.Enum):

ENDPOINT_GATES = (

"api.v1_endpoints.endpoint_gates",

(

"JSON dict mapping V1 URL names to their gate status."

" Set a URL name to 'enabled' to return HTTP 410 Gone for that endpoint (no deploy needed)."

' Example: {"api-filter-pairings": "enabled", "api-get-tag-group": "enabled"}.'

" URL names are defined in app/urls/apiv1.py."

" Unknown URL names are logged as warnings and ignored."

),

"{}",

PydanticJSONValidator(V1EndpointGates),

)

#+END_SRC

Decorator (app/views/api.py)

#+BEGIN_SRC python

def v1_endpoint_gate(view_func):

"""Gates a V1 view behind DynamicConfig. Returns HTTP 410 Gone when either layer is active.

Layer 1 (global): APIDoomsdayConfig.V1_ENDPOINTS_DISABLED — kills all V1 endpoints at once.

Layer 2 (per-endpoint): V1DeprecationConfig.ENDPOINT_GATES — a JSON dict of {url_name: 'enabled'}.

"""

@functools.wraps(view_func)

def wrapper(request, *args, **kwargs):

if bool_config_is_active(APIDoomsdayConfig.V1_ENDPOINTS_DISABLED):

return _v1_gone_response(request, view_func.__name__)

gates = DynamicConfigValueFetcher.get_validated_value(V1DeprecationConfig.ENDPOINT_GATES)

url_name = request.resolver_match.url_name if request.resolver_match else None

if url_name:

unknown = set(gates.root.keys()) - _V1_URL_NAMES

if unknown:

logger.warning("Unknown V1 URL names in endpoint gates config", unknown_names=sorted(unknown))

if gates.root.get(url_name) == "enabled":

return _v1_gone_response(request, view_func.__name__)

return view_func(request, *args, **kwargs)

return wrapper

#+END_SRC

Applied in apiv1.py

#+BEGIN_SRC python

from app.views.api import v1_endpoint_gate

urlpatterns = [

path("get_cancel_reasons/", v1_endpoint_gate(api.get_cancel_reasons), name="api-get-cancel-reasons"),

# ... all 40 endpoints wrapped

]

#+END_SRC

Tests

=app/tests/views/test_apiv1_gate.py= — 9 tests, all passing:

1. Both layers off → =/api/get_cancel_reasons/= returns 200

2. Global flag on → returns 410 with correct =detail= body

3. Global flag on → applies to multiple endpoints

4. Per-endpoint gate → named endpoint returns 410

5. Per-endpoint gate → other endpoints unaffected (returns non-410)

6. Per-endpoint ="disabled"= → passes through

7. Unknown URL name in gates → no crash, passes through

8. 410 response uses =detail= key

9. Global flag takes precedence over empty gates dict

Rollout

1. [ ] Merge PR (both layers off — no behavior change)

2. [ ] Check Datadog traffic per endpoint (links below) — identify zero-traffic endpoints

3. [ ] For each migrated endpoint: add ="api-<name>": "enabled"= to =api.v1_endpoints.endpoint_gates= in Django admin

4. [ ] Monitor 410s via Datadog =V1 endpoint disabled= log event after each disable

5. [ ] When all endpoints disabled individually → flip =api.v1_endpoints.disabled= to ="1"= (global kill-switch)

6. [ ] After 2-4 weeks confirmed zero traffic → remove URL patterns, view functions, enum members

Endpoint Traffic (Datadog Links)

Click to see traffic per endpoint (grouped by client, excluding bots):

| Endpoint | URL name | Datadog |

|-----------------------------------+-------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

| =/api/facebook_login/= | =api-facebook-login= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-facebook-login%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/apple_login/= | =api-apple-login= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-apple-login%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/google_login/= | =api-google-login= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-google-login%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_hold_time_period_options/= | =api-get-hold-time-period-options= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-hold-time-period-options%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_cancel_reasons/= | =api-get-cancel-reasons= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-cancel-reasons%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_cancel_subreasons/= | =api-get-cancel-subreasons= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-cancel-subreasons%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/cancel_customer_account/= | =api-process-cancel= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-process-cancel%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/set_periodicity/= | =api-set-periodicity= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-set-periodicity%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/set_pairing_rating/= | =api-set-pairing-rating= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-set-pairing-rating%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_order/= | =api-get-order= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-order%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/skip_order/= | =api-skip-order= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-skip-order%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/unskip_order/= | =api-unskip-order= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-unskip-order%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/reschedule_order/= | =api-reschedule-order= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-reschedule-order%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/record_order_view_date/= | =api-record-order-view-date= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-record-order-view-date%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/update_day_of_week/= | =api-update-day-of-week= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-update-day-of-week%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_product/= | =api-get-product= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-product%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_pairing/= | =api-get-pairing= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-pairing%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/filter_pairings/= | =api-filter-pairings= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-filter-pairings%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_product_categories/= | =api-get-product-categories= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-product-categories%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_pairing_dishtypes/= | =api-get-pairing-dishtypes= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-pairing-dishtypes%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_customer_dishtypes/= | =api-get-customer-dishtypes= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-customer-dishtypes%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/update_customer_dishtypes/= | =api-update-customer-dishtypes= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-update-customer-dishtypes%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_dishtypes/= | =api-get-dishtypes= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-dishtypes%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/auto_issue_credit/= | =api-auto-issue-credit= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-auto-issue-credit%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_discovery_survey_choices/= | =api-get-discovery-survey-choices= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-discovery-survey-choices%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/post_discovery_survey_choice/= | =api-post-discovery-survey-choice= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-post-discovery-survey-choice%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_possible_days/= | =api-get-possible-days= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-possible-days%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/save_shipping_day/= | =api-save-shipping-day= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-save-shipping-day%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/save_payment/= | =api-save-payment= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-save-payment%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/place_checkout_order/= | =api-place-checkout-order= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-place-checkout-order%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/submit_promo/= | =api-submit-promo= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-submit-promo%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_tag_group/= | =api-get-tag-group= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-tag-group%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_tag_groups/= | =api-get-tag-groups= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-tag-groups%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_customer_tags/= | =api-get-customer-tags= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-customer-tags%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/update_customer_tags/= | =api-update-customer-tags= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-update-customer-tags%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_customer_dashboard/= | =api-get-customer-dashboard= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-customer-dashboard%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/update_customer/= | =api-update-customer= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-update-customer%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_promo_data/= | =api-get-promo-data= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-promo-data%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/get_cyo_promos/= | =api-get-cyo-promos= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-get-cyo-promos%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

| =/api/update_customer_cyo/= | =api-update-customer-cyo= | [[https://app.datadoghq.com/logs?query=env%3Aprod%2A%20service%3Adjango%20%40route%3Aapi-update-customer-cyo%20-%40user_agent%3A%2Abot%2A&agg_m=count&agg_m_source=base&agg_q=%40route%2C%40client_name&agg_t=count&flat_group_bys=true&refresh_mode=sliding&sort_m=count&sort_t=count&storage=hot&top_n=1000&top_o=top&viz=query_table&x_missing=true&from_ts=1780704000000&to_ts=1782000000000&live=true][View traffic]] |

Risks

| Risk | Mitigation |

|---------------------------------------------+---------------------------------------------------------------------|

| Typo in JSON key → wrong endpoint | Unknown names logged as warnings; validated against =_V1_URL_NAMES= |

| High-traffic endpoints (get_tag_group, get_promo-data) | Check Datadog links above; only enable after confirmed zero |

| 5-min DC cache = up to 5-min rollback lag | Same as existing doomsday flags; acceptable SLA |

| Unauthenticated callers get 410 pre-auth | Intentional — endpoint is gone. Check client handling first |