<?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: Rob Askam</title>
    <description>The latest articles on DEV Community by Rob Askam (@contashare).</description>
    <link>https://dev.to/contashare</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%2F4242389%2Fb2eddb36-f280-48a8-85a3-ca8d4b54eabc.png</url>
      <title>DEV Community: Rob Askam</title>
      <link>https://dev.to/contashare</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/contashare"/>
    <language>en</language>
    <item>
      <title>Six decisions behind a webhook for personal data</title>
      <dc:creator>Rob Askam</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:18:32 +0000</pubDate>
      <link>https://dev.to/contashare/six-decisions-behind-a-webhook-for-personal-data-3c01</link>
      <guid>https://dev.to/contashare/six-decisions-behind-a-webhook-for-personal-data-3c01</guid>
      <description>&lt;p&gt;Most webhook advice is about reliability: retries, signatures, idempotency. All of it applies. But when the thing that changed is someone's home address or email, there is a second set of questions, about what happens when the webhook, the log it lands in or the account behind it falls into the wrong hands.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://contashare.com/businesses?utm_source=hashnode&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;ContaShare&lt;/a&gt;, where a person keeps their contact details in one account and chooses which companies can read them. When they change a detail, each company they share it with gets a webhook. Here are six decisions from building that, and what each one cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The webhook carries no personal data
&lt;/h2&gt;

&lt;p&gt;This is the whole payload:&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"e5aeb9bf-5676-41bc-9261-fc88920aaa56"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"resource_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://contashare.com/api/v1/address/e5aeb9bf-5676-41bc-9261-fc88920aaa56"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No address, no name, no customer ID. The receiver fetches &lt;code&gt;resource_url&lt;/code&gt; with that customer's OAuth token to get the new version.&lt;/p&gt;

&lt;p&gt;Putting the new address in the payload would save the receiver a request. But webhook bodies end up in places nobody audits: request logs, error trackers, a tunnelling tool's history, the retry queue of whatever sits in front of the app. A payload with nothing in it can be logged anywhere.&lt;/p&gt;

&lt;p&gt;It also means permission is checked at the moment of reading, not the moment of sending. If the customer stops sharing between the webhook and the fetch, the fetch returns &lt;code&gt;404&lt;/code&gt; and the company never sees the new address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; the receiver has to store which customer each record &lt;code&gt;id&lt;/code&gt; belongs to, and make a second request.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every webhook means "fetch the latest"
&lt;/h2&gt;

&lt;p&gt;A payload with values in it has an ordering problem. Two changes a minute apart, delivered out of order after a retry, leave the receiver holding the older one.&lt;/p&gt;

&lt;p&gt;A payload that only says "this record changed" doesn't. Two webhooks for the same record are identical, handling one twice is harmless, and whichever arrives last still fetches the current version. Receivers can fold queued duplicates into one fetch, and I tell them to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; you can't offer a history of changes through the webhook. For contact details nobody has asked for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Each company gets its own ID for the same person
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET /api/v1/me&lt;/code&gt; returns the ID a company knows a customer by. It never changes for that company, and every other company gets a different one for the same person.&lt;/p&gt;

&lt;p&gt;A single global user ID is easier to build and to debug. It is also a join key: any two companies holding it could match their customer lists against each other without asking anyone. Giving each company its own ID removes that option entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; support is harder. When a company quotes an ID at me, it only means something alongside which company is asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A change can wait an hour, on purpose
&lt;/h2&gt;

&lt;p&gt;This is the one that surprised me most while building it.&lt;/p&gt;

&lt;p&gt;A service that updates someone's email at every company at once is a very good target. Many sites reset a password by emailing the address on file. So if someone gets into a ContaShare account and changes the email, every connected company would be told the attacker's address, and the attacker then asks each one for a password reset.&lt;/p&gt;

