<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ubaid Ullah</title>
    <description>The latest articles on DEV Community by Ubaid Ullah (@uaahacker).</description>
    <link>https://dev.to/uaahacker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3965395%2F91926c6a-d63e-4368-a15f-ddf654710ba3.png</url>
      <title>DEV Community: Ubaid Ullah</title>
      <link>https://dev.to/uaahacker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uaahacker"/>
    <language>en</language>
    <item>
      <title>JWT Authentication in Django: Fix 'Invalid Token' and Token Expired 401 Errors</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Sat, 10 Oct 2026 08:20:16 +0000</pubDate>
      <link>https://dev.to/uaahacker/jwt-authentication-in-django-fix-invalid-token-and-token-expired-401-errors-8io</link>
      <guid>https://dev.to/uaahacker/jwt-authentication-in-django-fix-invalid-token-and-token-expired-401-errors-8io</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://djangix.com/blog/jwt-authentication-django-invalid-token-expired/" rel="noopener noreferrer"&gt;djangix.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;JWT authentication in Django usually breaks in a very specific way: login succeeds, you copy the access token, and the very next request comes back with 401 Unauthorized. The response body is the whole diagnosis — if you read it before changing code, this is a ten-minute fix instead of an afternoon of guessing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq44nwdpt64jw9cnw1acx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq44nwdpt64jw9cnw1acx.jpg" alt="Developer looking at an API rate limit error notification on a monitor" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short answer is this: a Simple JWT 401 almost always means one of four things. The access token has expired, the wrong token (a refresh token) was sent to an API endpoint, the Authorization header never reached Django, or the token was signed with a different secret than the server now uses. Below is how to tell which one you have, and the correct fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the JWT authentication error actually says
&lt;/h2&gt;

&lt;p&gt;Django REST Framework with Simple JWT returns a JSON body, not just a status code. Read the &lt;code&gt;detail&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, and the per-message fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Given token not valid for any token type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"token_not_valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AccessToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Token is invalid or expired"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one body already narrows the problem down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;code token_not_valid, message Token is invalid or expired:&lt;/strong&gt; the token failed validation — it is expired, malformed, blacklisted, or signed with a different key. Check the steps below in order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;detail Authentication credentials were not provided:&lt;/strong&gt; this is a different bug. No usable Authorization header reached the view at all, so Django never even saw a token. Skip to the header check below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;detail Given token not valid for any token type, with a valid-looking token:&lt;/strong&gt; you very often sent the refresh token where an access token belongs. The two token types are not interchangeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token is blacklisted:&lt;/strong&gt; the refresh token was already rotated or logged out. The client must log in again — no server-side setting will resurrect that token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  JWT authentication settings that cause most 401 errors
&lt;/h2&gt;

&lt;p&gt;Most production 401s are configuration, not code. This is a known-good baseline for Simple JWT 5.5.1, the current release on PyPI at the time of writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rest_framework&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rest_framework_simplejwt.token_blacklist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;REST_FRAMEWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rest_framework_simplejwt.authentication.JWTAuthentication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_PERMISSION_CLASSES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rest_framework.permissions.IsAuthenticated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SIMPLE_JWT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACCESS_TOKEN_LIFETIME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REFRESH_TOKEN_LIFETIME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROTATE_REFRESH_TOKENS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLACKLIST_AFTER_ROTATION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTH_HEADER_TYPES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three settings in that block explain most surprises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short default access lifetime:&lt;/strong&gt; out of the box, Simple JWT access tokens live for five minutes and refresh tokens for one day. If your frontend never calls the refresh endpoint, every user appears to be logged out after five minutes. That is expiry working as designed, not a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing key follows SECRET_KEY:&lt;/strong&gt; by default tokens are signed with your Django SECRET_KEY. Regenerate or change SECRET_KEY in production and every existing token instantly becomes invalid. Users must log in again, and no code fix changes that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The blacklist app must be installed:&lt;/strong&gt; if you enable blacklist-after-rotation without adding the token_blacklist app and running migrations, rotation breaks. Install the app, migrate, then enable the setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wire the endpoints once, in your root URL configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# urls.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework_simplejwt.views&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TokenObtainPairView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TokenRefreshView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TokenVerifyView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api/token/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TokenObtainPairView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_obtain_pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api/token/refresh/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TokenRefreshView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api/token/verify/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TokenVerifyView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix JWT token expired errors with a real refresh flow
&lt;/h2&gt;

