<?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: Drew-Opexcell</title>
    <description>The latest articles on DEV Community by Drew-Opexcell (@drewopexcell).</description>
    <link>https://dev.to/drewopexcell</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%2F4040762%2F889f52e7-eaf6-4ea5-b9d4-3e67454bdb9b.png</url>
      <title>DEV Community: Drew-Opexcell</title>
      <link>https://dev.to/drewopexcell</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/drewopexcell"/>
    <language>en</language>
    <item>
      <title>Why your Clio token stopped working</title>
      <dc:creator>Drew-Opexcell</dc:creator>
      <pubDate>Tue, 21 Jul 2026 21:21:50 +0000</pubDate>
      <link>https://dev.to/drewopexcell/why-your-clio-token-stopped-working-4ob8</link>
      <guid>https://dev.to/drewopexcell/why-your-clio-token-stopped-working-4ob8</guid>
      <description>&lt;p&gt;If your Clio integration started returning 401 and you are trying to work out why, the first question is not about your code. It is which of Clio's two OAuth systems you are on, because they have different rules and most advice on the internet does not say which one it is describing.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are two, and they are not interchangeable
&lt;/h2&gt;

&lt;p&gt;Clio Manage is the older one. OAuth at &lt;code&gt;app.clio.com/oauth&lt;/code&gt;, API at &lt;code&gt;app.clio.com/api/v4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Clio Platform is the newer one, covering Grow and the lead inbox among others. OAuth at &lt;code&gt;auth.api.clio.com/oauth&lt;/code&gt;, API at &lt;code&gt;api.clio.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They differ on essentially every point that matters when a token dies.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Manage&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access token lifetime&lt;/td&gt;
&lt;td&gt;2,592,000s, 30 days&lt;/td&gt;
&lt;td&gt;86,400s, 24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token expiry&lt;/td&gt;
&lt;td&gt;Documented as none&lt;/td&gt;
&lt;td&gt;No time expiry, but rotates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token rotation&lt;/td&gt;
&lt;td&gt;Not documented, and the refresh sample returns no new refresh token&lt;/td&gt;
&lt;td&gt;Documented: rotates on every use, previous one revoked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation endpoint&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST app.clio.com/oauth/deauthorize&lt;/code&gt;, Bearer auth&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST auth.api.clio.com/oauth/revoke&lt;/code&gt;, Basic auth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat the lifetime row as the documented default rather than a constant. Honour the &lt;code&gt;expires_in&lt;/code&gt; you get back on each response instead of hardcoding 30 days, because there are field reports of accounts issuing much shorter access tokens, and a hardcoded assumption fails in a way that looks exactly like revocation.&lt;/p&gt;

&lt;p&gt;That rotation row is the one that decides your debugging. On &lt;strong&gt;Platform&lt;/strong&gt;, every refresh gives you a new refresh token and kills the old one, so failing to persist the new value out of each response leaves you holding a dead token the next time you try. Clio's docs say it directly: store the new refresh token returned in each response.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;Manage&lt;/strong&gt;, the documented behaviour is a long-lived refresh token that does not expire and is not replaced. Their refresh response sample does not even include a &lt;code&gt;refresh_token&lt;/code&gt; field. So on Manage, a token that suddenly stops working usually points somewhere else: revocation, or the wrong region.&lt;/p&gt;

&lt;h2&gt;
  
  
  The region trap
&lt;/h2&gt;

&lt;p&gt;Clio runs four separate instances, and this is the one that turns a five minute fix into an afternoon because nothing about the error says "region."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Manage API&lt;/th&gt;
&lt;th&gt;Platform OAuth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app.clio.com/api/v4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auth.api.clio.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EU&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eu.app.clio.com/api/v4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eu.auth.api.clio.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ca.app.clio.com/api/v4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ca.auth.api.clio.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Australia&lt;/td&gt;
&lt;td&gt;&lt;code&gt;au.app.clio.com/api/v4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;au.auth.api.clio.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Clio's documentation states that a token issued in one region is not valid against another region's API. So if you built against &lt;code&gt;app.clio.com&lt;/code&gt; because that is what every tutorial uses, and you have just onboarded a firm on the Australian instance, nothing is wrong with the token. You are asking the wrong server.&lt;/p&gt;