&lt;p&gt;So a change to anything a company already holds waits an hour before it goes anywhere. The owner is emailed at once, at the old address too, with a link that cancels the change and locks the thief out. Until the hour is up there is no webhook and the API keeps returning the version the company already has.&lt;/p&gt;

&lt;p&gt;A change confirmed with an established passkey or two-factor, or with a link emailed to the owner, skips the wait. "Established" matters: a second factor added in the last 72 hours doesn't count, or the thief would add their own and carry on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; a real customer with no second factor waits an hour for their new address to arrive. I kept it short for that reason. It only needs to be long enough for the owner to see the warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The receiver is told not to trust the change either
&lt;/h2&gt;

&lt;p&gt;The hold reduces the risk and doesn't remove it. So the docs say it plainly: treat an &lt;code&gt;update&lt;/code&gt; as new contact details, not as proof of who someone is. Before a changed email or phone number is used to reset a password or send a login code, tell the old address and ask the customer to confirm in your own app.&lt;/p&gt;

&lt;p&gt;There is a webhook for the worst case too. If an account is reported stolen, it is locked, and each company gets &lt;code&gt;locked&lt;/code&gt; for the records it holds. Fetches return &lt;code&gt;423&lt;/code&gt; until &lt;code&gt;unlocked&lt;/code&gt; arrives. The advice on &lt;code&gt;locked&lt;/code&gt; is to treat anything received for that customer in the last 7 days as possibly not from them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; the integration guide is longer and less cheerful than a marketing page would like. I think that's the right way round.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Stopping sharing sends nothing
&lt;/h2&gt;

&lt;p&gt;When a customer stops sharing a record with a company, that company gets no webhook. Its next fetch of the record returns &lt;code&gt;404&lt;/code&gt;, the same as for a record that was deleted, and its token keeps working for anything else the customer still shares.&lt;/p&gt;

&lt;p&gt;A "revoked" webhook would be tidier for the receiver. But it would announce a decision the customer may not want announced, and it can't be something a company's endpoint is able to delay or fail. Stopping sharing takes effect straight away, with nothing on the other side to wait for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; receivers must handle &lt;code&gt;404&lt;/code&gt; as "remove this", which the guide repeats in three places.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordinary parts still matter
&lt;/h2&gt;

&lt;p&gt;For completeness, the reliability basics are all there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request has a &lt;code&gt;Signature&lt;/code&gt; header: the HMAC-SHA256 of the raw body with the team's secret. Check it against the raw body, with a constant-time comparison.&lt;/li&gt;
&lt;li&gt;A failed delivery is tried 3 times, 10 and then 100 seconds apart. After that it sits in a delivery history where the company can send it again once their endpoint is fixed.&lt;/li&gt;
&lt;li&gt;Webhooks are free and so are failed API calls. A company pays for a successful read: 1.25p for an address.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building the same thing
&lt;/h2&gt;

&lt;p&gt;Ask what your webhook is worth to someone who shouldn't have it, and what your API does for someone who took over an account. For most data the answer is "not much", and a full payload is fine. For anything another site would use to decide who someone is, the answers shape the design more than the retry policy does.&lt;/p&gt;

&lt;p&gt;If you want to see how it reads from the receiving side, the &lt;a href="https://contashare.com/help/webhooks" rel="noopener noreferrer"&gt;webhooks guide&lt;/a&gt; and &lt;a href="https://contashare.com/docs/api" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; are public, and there are packages for &lt;a href="https://github.com/contashare/contashare-socialite" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;, &lt;a href="https://github.com/contashare/contashare-node" rel="noopener noreferrer"&gt;Node&lt;/a&gt; and &lt;a href="https://github.com/contashare/contashare-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;. New company accounts get 1,000 free credits with no card: &lt;a href="https://contashare.com/businesses?utm_source=hashnode&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;contashare.com/businesses&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd welcome holes being picked in any of this, especially number 4.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>api</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep customer addresses current in Laravel with webhooks</title>
      <dc:creator>Rob Askam</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:17:25 +0000</pubDate>
      <link>https://dev.to/contashare/keep-customer-addresses-current-in-laravel-with-webhooks-4bhk</link>
      <guid>https://dev.to/contashare/keep-customer-addresses-current-in-laravel-with-webhooks-4bhk</guid>
      <description>&lt;p&gt;Every app that stores a customer's address has the same problem: the address was right on the day it was typed. People move, and your app is a long way down the list of who they tell.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://contashare.com/businesses?utm_source=hashnode&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;ContaShare&lt;/a&gt; to turn that round. A customer keeps their address, email, phone number and name in one account and ticks which of them your app may read. When they change one, your app gets a webhook and fetches the new version.&lt;/p&gt;