&lt;p&gt;Expired access tokens are normal. The bug is a client that treats expiry as a logout instead of refreshing once and retrying the original request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;makeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/token/refresh/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refreshResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// refresh is dead: log the user out&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;refreshResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;makeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the two details people miss. First, with rotation enabled the refresh response can contain a new refresh token — store it, or the next refresh will present a blacklisted token. Second, only retry on 401, and only once. Retrying in a loop turns a dead session into a request storm.&lt;/p&gt;

&lt;p&gt;Still not sure whether the token itself is the problem? Inspect it server-side before blaming the frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework_simplejwt.tokens&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AccessToken&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paste-the-access-token-here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# must be "access"
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;         &lt;span class="c1"&gt;# UTC epoch seconds: compare with your server clock
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If constructing the token raises an error, the token is malformed or signed with another key. If it parses but the exp value is in the past, it is simply expired and the refresh flow above is the fix. Also check server time: a container or VM with a skewed clock will reject fresh tokens as not yet valid or already expired.&lt;/p&gt;

&lt;p&gt;One related trap: a browser can drop the Authorization header before Django ever sees it. If your API and frontend are on different origins and preflight requests fail, treat it as a CORS problem first — see our guide to being &lt;a href="https://djangix.com/blog/blocked-by-cors-policy/" rel="noopener noreferrer"&gt;blocked by the CORS policy&lt;/a&gt;. And do not confuse this with cookie-session failures: header-based JWT auth does not need a CSRF token, which is a separate mechanism covered in our post on &lt;a href="https://djangix.com/blog/django-csrf-verification-failed/" rel="noopener noreferrer"&gt;Django CSRF verification failed&lt;/a&gt;. Verifying a credential before trusting it is the same discipline we apply to webhooks in our &lt;a href="https://djangix.com/blog/stripe-webhook-verification-django/" rel="noopener noreferrer"&gt;Stripe webhook verification guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong fixes for invalid token errors
&lt;/h2&gt;

&lt;p&gt;These appear in almost every forum thread, and each one trades a small bug for a bigger one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stretching the access token lifetime to 30 days:&lt;/strong&gt; the 401s stop, but a stolen access token now works for a month and cannot be revoked. Keep access tokens short and fix the refresh flow instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turning off expiry checks or adding a huge leeway:&lt;/strong&gt; a large LEEWAY setting hides clock problems for a while and quietly accepts tokens that should be dead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commenting out IsAuthenticated to make the error go away:&lt;/strong&gt; the endpoint now works for everyone, including people with no token at all. You have removed the lock, not fixed the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regenerating SECRET_KEY every deploy:&lt;/strong&gt; every deploy logs out every user and invalidates every token, because the signing key changed. Keep SECRET_KEY stable in your environment, and rotate it only as a deliberate, communicated event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending whichever token is at hand:&lt;/strong&gt; access and refresh tokens have different token_type claims and different jobs. An endpoint that expects an access token will correctly reject a refresh token every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final checklist for JWT authentication in Django
&lt;/h2&gt;

&lt;p&gt;Work through these in order and the 401 will name itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the response body first:&lt;/strong&gt; token_not_valid, credentials not provided, and blacklisted tokens are three different bugs with three different fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the token type:&lt;/strong&gt; decode the token and check that token_type is access for API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check expiry and server time:&lt;/strong&gt; compare the exp claim with a correct UTC clock on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the header arrives:&lt;/strong&gt; confirm the Authorization Bearer header reaches Django, through your proxy and CORS configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep SECRET_KEY stable:&lt;/strong&gt; if it changed, old tokens are invalid by design and users must log in again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement refresh properly:&lt;/strong&gt; refresh once on 401, store any rotated refresh token, retry the original request once, and log out cleanly if the refresh token is rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do that, and JWT authentication stops being a mystery error and becomes what it should be: a short-lived access token, a working refresh flow, and a 401 you can explain in one sentence.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>SaaS Product Development: From Idea to Launch, Step by Step</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:52:32 +0000</pubDate>
      <link>https://dev.to/uaahacker/saas-product-development-from-idea-to-launch-step-by-step-l34</link>
      <guid>https://dev.to/uaahacker/saas-product-development-from-idea-to-launch-step-by-step-l34</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/saas-product-development/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building a SaaS product is less a single build than a sequence of decisions. Doing them in the wrong order is what makes projects expensive, according to the original article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sequence, summarised
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate the problem first.&lt;/strong&gt; Talk to the people who would pay, and confirm the pain is frequent and costly enough to justify software.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define the smallest useful version.&lt;/strong&gt; Separate the core workflow that must work on day one from everything that can wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design around the workflow, not features.&lt;/strong&gt; Map the main user journey end to end before screens multiply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose boring, proven foundations.&lt;/strong&gt; Accounts, payments and data separation are not places to experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build in visible slices.&lt;/strong&gt; Working software shown regularly beats a long silent build with a big reveal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare for launch as its own step.&lt;/strong&gt; Billing checks, backups, monitoring and a support path need attention before users arrive, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the post-launch loop.&lt;/strong&gt; Early feedback, fixes and small improvements are part of development, not a separate phase.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The recurring theme: each stage reduces the risk of the next, and skipping early stages simply moves their cost later, with interest.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/saas-product-development/" rel="noopener noreferrer"&gt;SaaS Product Development: From Idea to Launch, Step by Step&lt;/a&gt; — with the complete step-by-step process and what happens at each stage.&lt;/p&gt;