&lt;p&gt;Do not stop at swapping the hostname, because it will fail again. Clio requires a separate developer application in each region, which means a separate &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; per region as well. Supporting a second region is a registration exercise, not a config change, and finding that out one failure at a time is the slow way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revocation is asymmetric, and it matters
&lt;/h2&gt;

&lt;p&gt;This is the detail most likely to explain a chain that died without anyone touching the code.&lt;/p&gt;

&lt;p&gt;Clio's Manage documentation draws a distinction that is easy to skim past: when &lt;strong&gt;the user&lt;/strong&gt; revokes access to your application from inside Clio, both access tokens and refresh tokens for that user are deauthorized. When &lt;strong&gt;your application&lt;/strong&gt; requests the deauthorization, only the supplied access token is deauthorized.&lt;/p&gt;

&lt;p&gt;So a firm administrator clicking "disconnect" in their Clio settings takes out your refresh token too, permanently, and your integrations will fail on their next call with nothing in your logs explaining why. Your own deauthorize call does something much narrower.&lt;/p&gt;

&lt;p&gt;Clio also offers a deauthorization callback URL you can set on the app, which POSTs &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;access_token&lt;/code&gt; when access is revoked, and sends the literal string &lt;code&gt;all&lt;/code&gt; for the access token when the user initiated it. If you run anything in production against Clio, set that callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no API path back
&lt;/h2&gt;

&lt;p&gt;Once a grant is revoked, the documented recovery is that the user re-authorizes through the browser. There is a deauthorize endpoint for ending access and nothing for restoring it.&lt;/p&gt;

&lt;p&gt;Recovery requires a specific human with access to the Clio account, in a browser, doing an OAuth flow. If that person is in a hearing, your integrations stay down until they are out. Two things worth having ready before you need them: a written re-authorization procedure that somebody other than the original developer can follow, including the exact scopes to request, because re-authorizing with fewer scopes brings half your integrations back in a subtler kind of broken; and alerting that fires on the token failure itself rather than on the downstream symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we run, and what it is for
&lt;/h2&gt;

&lt;p&gt;Several scheduled jobs sharing one stored Clio connection is a fragile shape whichever system you are on, because a rotating system punishes any two jobs that refresh at once, and every system punishes a stale cached token.&lt;/p&gt;

&lt;p&gt;Two measures, and they work together. Refresh proactively on a schedule so nothing is ever near expiry and on-demand refreshes almost never fire. And before any worker calls the token endpoint, have it re-read the stored token and stand down if a sibling already refreshed, using the persisted refresh token rather than whatever it cached at startup. That second detail matters more than it looks: a long-running process holding a token it read hours ago is exactly the thing that submits a stale one.&lt;/p&gt;

&lt;p&gt;If you want a hard guarantee rather than a small probability, a database advisory lock around the refresh gives you a single writer. We have not needed it on top of the two measures above.&lt;/p&gt;