&lt;p&gt;This post adds it to a Laravel app: an OAuth sign-in button with Socialite, then a webhook that keeps the address current. It takes about half an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Laravel app with a &lt;code&gt;users&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;A ContaShare company account. It comes with 1,000 free credits and needs no card.&lt;/li&gt;
&lt;li&gt;An OAuth client, created under &lt;strong&gt;API Clients&lt;/strong&gt; in that account, with your callback URL as its redirect URI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install the driver
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require contashare/socialite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the client to &lt;code&gt;config/services.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'contashare'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'client_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CONTASHARE_CLIENT_ID'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'client_secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CONTASHARE_CLIENT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redirect'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CONTASHARE_REDIRECT_URI'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'webhook_secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CONTASHARE_WEBHOOK_SECRET'&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="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And give your users somewhere to keep the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare_id'&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;nullable&lt;/span&gt;&lt;span class="p"&gt;(&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;unique&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare_token'&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;nullable&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="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare_refresh_token'&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;nullable&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="p"&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;Cast both tokens as &lt;code&gt;encrypted&lt;/code&gt; on the model. They read a person's home address, so treat them like passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign up and sign in
&lt;/h2&gt;

&lt;p&gt;Two routes. The first sends the customer to ContaShare, where they see their records and tick the ones your app may read. The second handles them coming back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;\&lt;span class="nc"&gt;Models&lt;/span&gt;\&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Support&lt;/span&gt;\&lt;span class="nc"&gt;Facades&lt;/span&gt;\&lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Support&lt;/span&gt;\&lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel&lt;/span&gt;\&lt;span class="nc"&gt;Socialite&lt;/span&gt;\&lt;span class="nc"&gt;Facades&lt;/span&gt;\&lt;span class="nc"&gt;Socialite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/auth/contashare'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Socialite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare'&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;redirect&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="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/auth/contashare/callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&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="nv"&gt;$contashare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Socialite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$contashare&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&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="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&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="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="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// New customer: one call returns everything they shared.&lt;/span&gt;
        &lt;span class="nv"&gt;$shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$contashare&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sharedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'emails'&lt;/span&gt;&lt;span class="p"&gt;]&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="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sortByDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email_detail.is_verified'&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;first&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="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'names'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;abort_if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Share an email address to sign up.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Never sign someone in to an account that already has this email address.&lt;/span&gt;
        &lt;span class="nf"&gt;abort_if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_address'&lt;/span&gt;&lt;span class="p"&gt;]&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;exists&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="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Log in with your password instead.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'contashare_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&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="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'first_name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'last_name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email_address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Remember which records are this customer's. The webhook needs it.&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'addresses'&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;(&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;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'contashare_record_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'line_one'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'building_number'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_line_one'&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="s1"&gt;'city'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'postcode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'postcode'&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="p"&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="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forceFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'contashare_token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'contashare_refresh_token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;refreshToken&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&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="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&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;session&lt;/span&gt;&lt;span class="p"&gt;(&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;regenerate&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;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&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="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 things in there are worth a second look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getId()&lt;/code&gt; is your own ID for this person.&lt;/strong&gt; It comes from &lt;code&gt;GET /api/v1/me&lt;/code&gt;, which is free. It never changes, and another company gets a different ID for the same person, so two companies can't compare notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 409 matters.&lt;/strong&gt; If someone already registered with that email, don't sign them in or link the accounts, even when the email is verified. Otherwise every account in your app, an admin's included, is only as safe as whichever ContaShare account holds that address. Ask them to log in the usual way and connect from their settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store the record &lt;code&gt;id&lt;/code&gt;s.&lt;/strong&gt; The webhook in the next section tells you which record changed, not whose it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hear about changes