</description>
      <category>saas</category>
    </item>
    <item>
      <title>API Integration Services: What They Include, What They Cost, and When You Actually Need Them</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:52:13 +0000</pubDate>
      <link>https://dev.to/uaahacker/api-integration-services-what-they-include-what-they-cost-and-when-you-actually-need-them-51m8</link>
      <guid>https://dev.to/uaahacker/api-integration-services-what-they-include-what-they-cost-and-when-you-actually-need-them-51m8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/api-integration-services/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If staff are copying information between systems by hand, you are already paying for an integration — in hours, errors and delays. The original article explains what a proper integration service actually covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is usually included
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery and mapping:&lt;/strong&gt; Which systems hold which data, what should move where, and in which direction it should flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building the connection:&lt;/strong&gt; Authentication, data transformation, and the scheduled or event-driven movement itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling and visibility:&lt;/strong&gt; Retries for temporary failures, alerts when something breaks, and logs a non-developer can check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and handover:&lt;/strong&gt; Checking edge cases such as duplicates and missing fields, plus documentation so the integration is not a mystery box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost and need, in brief
&lt;/h2&gt;

&lt;p&gt;The article frames cost around complexity — number of systems, quality of their documentation, and how critical real-time movement is. Simple, well-documented connections sit at one end; fragile or poorly documented systems at the other. You probably do not need a custom integration when a platform already offers a maintained built-in connector that does exactly what you need.&lt;/p&gt;

&lt;p&gt;A useful test: if the manual copying is frequent, error-prone and growing with the business, an integration usually pays for itself; if it is rare and stable, leave it alone.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/api-integration-services/" rel="noopener noreferrer"&gt;API Integration Services&lt;/a&gt; — with cost drivers, examples and the build-vs-built-in decision.&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Choose a SaaS Development Company: 9 Questions That Save You Money</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:51:50 +0000</pubDate>
      <link>https://dev.to/uaahacker/how-to-choose-a-saas-development-company-9-questions-that-save-you-money-2p5n</link>
      <guid>https://dev.to/uaahacker/how-to-choose-a-saas-development-company-9-questions-that-save-you-money-2p5n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/how-to-choose-a-saas-development-company/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Choosing the wrong builder for a SaaS product is expensive twice: you pay for the first build, then pay again for the rebuild. The original article turns the decision into questions you can ask before signing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The themes behind the nine questions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relevant proof:&lt;/strong&gt; Has the company shipped SaaS products — with subscriptions, accounts and ongoing users — or only brochure sites and demos?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who actually builds it:&lt;/strong&gt; Will the people you met do the work, and how is the project staffed if someone leaves?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How billing and accounts are handled:&lt;/strong&gt; SaaS-specific experience matters in areas like recurring payments, permissions and multi-user data separation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication and visibility:&lt;/strong&gt; How often will you see working software, and how are delays or scope changes surfaced?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership and exit:&lt;/strong&gt; Do you own the code, infrastructure accounts and documentation, so you are not locked in?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After launch:&lt;/strong&gt; What does support look like once real users arrive — fixes, monitoring and small improvements?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing clarity:&lt;/strong&gt; What is included, what is assumed, and what triggers extra cost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Score answers consistently across candidates, be wary of answers that stay vague when you ask for examples, and prefer a small paid discovery over a large leap of faith.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/how-to-choose-a-saas-development-company/" rel="noopener noreferrer"&gt;How to Choose a SaaS Development Company&lt;/a&gt; — with all nine questions and the red flags for each.&lt;/p&gt;