&lt;p&gt;One diagnostic worth keeping: if &lt;strong&gt;every&lt;/strong&gt; integration fails at the same timestamp, that is a shared-credential problem, not five coincidences. It rules out a lot quickly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Lifetimes, rotation behaviour, region hosts and the revocation asymmetry above are from Clio's &lt;a href="https://docs.developers.clio.com/" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt; for Clio Manage and Clio Platform, checked July 2026. Clio does not publish the error body its token endpoint returns for an invalid refresh token, so this page does not guess at one. The refresh pattern in the last section is what we run in production. If Clio changes any of this, believe their docs over this page.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>security</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How to apply a Clio task template to a matter through the API</title>
      <dc:creator>Drew-Opexcell</dc:creator>
      <pubDate>Tue, 21 Jul 2026 21:21:21 +0000</pubDate>
      <link>https://dev.to/drewopexcell/how-to-apply-a-clio-task-template-to-a-matter-through-the-api-3ho0</link>
      <guid>https://dev.to/drewopexcell/how-to-apply-a-clio-task-template-to-a-matter-through-the-api-3ho0</guid>
      <description>&lt;p&gt;There are two versions of this job and they have different answers. Most of the confusion, including ours, comes from assuming they are the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying a whole template list to a matter
&lt;/h2&gt;

&lt;p&gt;Clio's interface has a button for this, and the natural assumption is that there is a matching endpoint on the task template resource. There is not. Nothing under &lt;code&gt;/task_template_lists&lt;/code&gt; will assign anything to a matter, and no path in Clio's spec contains "apply".&lt;/p&gt;

&lt;p&gt;It is done from the matter instead. Both &lt;code&gt;POST /matters.json&lt;/code&gt; and &lt;code&gt;PATCH /matters/{id}.json&lt;/code&gt; accept a nested array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;task_template_list_instances[]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;task_template_list &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="s"&gt;required on POST&lt;/span&gt;
  &lt;span class="na"&gt;assignee_id        &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;the user the list is assigned to&lt;/span&gt;
  &lt;span class="na"&gt;notify_assignees    &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;whether assignees get notified&lt;/span&gt;
  &lt;span class="na"&gt;due_at             : ISO-8601 date (format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date, not date-time)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are the only two operations in the API that accept it. Note the asymmetry: on &lt;code&gt;POST /matters.json&lt;/code&gt; the &lt;code&gt;task_template_list&lt;/code&gt; object is required, and on &lt;code&gt;PATCH /matters/{id}.json&lt;/code&gt; the item schema marks nothing as required at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /task_template_lists/{id}/copy.json&lt;/code&gt; exists and sounds like the thing you want. It is not. It duplicates a list into another &lt;em&gt;list&lt;/em&gt;, takes &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;practice_area&lt;/code&gt; (all optional), and has no matter parameter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two problems that will actually cost you time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Instances are write-only.&lt;/strong&gt; &lt;code&gt;task_template_list_instances&lt;/code&gt; appears in those two request bodies and nowhere in the matter response schema. So there is no documented way to ask "does this matter already have the list on it?" You verify by fetching &lt;code&gt;/tasks.json&lt;/code&gt; filtered to the matter and matching on names, which is uglier than it sounds and is most of the reconciliation work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which makes idempotency your problem.&lt;/strong&gt; If your automation fires the PATCH twice, nothing in the API stops you assigning the checklist twice. For anything triggered off matter creation or a stage change, decide up front how you detect an already-applied list, because the readback above is the only tool you have.&lt;/p&gt;

&lt;p&gt;Between them, those two are roughly half the build. Scope accordingly: with the trigger, the guard and the reconciliation this is a two to four day job. It looks like an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Due dates, and what &lt;code&gt;due_at&lt;/code&gt; actually seeds
&lt;/h2&gt;

&lt;p&gt;Task template due dates are a &lt;strong&gt;chain&lt;/strong&gt;, not a flat set of offsets. Each template carries &lt;code&gt;cascading&lt;/code&gt;, &lt;code&gt;cascading_source&lt;/code&gt; (the parent template it derives from), &lt;code&gt;cascading_offset&lt;/code&gt;, &lt;code&gt;cascading_offset_polarity&lt;/code&gt; (before or after the parent) and &lt;code&gt;cascading_offset_type&lt;/code&gt; (CalendarDays, CalendarWeeks and so on). Templates with &lt;code&gt;cascading: false&lt;/code&gt; carry no offset at all.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;due_at&lt;/code&gt; you pass on the instance seeds the root of that chain, and Clio resolves the rest.&lt;/p&gt;