&lt;/h2&gt;

&lt;p&gt;Create a webhook under &lt;strong&gt;Webhooks&lt;/strong&gt; in your ContaShare team settings, and put the secret it shows you in &lt;code&gt;CONTASHARE_WEBHOOK_SECRET&lt;/code&gt;. It is shown once.&lt;/p&gt;

&lt;p&gt;The webhook is small on purpose. It carries no personal data, only what changed and where to fetch it:&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"e5aeb9bf-5676-41bc-9261-fc88920aaa56"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"resource_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://contashare.com/api/v1/address/e5aeb9bf-5676-41bc-9261-fc88920aaa56"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the signature against the raw body, queue the work and answer straight away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;\&lt;span class="nc"&gt;Jobs&lt;/span&gt;\&lt;span class="nc"&gt;SyncContaShareRecord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ContaShare&lt;/span&gt;\&lt;span class="nc"&gt;Socialite&lt;/span&gt;\&lt;span class="nc"&gt;WebhookSignature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Http&lt;/span&gt;\&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/webhooks/contashare'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;abort_unless&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebhookSignature&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&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="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Signature'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.contashare.webhook_secret'&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="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;SyncContaShareRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'action'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'resource_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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&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;noContent&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="p"&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;Exempt that URL from CSRF checks in &lt;code&gt;bootstrap/app.php&lt;/code&gt;. The signature does that job.&lt;/p&gt;