</description>
      <category>saas</category>
    </item>
    <item>
      <title>What Does an AI Automation Agency Actually Do? (Process, Projects, Costs)</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:51:27 +0000</pubDate>
      <link>https://dev.to/uaahacker/what-does-an-ai-automation-agency-actually-do-process-projects-costs-3j2b</link>
      <guid>https://dev.to/uaahacker/what-does-an-ai-automation-agency-actually-do-process-projects-costs-3j2b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/what-does-an-ai-automation-agency-do/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The label sounds grand, but the day-to-day work is practical: find a repetitive process, connect the tools involved, add AI only where it genuinely helps, and keep a person in charge of the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a typical engagement looks like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit first.&lt;/strong&gt; Map where time is actually lost, check the quality of the underlying data, and pick one workflow with a measurable payoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a scoped project.&lt;/strong&gt; Common examples include document handling, enquiry follow-up, internal search over company material, and reporting assembled from existing systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver with guardrails.&lt;/strong&gt; Outputs that affect customers or money start as drafts or recommendations for human approval, with logging so mistakes can be traced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support after launch.&lt;/strong&gt; Models, tools and processes change, so monitoring and small adjustments are part of the service, not an afterthought.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About cost and fit
&lt;/h2&gt;

&lt;p&gt;Pricing in the original article is framed around scope: a single workflow is a very different budget from a multi-system programme. The honest test is whether the time and error cost of the manual process clearly exceeds the cost of automating it. And sometimes the right answer is that you do not need an agency at all — a built-in feature of a tool you already pay for may cover the need.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/what-does-an-ai-automation-agency-do/" rel="noopener noreferrer"&gt;What Does an AI Automation Agency Actually Do?&lt;/a&gt; — with project examples, process detail and cost ranges.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Stripe API Integration for SaaS: Connect Payments Without the Usual Mistakes</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:51:06 +0000</pubDate>
      <link>https://dev.to/uaahacker/stripe-api-integration-for-saas-connect-payments-without-the-usual-mistakes-dc7</link>
      <guid>https://dev.to/uaahacker/stripe-api-integration-for-saas-connect-payments-without-the-usual-mistakes-dc7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/stripe-api-integration/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Adding payments to a SaaS product looks simple until the first edge case: a card fails after access was granted, a customer is charged twice, or a cancellation never reaches your database. The original article treats billing as a state problem, not just a checkout form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core sequence, in my summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model billing in your own data first.&lt;/strong&gt; Keep the customer and subscription identifiers and the current status on your side, so your product does not depend on a live call to decide who has access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a hosted checkout for the first version.&lt;/strong&gt; It handles card collection, common payment methods and compliance scope far better than a custom form built quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let server-side events drive access.&lt;/strong&gt; Payment and subscription changes should update your records from verified notifications arriving at your server — not from what the browser reports after redirect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle the awkward lifecycle events.&lt;/strong&gt; Failed renewals, cancellations, plan changes and refunds each need an explicit response in your access logic, tested in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make retries safe.&lt;/strong&gt; Repeating a create call after a timeout should not create a second charge — use the provider's idempotency mechanism and test that path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with simulated events before going live&lt;/strong&gt;, including the failure cases, using separate test and live credentials carefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;Granting access from an unverified browser redirect, burying billing failures where support cannot see them, and assuming the happy path covers renewals. Most billing bugs are lifecycle bugs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/stripe-api-integration/" rel="noopener noreferrer"&gt;Stripe API Integration for SaaS&lt;/a&gt; — with the full architecture, event list and common-mistakes checklist.&lt;/p&gt;