&lt;p&gt;Worth being precise about the consequence, because it is easy to overstate: &lt;strong&gt;you do not lose the cascade by building tasks yourself.&lt;/strong&gt; &lt;code&gt;POST /tasks.json&lt;/code&gt; exposes the same five fields, including &lt;code&gt;cascading_source&lt;/code&gt;, described as the parent task used to determine the cascaded task's &lt;code&gt;due_at&lt;/code&gt;. So you can rebuild the chain manually by pointing each task at the one before it.&lt;/p&gt;

&lt;p&gt;The honest difference is effort and fidelity, not capability. Using the instance means Clio resolves the chain from one date you supply. Building by hand means you reconstruct the parent relationships yourself and keep them correct as the template list changes. That is a real reason to prefer the instance and it is not the same as "you cannot do it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating one task from one template
&lt;/h2&gt;

&lt;p&gt;Different job. Here you want a single task created from a single template, which is what you need when something in your own workflow decides a specific task is due.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /tasks.json&lt;/code&gt; has no template field of any kind. Its required fields are &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;assignee&lt;/code&gt;, so nothing is inherited for you. Read the template and pass the fields explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET  /api/v4/task_templates/{id}.json?fields=id,name,description,priority

POST /api/v4/tasks.json
{
  "data": {
    "matter":      { "id": &amp;lt;MATTER_ID&amp;gt; },
    "name":        "&amp;lt;template name&amp;gt;",
    "description": "&amp;lt;template description&amp;gt;",
    "priority":    "&amp;lt;template priority&amp;gt;",
    "assignee":    { "id": &amp;lt;USER_ID&amp;gt;, "type": "User" }
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth knowing before you build it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;description&lt;/code&gt; is required, and templates can have an empty one.&lt;/strong&gt; Copying an empty description straight across fails the call, so you need a fallback rather than passing through whatever the template holds. This one cost us a production run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assignee does not carry itself.&lt;/strong&gt; We resolve it from the matter's &lt;code&gt;responsible_staff&lt;/code&gt;, falling back to &lt;code&gt;responsible_attorney&lt;/code&gt;, and throw rather than guess when the matter has neither. Guessing is worse than failing loudly, because a task assigned to the wrong person looks done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you list templates, get the pagination right.&lt;/strong&gt; &lt;code&gt;/task_templates.json&lt;/code&gt; uses cursor paging and returns &lt;code&gt;meta.paging.next&lt;/code&gt; as a &lt;strong&gt;full URL&lt;/strong&gt;, not a token to append. Cursor paging also requires ordering by &lt;code&gt;order=id(asc)&lt;/code&gt;; without it you silently fall back to offset paging, which is capped at 10,000 records. Page size is 200, so this matters at real volume rather than on a single practice area's list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one you want
&lt;/h2&gt;

&lt;p&gt;If your trigger is "a matter of this type was created and should get its standard checklist," you want the list instance on the matter, passed at creation so the chain anchors correctly.&lt;/p&gt;

&lt;p&gt;If your trigger is something your own code decided, a document arrived, a stage changed, a deadline moved, you want the single-task path, because there is no list involved and you are creating one task on purpose.&lt;/p&gt;

&lt;p&gt;The expensive mistake is finding only the second path, concluding lists cannot be assigned through the API at all, and rebuilding every checklist task by task. That happens because the obvious place to look has nothing in it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Endpoint shapes, field names and required flags above are from Clio's published &lt;a href="https://docs.developers.clio.com/" rel="noopener noreferrer"&gt;API reference and OpenAPI spec&lt;/a&gt; for Clio Manage v4, checked July 2026. The empty-description failure and the assignee handling are from our own production integration. Clio does not publish the error string for the first of those, so it is reported here as our observation rather than as documentation. If Clio changes any of this, believe the reference over this page.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>backend</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