&lt;p&gt;The job fetches the record with the customer's own token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncContaShareRecord&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ShouldBeUniqueUntilProcessing&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$resourceUrl&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uniqueId&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="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&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="kt"&gt;void&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Address&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contashare_record_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&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="nv"&gt;$address&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'delete'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;delete&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;return&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'update'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;contashare_token&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;acceptJson&lt;/span&gt;&lt;span class="p"&gt;(&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;resourceUrl&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="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notFound&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="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Deleted, or the customer stopped sharing it.&lt;/span&gt;
            &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;delete&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;(&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;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'address_detail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'line_one'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'building_number'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address_line_one'&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="s1"&gt;'city'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'postcode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'postcode'&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ShouldBeUniqueUntilProcessing&lt;/code&gt; folds two webhooks for the same record into one fetch while the first is still queued. Every webhook means "fetch the latest", so handling one twice is harmless.&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Send test&lt;/strong&gt; on the Webhooks page to try your endpoint. The test uses a random &lt;code&gt;id&lt;/code&gt;, so your job finds no address and stops, which is what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to know before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 404 on fetch is a delete.&lt;/strong&gt; The customer removed the record or stopped sharing it with you. They can do that at any time, and stopping sharing sends no webhook of its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens last a year.&lt;/strong&gt; Refresh well before then with &lt;code&gt;Socialite::driver('contashare')-&amp;gt;refreshToken($refreshToken)&lt;/code&gt; and store the new pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A changed email is new contact details, not proof of identity.&lt;/strong&gt; Don't let it reset a password in your app without telling the old address first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A change can take up to an hour to arrive.&lt;/strong&gt; A change to something you already hold waits an hour unless the customer confirmed it with a passkey, two-factor or an emailed link. That gives the owner time to stop a change made from a stolen account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It covers customers who connect.&lt;/strong&gt; It won't clean a list of people who haven't, and it isn't a postcode lookup. Keep the postcode lookup on your form and use this for the years afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;You pay per successful API call with credits. The first call that returns everything a new customer shared is 1 credit (0.625p). Fetching an address is 2 credits (1.25p). &lt;code&gt;GET /api/v1/me&lt;/code&gt;, webhooks and failed calls are free, and there is no monthly fee.&lt;/p&gt;

&lt;p&gt;So an address costs you 1.25p when it changes, and nothing in the years it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/contashare/contashare-socialite" rel="noopener noreferrer"&gt;The driver on GitHub&lt;/a&gt;. There are &lt;a href="https://github.com/contashare/contashare-node" rel="noopener noreferrer"&gt;Node&lt;/a&gt; and &lt;a href="https://github.com/contashare/contashare-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt; packages too.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://contashare.com/help/oauth" rel="noopener noreferrer"&gt;OAuth guide&lt;/a&gt; and &lt;a href="https://contashare.com/help/webhooks" rel="noopener noreferrer"&gt;webhooks guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://contashare.com/docs/api" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://contashare.com/businesses?utm_source=hashnode&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;What it does for a business, with prices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd like to hear what's missing. If you try it and something in the flow is awkward, tell me in the comments.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>oauth</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Address lookup API pricing: what you are paying for</title>
      <dc:creator>Rob Askam</dc:creator>
      <pubDate>Thu, 12 Nov 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/contashare/address-lookup-api-pricing-what-you-are-paying-for-3nk3</link>
      <guid>https://dev.to/contashare/address-lookup-api-pricing-what-you-are-paying-for-3nk3</guid>
      <description>&lt;p&gt;"Address lookup" covers several different jobs, and they are priced differently because they cost different amounts to provide. Before comparing prices it helps to know which job you need doing. This guide explains the main kinds, how each is usually charged and how to work out what you will really spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three jobs that get called address lookup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Postcode lookup and address autocomplete.&lt;/strong&gt; A customer types a postcode or the start of an address into your form and picks theirs from a list. It makes forms quicker and stops typing mistakes. This is what most people mean by an address lookup API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Address validation and cleansing.&lt;/strong&gt; You already hold addresses and want to check they exist and are formatted correctly, often in bulk before a mailing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current customer address.&lt;/strong&gt; You want to know where a particular customer lives now, not whether an address exists. A postcode lookup cannot answer this. It tells you that 18 Harbour Street is a real address, and nothing about whether your customer still lives there.&lt;/p&gt;

&lt;p&gt;Most businesses need the first job at sign-up and the third for as long as the customer stays with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How postcode lookup is priced
&lt;/h2&gt;

&lt;p&gt;In the UK, postcode lookup services are mostly built on Royal Mail's Postcode Address File, which is licensed. Part of what you pay covers that licence, so the price has a floor however efficient the provider is.&lt;/p&gt;

&lt;p&gt;The usual models are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pay per lookup&lt;/strong&gt; , with credits bought in packs. The price per lookup falls as the pack gets bigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A monthly or yearly plan&lt;/strong&gt; with an allowance of lookups, and a charge for going over it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A licence per user or per site&lt;/strong&gt; for internal systems such as a call centre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check what counts as one lookup. Some services charge when the customer picks an address, some charge for each search as they type, and some charge again if the same customer comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to compare besides the price per lookup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum spend and expiry.&lt;/strong&gt; A cheap lookup in a pack that expires after a year costs more than it looks if you do not use it all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly fees&lt;/strong&gt; that apply whether or not you use the service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What a failed or empty search costs.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage.&lt;/strong&gt; Do you need addresses outside the UK, or properties that are newly built?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens afterwards.&lt;/strong&gt; The address was right on the day it was typed. How will you find out when it stops being right?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last question is usually missing from a price comparison, and it is where a lot of money goes: returned post, failed deliveries and staff time spent tracing customers who moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of checking again
&lt;/h2&gt;

&lt;p&gt;An address captured perfectly still goes out of date when the customer moves. The traditional answers are to screen your list against mover and suppression files, which is charged per record matched, or to ask customers to confirm their details, which most of them ignore. Our guide to &lt;a href="https://contashare.com/blog/keep-customer-addresses-up-to-date" rel="noopener noreferrer"&gt;keeping customer addresses up to date&lt;/a&gt; compares these.&lt;/p&gt;

&lt;p&gt;Either way you pay to check records that have not changed in order to find the few that have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paying only when something changes
&lt;/h2&gt;

&lt;p&gt;ContaShare works the other way round. Your customer keeps their own address, phone number, email and name up to date in a free ContaShare account and chooses to share them with you. You read the current version through an API, and a webhook tells you when they change something, so you fetch an address only when there is a new one.&lt;/p&gt;

&lt;p&gt;Reading an address costs 1.25p at list price, and less in larger credit packs. Webhooks and failed requests are free. There is no monthly fee and no minimum spend. It costs that little because there is no address file to license: the customer is the source.&lt;/p&gt;

&lt;p&gt;It is not a postcode lookup and does not replace one. It covers customers who have connected their ContaShare account to you, so it will not clean a list of people who have not. Coverage grows as you add the sign-in button to your site and send customers your invite link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working out your own numbers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Count the lookups you make at sign-up. That is your postcode lookup bill, and it depends on new customers, not on how many you have.&lt;/li&gt;
&lt;li&gt;Estimate how many customers move in a year, and what a wrong address costs you each time in post, redelivery and staff time.&lt;/li&gt;
&lt;li&gt;Compare that with the cost of hearing about each move when it happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For many businesses the cheapest set-up is both: a postcode lookup on the form, and a live connection for the years afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://contashare.com/businesses" rel="noopener noreferrer"&gt;Read how businesses use ContaShare&lt;/a&gt;, with current prices, or go straight to the &lt;a href="https://contashare.com/help/oauth" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt;. New business accounts get free credits to test with, and no card is needed.&lt;/p&gt;

</description>
      <category>forbusinesses</category>
    </item>
    <item>
      <title>How to keep customer addresses up to date</title>
      <dc:creator>Rob Askam</dc:creator>
      <pubDate>Thu, 12 Nov 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/contashare/how-to-keep-customer-addresses-up-to-date-2ni0</link>
      <guid>https://dev.to/contashare/how-to-keep-customer-addresses-up-to-date-2ni0</guid>
      <description>&lt;p&gt;Every customer database starts going out of date the day it is filled in. People move, change phone numbers and abandon email addresses, and most of them do not tell you. This guide looks at why that happens, what it costs and the ways to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why addresses go stale
&lt;/h2&gt;

&lt;p&gt;A customer who moves has dozens of organisations to tell. They start with the ones that matter most to them, such as their bank and the DVLA, and run out of energy long before they reach a retailer, a membership body or a utility they are leaving. Nobody decides not to tell you. You are simply far down a long list.&lt;/p&gt;

&lt;p&gt;The same goes for phone numbers and email addresses, with one difference: nothing bounces when a mobile number changes hands. Your texts just go to someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Returned and lost post.&lt;/strong&gt; Printing, postage and handling for letters that come back marked "gone away", and for those that never come back at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed deliveries&lt;/strong&gt; , redelivery fees and replacement goods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missed payments and renewals&lt;/strong&gt; , because the reminder went to the old address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer service time&lt;/strong&gt; spent tracing people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fraud and privacy risk.&lt;/strong&gt; A statement posted to a former address puts personal data in a stranger's hands. That can be a reportable data breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; UK data protection law requires personal data to be accurate and, where necessary, kept up to date. See &lt;a href="https://contashare.com/blog/uk-gdpr-accuracy-customer-contact-data" rel="noopener noreferrer"&gt;what the accuracy principle asks of you&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five ways to keep details current
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Ask customers to check their details
&lt;/h3&gt;

&lt;p&gt;Prompt at login, in the app and in regular emails: "Is this still your address?" It is cheap and should be standard. Its limit is that it depends on the customer acting, and it cannot reach someone whose email address has also changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Validate at the point of capture
&lt;/h3&gt;

&lt;p&gt;Postcode lookup and email and phone validation make sure details are well formed when they are typed in. This prevents typing errors. It does nothing once a correct address becomes an old one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Act on bounces and returns
&lt;/h3&gt;

&lt;p&gt;Treat every returned letter, hard bounce and failed delivery as a signal: flag the record, stop posting to it and ask the customer through another channel. This is essential hygiene, but it only tells you an address is wrong, not what the right one is, and only after you have paid to send something.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Screen against mover and suppression files
&lt;/h3&gt;

&lt;p&gt;Data suppliers sell files of people who have moved or died, built from sources such as redirection records. Screening a mailing list against them removes known gone-aways, and some services supply a new address. It works on your existing data without the customer doing anything. It is a periodic batch process, coverage is partial, and you need to be confident of your lawful basis for using a new address you were not given by the customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Let customers share live details with you
&lt;/h3&gt;

&lt;p&gt;Rather than holding a copy that ages, connect to a record the customer keeps current themselves. This is what ContaShare does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer keeps their address, phone number, email and name in a ContaShare account.&lt;/li&gt;
&lt;li&gt;They sign in to your service with ContaShare and tick which details you may read.&lt;/li&gt;
&lt;li&gt;You fetch those details through an API, and receive a signed webhook when one changes, so you only fetch again when there is something new.&lt;/li&gt;
&lt;li&gt;The customer can stop sharing at any time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its limit is the reverse of screening: it covers only customers who have connected their account, so it does not clean an existing mailing list. It grows as you invite customers, for example with a link in your emails or at checkout. For those customers the details are current, supplied by the person themselves, with their permission recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you use?
&lt;/h2&gt;

&lt;p&gt;Use more than one. Validate at capture, act on bounces, and prompt customers to review. Then choose between periodic screening, a live connection or both, depending on how much a wrong address costs you. Where a wrong address means a failed delivery, a missed renewal or a letter with personal data going astray, hearing about a move when it happens is worth more than finding out at the next mailing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;ContaShare has no monthly fee: you buy credits and pay per API request, and reading a customer's current address costs 1.25p. &lt;a href="https://contashare.com/businesses" rel="noopener noreferrer"&gt;Read how businesses use ContaShare&lt;/a&gt;, or see the &lt;a href="https://contashare.com/help/oauth" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt; for the technical detail.&lt;/p&gt;

</description>
      <category>forbusinesses</category>
    </item>
    <item>
      <title>UK GDPR accuracy principle: keeping contact data up to date</title>
      <dc:creator>Rob Askam</dc:creator>
      <pubDate>Thu, 12 Nov 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/contashare/uk-gdpr-accuracy-principle-keeping-contact-data-up-to-date-5b0n</link>
      <guid>https://dev.to/contashare/uk-gdpr-accuracy-principle-keeping-contact-data-up-to-date-5b0n</guid>
      <description>&lt;p&gt;The UK GDPR sets out seven principles for handling personal data. The fourth is accuracy: personal data must be accurate and, where necessary, kept up to date, and inaccurate data must be erased or corrected without delay. For most organisations the data this bites on first is the plainest: names, addresses, phone numbers and email addresses.&lt;/p&gt;

&lt;p&gt;This is a practical overview, not legal advice. The Information Commissioner's Office (ICO) publishes the authoritative guidance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the principle says
&lt;/h2&gt;

&lt;p&gt;In summary, you should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;take reasonable steps to make sure the personal data you hold is not incorrect or misleading&lt;/li&gt;
&lt;li&gt;keep it up to date where that is necessary for what you use it for&lt;/li&gt;
&lt;li&gt;correct or erase it promptly when you find it is wrong&lt;/li&gt;
&lt;li&gt;consider any challenge to its accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "Where necessary" depends on what the data is for
&lt;/h2&gt;

&lt;p&gt;You do not have to update everything for ever. The question is whether out-of-date data would matter for your purpose.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A record of where an order was delivered in 2023&lt;/strong&gt; is accurate as a historical record. It does not need changing when the customer moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The address you send statements to&lt;/strong&gt; is used today. If it is out of date, personal and financial information goes to the wrong household.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contact details used for ongoing communication nearly always fall in the second group.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as "reasonable steps"?
&lt;/h2&gt;

&lt;p&gt;It scales with the risk. The more harm an error could cause, the more you are expected to do. Reasonable steps for contact data commonly include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recording where the data came from&lt;/strong&gt; and when it was last confirmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Making it easy for people to update their own details&lt;/strong&gt; , online and without a phone call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompting people to review&lt;/strong&gt; their details periodically, and before anything sensitive is posted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acting on evidence that data is wrong:&lt;/strong&gt; returned post, bounced email, a failed delivery or a text answered by a stranger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not repeating the error:&lt;/strong&gt; when one system is corrected, the others that hold a copy are corrected too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing nothing until a customer complains is hard to defend where you are sending anything confidential.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right to rectification
&lt;/h2&gt;

&lt;p&gt;Individuals have the right to have inaccurate personal data corrected. When someone asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you must respond &lt;strong&gt;within one month&lt;/strong&gt; , which can be extended for complex requests&lt;/li&gt;
&lt;li&gt;you cannot normally charge a fee&lt;/li&gt;
&lt;li&gt;if you have shared the data with others, you should tell them about the correction where possible&lt;/li&gt;
&lt;li&gt;the request can be made verbally or in writing, to any part of your organisation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A change of address is the commonest rectification request there is, even though nobody calls it that. Your process for it should be quick and should reach every system.&lt;/p&gt;

&lt;h2&gt;
  
  
  When wrong contact data becomes a breach
&lt;/h2&gt;

&lt;p&gt;Sending personal data to an old address or a recycled phone number can be a personal data breach: an unauthorised disclosure. Depending on the risk to the person, you may need to report it to the ICO within 72 hours and tell the individual. A bank statement, a medical appointment letter or a payslip reaching the wrong household is exactly the kind of incident that gets reported.&lt;/p&gt;

&lt;p&gt;Keeping contact details accurate is therefore also part of keeping data secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to reduce the risk
&lt;/h2&gt;

&lt;p&gt;Our guide to &lt;a href="https://contashare.com/blog/keep-customer-addresses-up-to-date" rel="noopener noreferrer"&gt;keeping customer addresses up to date&lt;/a&gt; compares the options, from review prompts to mover files. In short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate details when they are captured.&lt;/li&gt;
&lt;li&gt;Give customers a simple way to change them.&lt;/li&gt;
&lt;li&gt;Treat bounces and returns as signals and act on them.&lt;/li&gt;
&lt;li&gt;Keep one source of truth, so a correction reaches every system.&lt;/li&gt;
&lt;li&gt;Where you can, get changes from the individual as they happen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where ContaShare fits
&lt;/h2&gt;

&lt;p&gt;ContaShare supports the fifth point. Your customers keep their own address, phone number, email and name current in a ContaShare account, and choose which of them your organisation may read. You read the current version through an API and receive a notification when something changes.&lt;/p&gt;

&lt;p&gt;For accuracy, that means the data comes from the individual, with a record of their permission, and corrections arrive without your asking. It covers the customers who connect their account, not your whole database, so it sits alongside the other steps and does not replace them. You remain the controller of the data you hold and responsible for your own compliance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://contashare.com/businesses" rel="noopener noreferrer"&gt;See how businesses use ContaShare&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>forbusinesses</category>
    </item>
  </channel>
</rss>