</description>
      <category>stripe</category>
    </item>
    <item>
      <title>AI Automation for Small Business: 7 Workflows Worth Automating First</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:50:45 +0000</pubDate>
      <link>https://dev.to/uaahacker/ai-automation-for-small-business-7-workflows-worth-automating-first-k4n</link>
      <guid>https://dev.to/uaahacker/ai-automation-for-small-business-7-workflows-worth-automating-first-k4n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/ai-automation-for-small-business/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A small business rarely needs a grand AI strategy first. It needs one repetitive workflow given back. The original article picks seven starting points and explains, for each, what it does and where human judgement should stay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven, summarised
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lead capture and follow-up&lt;/strong&gt; — record new enquiries automatically and make sure first responses and reminders do not depend on someone remembering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbox triage and reply drafts&lt;/strong&gt; — sort incoming mail by topic and urgency and draft answers for a person to approve, rather than sending unchecked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotes and invoices from job details&lt;/strong&gt; — turn the details you already collect into consistent documents, and chase late payment politely and automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviews and referrals&lt;/strong&gt; — ask at the right moment after a job and route unhappy responses to a human quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQ answers on your site&lt;/strong&gt; — a grounded assistant trained on your own material for the questions customers repeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports from data you already have&lt;/strong&gt; — regular summaries assembled from your existing systems instead of manual copying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data entry between systems&lt;/strong&gt; — move information you currently re-type from one tool to another.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Rules that keep it useful
&lt;/h2&gt;

&lt;p&gt;Start with work that is high-volume, repetitive and easy to check. Keep a human approving anything financial, sensitive or irreversible at first, measure the time actually saved, and expand only after the first workflow is trusted. Some work — nuanced complaints, judgement calls, relationships — should stay human.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/ai-automation-for-small-business/" rel="noopener noreferrer"&gt;AI Automation for Small Business: 7 Workflows Worth Automating First&lt;/a&gt; — with setup and cost notes for each workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Custom Software Development Companies: How to Compare Them Before You Hire</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:50:19 +0000</pubDate>
      <link>https://dev.to/uaahacker/custom-software-development-companies-how-to-compare-them-before-you-hire-fbm</link>
      <guid>https://dev.to/uaahacker/custom-software-development-companies-how-to-compare-them-before-you-hire-fbm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/custom-software-development-companies/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most vendors sound identical on a sales call: similar stacks, similar timelines, similar promises. The useful differences appear when you ask for evidence and look at the terms around the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to compare
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Match the portfolio to your problem.&lt;/strong&gt; A proof-of-concept or a demo is not the same as a shipped system handling real users. Ask whether the firm has actually delivered something like what you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin down code ownership and access from day one.&lt;/strong&gt; Who owns the code, the repository and the documentation? Can someone else take over if the relationship ends?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask how communication really works.&lt;/strong&gt; How often are demos, how fast are answers, and who is accountable when a deadline slips or a scope question appears?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make pricing comparable.&lt;/strong&gt; Is the quote fixed, time-based or staged? What is explicitly excluded, and what happens when assumptions change?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs worth naming
&lt;/h2&gt;

&lt;p&gt;A large agency can bring a broader bench but also more overhead and layers. A boutique team can be closer and faster, but capacity is narrower. In-house hiring gives control but is the slowest and most expensive path to assemble. None is automatically best — fit matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple process
&lt;/h2&gt;

&lt;p&gt;Score the same criteria for every candidate, run a short discovery before committing to a full build, verify claims with references or shipped work, and let that evidence drive the choice — not the polish of the proposal.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/custom-software-development-companies/" rel="noopener noreferrer"&gt;Custom Software Development Companies: How to Compare Them Before You Hire&lt;/a&gt; — with the full scorecard and final checklist.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>select_related vs prefetch_related: The Django N+1 Fix, Explained With Query Counts</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:49:57 +0000</pubDate>
      <link>https://dev.to/uaahacker/selectrelated-vs-prefetchrelated-the-django-n1-fix-explained-with-query-counts-514p</link>
      <guid>https://dev.to/uaahacker/selectrelated-vs-prefetchrelated-the-django-n1-fix-explained-with-query-counts-514p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/django-select-related-vs-prefetch-related/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Django page that feels instant with a handful of rows can crawl once the table grows. Often the cause is not the database itself, but how many separate trips the ORM makes to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The N+1 pattern, in plain terms
&lt;/h2&gt;

&lt;p&gt;Fetch a list in one query, then follow a related object for each row — and each follow-up is another query. Ten rows hide the problem; a thousand rows turn it into a thousand extra trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For a single-valued relationship&lt;/strong&gt; — such as the one author of an article — fetch the related row together with the main row in a joined query. One trip, and later attribute access is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For a collection&lt;/strong&gt; — such as all the comments or books belonging to each item — fetch the related rows in a separate batched query and match them in Python. Trying to join collections into the main query can multiply rows and repeat the parent data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That direction — single-valued versus collection — is the core decision rule in the original article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful variations the full article covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Following a chain of single-valued relationships in one call.&lt;/li&gt;
&lt;li&gt;Restricting a prefetch to only the rows you need, for example only visible comments, and storing that filtered result under a custom attribute.&lt;/li&gt;
&lt;li&gt;Prefetching through a relationship into a deeper related collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Habits that keep you honest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Count queries in tests and development rather than guessing — an assertion on query count catches regressions when a template starts touching a new relation.&lt;/li&gt;
&lt;li&gt;Watch what your templates and serializers actually access; the fix belongs where the access happens.&lt;/li&gt;
&lt;li&gt;Remember the limits: these helpers do not remove queries for later, separate querysets, and joining a collection can bloat results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short version: measure the query count, match the helper to the shape of the relationship, and re-check after you change it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/django-select-related-vs-prefetch-related/" rel="noopener noreferrer"&gt;select_related vs prefetch_related: The Django N+1 Fix&lt;/a&gt; — with worked examples and the query count at each step.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
    </item>
    <item>
      <title>Content Security Policy Blocking Your Scripts? Fix "Refused to Execute Inline Script" Properly</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:49:23 +0000</pubDate>
      <link>https://dev.to/uaahacker/content-security-policy-blocking-your-scripts-fix-refused-to-execute-inline-script-properly-hjn</link>
      <guid>https://dev.to/uaahacker/content-security-policy-blocking-your-scripts-fix-refused-to-execute-inline-script-properly-hjn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/content-security-policy-errors/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You add a Content Security Policy, deploy, and the site looks fine but nothing responds. That is not the policy failing — it is the policy working. Your own inline code simply is not allowed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the policy is doing
&lt;/h2&gt;

&lt;p&gt;CSP is a browser-enforced allowlist for scripts, styles, images and connections. Its main security value is against injected scripts, and injected code is usually inline — so a strict policy blocks inline code by default, including yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five fixes, in the order to try them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move inline code to external files.&lt;/strong&gt; This is the fix the policy is pushing you toward. A script file served from your own origin is already allowed by a self-only script rule, and it also gains caching and linting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a fresh nonce for code that must stay inline.&lt;/strong&gt; Some per-page configuration has to be rendered inline. Generate a random value for every response, put the same value in the policy and the tag, and never reuse it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash a script that never changes.&lt;/strong&gt; A fixed snippet can be allowed by its hash — but any edit, even formatting, changes the hash and blocks it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace inline handlers with listeners.&lt;/strong&gt; Attributes such as click handlers in markup are blocked, which is why pages can load while buttons do nothing. Move the behaviour into JavaScript and attach listeners there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name third parties explicitly.&lt;/strong&gt; Analytics, chat and embeds each need their own origins listed in the relevant directives. Do not use a wildcard — that also allows an attacker's origin.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The shortcut to avoid
&lt;/h2&gt;

&lt;p&gt;Allowing inline scripts generally makes the errors disappear by disabling the protection CSP exists to provide. If that is the fix, the policy is mostly decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer rollout
&lt;/h2&gt;

&lt;p&gt;Start in report-only mode: the browser enforces nothing but reports violations, so you can find and fix real usage before enforcing the same policy for users.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/content-security-policy-errors/" rel="noopener noreferrer"&gt;Content Security Policy Blocking Your Scripts?&lt;/a&gt; — including the Django nonce middleware example and the full ranked fixes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Streaming OpenAI API Responses: How It Works, in Python, Django &amp; FastAPI</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:48:36 +0000</pubDate>
      <link>https://dev.to/uaahacker/streaming-openai-api-responses-how-it-works-in-python-django-fastapi-30a5</link>
      <guid>https://dev.to/uaahacker/streaming-openai-api-responses-how-it-works-in-python-django-fastapi-30a5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://djangix.com/blog/streaming-openai-api-responses/" rel="noopener noreferrer"&gt;Djangix blog&lt;/a&gt;. This is a condensed version — the full article, with complete code, is linked at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A non-streamed chat answer leaves the user staring at a spinner for the whole generation. Streaming changes the feel, not the total time: the first tokens arrive in under a second and the answer appears to type itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually sent
&lt;/h2&gt;

&lt;p&gt;The transport is Server-Sent Events — one HTTP response that stays open and delivers small &lt;code&gt;data:&lt;/code&gt; messages. Each message carries a JSON chunk, and the useful text lives in a tiny delta field. Most chunks are fragments, not whole words, so the client simply concatenates them until a final done marker arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going through your own backend
&lt;/h2&gt;

&lt;p&gt;Browsers should not call the provider directly, because that would expose your API key. So your server receives the chunks and re-emits them — and that middle step is where many implementations break.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrap each token as JSON.&lt;/strong&gt; Raw token text can contain newlines, which break the event framing. Encoding the token inside a JSON object avoids mangled multi-line answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read with fetch, not EventSource, for POST chats.&lt;/strong&gt; The built-in event API cannot send a JSON body or an authorization header, so read the response stream manually and keep a buffer for events split across network chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for usage explicitly.&lt;/strong&gt; In the example in the full article, a stream option is needed if you want a final usage chunk for billing or budgeting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production pitfalls worth planning for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buffering proxies:&lt;/strong&gt; Reverse proxies and CDNs may hold the response and deliver it all at once, silently defeating streaming. Disable buffering for that route and test through the real production path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors after the stream starts:&lt;/strong&gt; Once the first chunk is sent, you cannot change the status code. Send an error as an event instead and render it in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disconnected users and saving:&lt;/strong&gt; Stop consuming when the client goes away, and save the assembled answer once at the end rather than writing to the database per token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle timeouts:&lt;/strong&gt; Periodic keepalive comments can keep a long generation from being cut off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to stream
&lt;/h2&gt;

&lt;p&gt;Short answers gain little, machine-consumed structured output usually needs to be complete before it can be parsed, and background jobs have no one watching. Streaming is a user-experience feature — use it where a person is waiting.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read the full article on Djangix:&lt;/strong&gt; &lt;a href="https://djangix.com/blog/streaming-openai-api-responses/" rel="noopener noreferrer"&gt;Streaming OpenAI API Responses: How It Works, in Python, Django &amp;amp; FastAPI&lt;/a&gt; — with the complete Python, Django &lt;code&gt;StreamingHttpResponse&lt;/code&gt; and FastAPI &lt;code&gt;StreamingResponse&lt;/code&gt; code.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>openai</category>
    </item>
    <item>
      <title>Building SafetyGraph: 546,891 Industrial Incident Records in One Queryable Dataset</title>
      <dc:creator>Ubaid Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:42:15 +0000</pubDate>
      <link>https://dev.to/uaahacker/building-safetygraph-546891-industrial-incident-records-in-one-queryable-dataset-1g4b</link>
      <guid>https://dev.to/uaahacker/building-safetygraph-546891-industrial-incident-records-in-one-queryable-dataset-1g4b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the Djangix blog: &lt;a href="https://djangix.com/blog/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-datase/" rel="noopener noreferrer"&gt;Building SafetyGraph: 546,891 Industrial Incident Records in One Queryable Dataset&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Public industrial safety records in the US are rich and, historically, painful to use: the same underlying information has been spread across separate agency systems, each with its own search form and export quirks, and much of it reachable only one lookup at a time. SafetyGraph, the project described in the full article, brings those records together into one queryable dataset of more than half a million incidents.&lt;/p&gt;

&lt;p&gt;The interesting engineering is less about scraping pages than about identity. Records describing the same company appear under spelling variants, punctuation differences, and changed names, so the pipeline normalises names, pairs likely matches conservatively, and builds explicit relationships between establishments, inspections, violations, and accidents instead of leaving users with a pile of disconnected rows.&lt;/p&gt;

&lt;p&gt;With that structure in place, questions that once meant hours of manual cross-referencing become a single query: show a company's history across sources, or follow the chain from an inspection to what it found and what happened next. The dataset is positioned for safety teams, researchers, journalists, and builders who need that history in a form software can actually use.&lt;/p&gt;

&lt;p&gt;The full article explains where the records come from, how the matching and linking work, what the resulting data model looks like, and what you can ask it on day one.&lt;/p&gt;

&lt;p&gt;Full article: &lt;a href="https://djangix.com/blog/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-datase/" rel="noopener noreferrer"&gt;Building SafetyGraph: 546,891 Industrial Incident Records in One Queryable Dataset&lt;/a&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>data</category>
      <category>database</category>
    </item>
  </channel>
</rss>
