<?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: choong</title>
    <description>The latest articles on DEV Community by choong (@choong-devsan).</description>
    <link>https://dev.to/choong-devsan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3826265%2Ffbeea2d1-1379-4aeb-8b81-7f0af31621b8.png</url>
      <title>DEV Community: choong</title>
      <link>https://dev.to/choong-devsan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/choong-devsan"/>
    <language>en</language>
    <item>
      <title>How to Build a Multi-Step HS Classification Workflow with 3 APIs</title>
      <dc:creator>choong</dc:creator>
      <pubDate>Fri, 27 Mar 2026 01:38:45 +0000</pubDate>
      <link>https://dev.to/choong-devsan/how-to-build-a-multi-step-hs-classification-workflow-with-3-apis-4hpb</link>
      <guid>https://dev.to/choong-devsan/how-to-build-a-multi-step-hs-classification-workflow-with-3-apis-4hpb</guid>
      <description>&lt;p&gt;A wrong HS code on a commercial invoice can mean your shipment gets held at customs, you get hit with unexpected duties, or you trigger a compliance flag that takes days to resolve. &lt;/p&gt;

&lt;p&gt;Most businesses still handle this manually. Someone Googles the product, cross-references a tariff schedule, makes their best guess, and types it in.&lt;/p&gt;

&lt;p&gt;It works, until it doesn't. &lt;/p&gt;

&lt;p&gt;And even when it does, a single misclassification on a high-value shipment is the kind of mistake that reminds you why experienced customs brokers exist.&lt;/p&gt;

&lt;p&gt;Here's how to build a workflow that classifies your products systematically, with a human review step built in for anything the system isn't confident about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Product data in (photo, spec, or text) → Gemini normalises it into a clean commodity term → HS Ping classifies it and returns the HS code → low-confidence results are flagged for human review → approved codes written back to your system.&lt;/p&gt;

&lt;p&gt;The multi-step part is deliberate. Full automation without a human review gate is how classification errors get baked silently into your operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  API #1 — Gemini (product normalisation)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt; is Google's multimodal AI API. &lt;/p&gt;

&lt;p&gt;It handles the messiest part of the workflow: turning whatever product data you have (example: a photo, a spec sheet excerpt, or a raw product title) into a clean 2–3 word commodity term that the &lt;a href="https://hsping.com" rel="noopener noreferrer"&gt;HS code lookup API, HS Ping&lt;/a&gt; can work with reliably.&lt;/p&gt;

&lt;p&gt;This is the step most HS classification workflows skip, and it's the reason they produce poor results. "Hand-painted 12oz ceramic mug with bamboo lid — gift box included" is not a useful input for a tariff lookup. "Ceramic mug" is.&lt;/p&gt;

&lt;p&gt;For a product photo or spec upload, you pass the image as base64:&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;POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent

Headers:
  x-goog-api-key: YOUR_API_KEY
  Content-Type: application/json

{
  "contents": [{
    "parts": [
      {
        "inline_data": {
          "mime_type": "image/jpeg",
          "data": "&amp;lt;base64_encoded_image&amp;gt;"
        }
      },
      {
        "text": "Identify the product in this image. Return only a short 2-3 word commodity term suitable for customs classification. No explanation."
      }
    ]
  }]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a plain text product title or spec, drop the &lt;code&gt;inline_data&lt;/code&gt; block and pass the description directly in the &lt;code&gt;text&lt;/code&gt; field. Same endpoint, same model.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tips: Be explicit in your prompt that you want only the commodity term returned — no explanation, no preamble. Gemini will otherwise return a full sentence. &lt;code&gt;gemini-3-flash-preview&lt;/code&gt; has a free tier, making it practical for prototyping without upfront cost.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API #2 — HS Ping (classification)
&lt;/h2&gt;

&lt;p&gt;HS Ping looks up the HS code for your cleaned commodity term against official tariff data, sourced directly official government sites. &lt;/p&gt;

&lt;p&gt;You pass the term and the destination country, and it returns the HS code, description, source publication, and version.&lt;/p&gt;

&lt;p&gt;For a single product:&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 https://api.hsping.com/api/v1/find?q=ceramic+mug&amp;amp;country=US

Headers:
  Authorization: Bearer YOUR_API_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For bulk classification like a product catalog or CSV import, you can use the batch endpoint instead:&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;POST https://api.hsping.com/api/v1/find

Headers:
  Authorization: Bearer YOUR_API_KEY
  Content-Type: application/json

[
  { "query": "ceramic mug", "country": "US" },
  { "query": "polycarbonate sheet", "country": "US" },
  { "query": "leather wallet", "country": "US" }
]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both endpoints return the same response structure per query item. &lt;/p&gt;

&lt;p&gt;The batch endpoint means you don't need to loop and fire individual requests for large catalogs, which keeps processing time and rate limit usage manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #3 — Your ERP or system of record (write-back)
&lt;/h2&gt;

&lt;p&gt;Once a code is approved (automatically or by a reviewer), you write it back to wherever your product data lives. The exact endpoint depends on your system.&lt;/p&gt;

&lt;p&gt;For a generic REST-based ERP or PIM:&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;PATCH https://your-erp.com/api/products/{product_id}

Headers:
  Authorization: Bearer YOUR_API_KEY
  Content-Type: application/json

{
  "hs_code": "6912000000",
  "hs_source": "HTSUS",
  "hs_classified_at": "2026-03-17T10:00:00Z"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your system doesn't have a write-back API, export the approved results as a CSV and import manually. &lt;/p&gt;

&lt;p&gt;Not glamorous, but it keeps the classification logic automated even if the final sync isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Everything Works Together
&lt;/h2&gt;

&lt;p&gt;Your app accepts the product input, calls Gemini to normalise it, calls HS Ping to classify it, evaluates the result against your confidence rules, and writes back approved codes. &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.amazonaws.com%2Fuploads%2Farticles%2Fuqw9fgah5ykk2lq4nelg.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fuqw9fgah5ykk2lq4nelg.png" alt=" " width="494" height="785"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flagged items wait in a review queue. &lt;/p&gt;

&lt;p&gt;Nothing gets written back without either a clean automated match or a human sign-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this doesn't cover
&lt;/h2&gt;

&lt;p&gt;Duty rate calculation and denied party screening are downstream steps this workflow doesn't solve. &lt;/p&gt;

&lt;p&gt;If you need a customs-ready commercial invoice at the end of the process rather than just classified product records, pairing this workflow with an invoice generation layer is the natural next step.&lt;/p&gt;

</description>
      <category>api</category>
    </item>
    <item>
      <title>4 APIs to Automate Expense Reporting: Receipt Scan Slack Approval Payout</title>
      <dc:creator>choong</dc:creator>
      <pubDate>Wed, 25 Mar 2026 02:18:29 +0000</pubDate>
      <link>https://dev.to/choong-devsan/4-apis-to-automate-expense-reporting-receipt-scan-slack-approval-payout-2n89</link>
      <guid>https://dev.to/choong-devsan/4-apis-to-automate-expense-reporting-receipt-scan-slack-approval-payout-2n89</guid>
      <description>&lt;p&gt;Every finance manager has a folder they don't talk about. &lt;/p&gt;

&lt;p&gt;A quiet accumulation of receipt photos, Slack forwards, and email attachments with subjects like "pls reimburse ty." It grows a little every week, an it never shrinks on its own.&lt;/p&gt;

&lt;p&gt;Here's how to stop feeding that folder, with a scan to bank transfer automated workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Employee submits a receipt → Veryfi extracts the data → Slack sends the manager an approve/reject message → QuickBooks logs the expense → Wise transfers the reimbursement.&lt;/p&gt;




&lt;h2&gt;
  
  
  API #1 — Veryfi (receipt OCR)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.veryfi.com/" rel="noopener noreferrer"&gt;Veryfi&lt;/a&gt; takes a receipt image or PDF and returns structured JSON in seconds: vendor name, date, total amount, line items, currency, and tax. &lt;/p&gt;

&lt;p&gt;It's pre-trained on hundreds of millions of receipts and works across 38 languages and 91 currencies. &lt;/p&gt;

&lt;p&gt;No model training required on your end.&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;POST https://api.veryfi.com/api/v8/partner/documents/

Headers:
  CLIENT-ID: your_client_id
  AUTHORIZATION: apikey your_username:your_api_key
  Content-Type: application/json

{
  "file_url": "https://your-storage.com/receipts/receipt-001.jpg",
  "tags": ["expense", "reimbursement"],
  "categories": ["Meals", "Travel", "Office Supplies"]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;vendor.name&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;currency_code&lt;/code&gt;, and a full &lt;code&gt;line_items&lt;/code&gt; array. All of that feeds into the Slack message in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #2 — Slack (manager approval)
&lt;/h2&gt;

&lt;p&gt;Rather than routing approvals through email, you send the manager an interactive Slack message using Block Kit - Slack's UI framework for composing messages with buttons and structured data. &lt;/p&gt;

&lt;p&gt;The manager sees the receipt details and clicks Approve or Reject directly in Slack.&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;POST https://slack.com/api/chat.postMessage

Headers:
  Authorization: Bearer xoxb-your-bot-token
  Content-Type: application/json

{
  "channel": "manager-slack-user-id",
  "text": "New expense reimbursement request",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*New expense request*\n*Employee:* John Doe\n*Vendor:* Marriott\n*Amount:* USD 189.00\n*Date:* 2025-03-10"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": { "type": "plain_text", "text": "Approve" },
          "style": "primary",
          "action_id": "approve_expense",
          "value": "expense_id_123"
        },
        {
          "type": "button",
          "text": { "type": "plain_text", "text": "Reject" },
          "style": "danger",
          "action_id": "reject_expense",
          "value": "expense_id_123"
        }
      ]
    }
  ]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the manager clicks a button, Slack sends an interaction payload to your app's endpoint with the &lt;code&gt;action_id&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt;. You use that to trigger the next step.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tips: You'll need to enable &lt;a href="https://docs.slack.dev/messaging/creating-interactive-messages/" rel="noopener noreferrer"&gt;Interactivity&lt;/a&gt; in your Slack app settings and provide a Request URL — that's the endpoint your app exposes to receive button click payloads. Test this locally first using a tunnel like ngrok before deploying.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API #3 — QuickBooks (accounting sync)
&lt;/h2&gt;

&lt;p&gt;Once approved, you log the expense in &lt;a href="https://quickbooks.intuit.com/" rel="noopener noreferrer"&gt;QuickBooks Online&lt;/a&gt; via the Purchase endpoint. This creates a record in the books against the right account and employee.&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;POST https://quickbooks.api.intuit.com/v3/company/{realmId}/purchase

Headers:
  Authorization: Bearer your_oauth2_access_token
  Content-Type: application/json
  Accept: application/json

{
  "PaymentType": "Cash",
  "AccountRef": { "value": "35", "name": "Checking" },
  "EntityRef": { "value": "employee_id", "type": "Employee" },
  "TotalAmt": 189.00,
  "Line": [
    {
      "Amount": 189.00,
      "DetailType": "AccountBasedExpenseLineDetail",
      "AccountBasedExpenseLineDetail": {
        "AccountRef": { "value": "7", "name": "Travel" }
      }
    }
  ]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  API #4 — Wise (reimbursement payout)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wise.com" rel="noopener noreferrer"&gt;Wise&lt;/a&gt; handles the actual bank transfer to the employee. &lt;/p&gt;

&lt;p&gt;It's API-first, uses the mid-market exchange rate with a flat transparent fee, and works for both domestic and international payouts across 40+ currencies.&lt;/p&gt;

&lt;p&gt;The payout flow is three calls: create a quote, create the transfer, then fund it.&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;# Step 1 — Create a quote
POST https://api.wise.com/v3/profiles/{profileId}/quotes

{
  "sourceCurrency": "USD",
  "targetCurrency": "USD",
  "sourceAmount": 189.00,
  "targetAccount": "&amp;lt;recipient_account_id&amp;gt;"
}

# Step 2 — Create the transfer
POST https://api.wise.com/v1/transfers

{
  "targetAccount": "&amp;lt;recipient_account_id&amp;gt;",
  "quoteUuid": "&amp;lt;quote_id_from_step_1&amp;gt;",
  "customerTransactionId": "expense_id_123",
  "details": { "reference": "Expense reimbursement — March 2025" }
}

# Step 3 — Fund the transfer
POST https://api.wise.com/v3/profiles/{profileId}/transfers/{transferId}/payments

{
  "type": "BALANCE"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Piecing things together
&lt;/h2&gt;

&lt;p&gt;Your app receives the receipt, calls Veryfi to extract the data, formats it into a Slack Block Kit message, and waits for the manager's response. &lt;/p&gt;

&lt;p&gt;On approval, it logs the expense in QuickBooks and triggers the Wise payout. &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.amazonaws.com%2Fuploads%2Farticles%2F5tus8vnf5riw6z0b3ise.png" 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.amazonaws.com%2Fuploads%2Farticles%2F5tus8vnf5riw6z0b3ise.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole chain from submission to transfer initiation runs without anyone opening a spreadsheet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final note: What this doesn't cover
&lt;/h2&gt;

&lt;p&gt;Receipt policy enforcement: flagging expenses that exceed per-category limits, and multi-level approval chains are not covered here. &lt;/p&gt;

&lt;p&gt;Those are logic layers you can add on top of the Slack interaction step, no additional APIs needed.&lt;/p&gt;

</description>
      <category>api</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Automate Your HR Recruitment Pipeline with These 4 APIs</title>
      <dc:creator>choong</dc:creator>
      <pubDate>Mon, 23 Mar 2026 14:05:36 +0000</pubDate>
      <link>https://dev.to/choong-devsan/automate-your-hr-recruitment-pipeline-with-these-4-apis-1178</link>
      <guid>https://dev.to/choong-devsan/automate-your-hr-recruitment-pipeline-with-these-4-apis-1178</guid>
      <description>&lt;p&gt;Most HR recruitment pipelines share the same bottleneck, it goes something like this:&lt;/p&gt;

&lt;p&gt;The recruiter manually opens each application, downloads the resume, reads through it, copies details into a spreadsheet, emails the candidate to book a call, and chases the background check days later. &lt;/p&gt;

&lt;p&gt;It's fine when you're hiring once a quarter. &lt;/p&gt;

&lt;p&gt;When you're running multiple roles at once, it falls apart - fast.&lt;/p&gt;

&lt;p&gt;Here's how to automate the screening half of that pipeline: from application received to interview scheduled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Candidate applies via Greenhouse → resume parsed automatically by APILayer → background check triggered via Checkr → interview booked via SavvyCal.&lt;/p&gt;

&lt;p&gt;The recruiter's job goes from processing paperwork to reviewing a structured shortlist.&lt;/p&gt;




&lt;h2&gt;
  
  
  API #1 — Greenhouse (your system of record)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.greenhouse.com" rel="noopener noreferrer"&gt;Greenhouse&lt;/a&gt; is where candidates apply and where their records live throughout the hiring process. &lt;/p&gt;

&lt;p&gt;Rather than polling for new applications, you subscribe to &lt;a href="https://developers.greenhouse.io/webhooks.html" rel="noopener noreferrer"&gt;the &lt;code&gt;application_submitted&lt;/code&gt; webhook topic&lt;/a&gt;, which fires the moment a candidate completes their application.&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;POST https://{your-domain}.greenhouse.io/webhooks

{
  "webhook": {
    "name": "New application trigger",
    "url": "https://your-app.com/webhooks/application",
    "event": "application_submitted",
    "secret_key": "your_secret_key"
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload includes the candidate's name, email, application ID, and a URL to their attached resume. &lt;/p&gt;

&lt;p&gt;That resume URL is what feeds into the next step.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tips: Greenhouse's Harvest API v1 and v2 are being deprecated after August 2026 — build against v3 from the start. API keys are managed under Configure → Dev Center → API Credential Management, and permissions are scoped per endpoint.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API #2 — APILayer Resume Parser (resume parsing)
&lt;/h2&gt;

&lt;p&gt;APILayer's &lt;a href="https://marketplace.apilayer.com/resume_parser-api" rel="noopener noreferrer"&gt;Resume Parser&lt;/a&gt; takes a resume in PDF or Word format and returns structured JSON: name, email, skills, work history, education. &lt;/p&gt;

&lt;p&gt;Free tier is available with no credit card required, paid plans start at $9.99/month for higher volumes.&lt;/p&gt;

&lt;p&gt;You pass the resume URL from the Greenhouse webhook payload directly:&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;POST https://api.apilayer.com/resume_parser/url

Headers:
  apikey: YOUR_API_KEY

{
  "url": "&amp;lt;resume_url_from_greenhouse_payload&amp;gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you structured candidate data you can write back to Greenhouse or use to score the application against your job requirements.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tips: The parser handles PDF and DOCX well but can struggle with heavily designed or image-based resumes. Plain text CVs get the most accurate results.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API #3 — Checkr (background check)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://checkr.com/" rel="noopener noreferrer"&gt;Checkr&lt;/a&gt; is an API-first background check platform. &lt;/p&gt;

&lt;p&gt;You trigger a check programmatically once a candidate clears the initial resume screen, without follow-up emails, or chasing providers.&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;POST https://api.checkr.com/v1/invitations

{
  "package": "tasker_standard",
  "candidate_id": "&amp;lt;checkr_candidate_id&amp;gt;",
  "work_locations": [{ "country": "US" }]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checkr sends the candidate an email to complete their consent form. When the check completes, a webhook fires back to your app with the result, which you can use to gate the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #4 — SavvyCal (interview scheduling)
&lt;/h2&gt;

&lt;p&gt;Once the background check clears, you automatically send the candidate an interview slot. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://savvycal.com/" rel="noopener noreferrer"&gt;SavvyCal&lt;/a&gt; has a clean REST API that lets you query available slots and book one programmatically. &lt;/p&gt;

&lt;p&gt;It's a two-call pattern. First, get available slots for the interviewer's scheduling link:&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 https://savvycal.com/api/v1/scheduling_links/{link_slug}/slots

Headers:
  Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then book the first available slot on the candidate's behalf:&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;POST https://savvycal.com/api/v1/events

{
  "scheduling_link_slug": "&amp;lt;link_slug&amp;gt;",
  "start_at": "&amp;lt;slot_start_at_from_previous_call&amp;gt;",
  "attendees": [
    {
      "name": "&amp;lt;candidate_name&amp;gt;",
      "email": "&amp;lt;candidate_email&amp;gt;"
    }
  ]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends a calendar invite to the candidate, while the interviewer's calendar updates automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Everything Works Together
&lt;/h2&gt;

&lt;p&gt;Your app listens on a webhook endpoint. Greenhouse fires when an application comes in, you parse the resume, trigger the background check, and — once it clears — automatically book the interview. &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.amazonaws.com%2Fuploads%2Farticles%2Fr3lifef7eyy1dxqw65e3.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fr3lifef7eyy1dxqw65e3.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recruiter steps in at the review stage with a structured candidate profile already waiting in Greenhouse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compare to doing this manually in Greenhoue
&lt;/h2&gt;

&lt;p&gt;Greenhouse does have built-in stages and scorecards for managing candidates. &lt;/p&gt;

&lt;p&gt;However, it doesn't automatically move candidate between stages. From "applied" to "screened" to "interview scheduled", you still require someone to manually trigger each step. &lt;/p&gt;

&lt;p&gt;At low volume that's fine. When you're handling dozens of applications a week across multiple roles, the manual overhead adds up quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final note: What this doesn't cover
&lt;/h2&gt;

&lt;p&gt;This workflow handles the screening pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application in&lt;/li&gt;
&lt;li&gt;Interview scheduled. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interview itself, feedback collection, and offer letter are outside the scope here. &lt;/p&gt;

&lt;p&gt;If you want to close the loop on the offer side, pairing this with a transactional email API like &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; to programmatically send offer letters is a natural next step.&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>4 APIs to Build an Export-Ready Invoice Generation Workflow in Xero</title>
      <dc:creator>choong</dc:creator>
      <pubDate>Wed, 18 Mar 2026 14:03:17 +0000</pubDate>
      <link>https://dev.to/choong-devsan/4-apis-to-build-an-export-ready-invoice-generation-workflow-in-xero-31h2</link>
      <guid>https://dev.to/choong-devsan/4-apis-to-build-an-export-ready-invoice-generation-workflow-in-xero-31h2</guid>
      <description>&lt;p&gt;The average invoicing softwares are designed for domestic billing. &lt;/p&gt;

&lt;p&gt;While they handle tax, formatting, and payment terms well; they lack support for cross border trade functions like custom compliance: no HS code lookup, no destination country logic, no audit trail for where the code came from.&lt;/p&gt;

&lt;p&gt;You end up doing these part manually every time.&lt;/p&gt;

&lt;p&gt;Here's how to automate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Pull product and order data from Xero → classify each product line with HS Ping → write the HS codes back into the Xero invoice.&lt;/p&gt;




&lt;h2&gt;
  
  
  API #1 — Xero (your system of record)
&lt;/h2&gt;

&lt;p&gt;Xero is where your order and product data lives. &lt;/p&gt;

&lt;p&gt;You're pulling two things: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the line items on a draft invoice (product description, quantity, unit price) &lt;/li&gt;
&lt;li&gt;the buyer's details.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.xero.com/api.xro/2.0/Invoices/{InvoiceID}

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you the full invoice object including &lt;code&gt;LineItems&lt;/code&gt;, each with a &lt;code&gt;Description&lt;/code&gt; field. &lt;/p&gt;

&lt;p&gt;That description is what you'll feed into HS Ping.&lt;/p&gt;

&lt;p&gt;Tips: You'll need OAuth 2.0 with the &lt;code&gt;accounting.transactions&lt;/code&gt; scope. Xero has a sandbox environment with the rate limits of 5,000 calls/day per org.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #2 — HS Ping (HS classification)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://hsping.com" rel="noopener noreferrer"&gt;HS Ping&lt;/a&gt; looks up the correct HS code for a product term against official tariff data, sourced directly from official government sources.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/find&lt;/code&gt; endpoint works best with a concise commodity term rather than a full product title. &lt;/p&gt;

&lt;p&gt;"Ceramic mug" works well. &lt;/p&gt;

&lt;p&gt;"Hand-painted 12oz artisan mug with gift box" will likely return nothing useful.&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 https://api.hsping.com/api/v1/find?q=ceramic+mug&amp;amp;country=US
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns the HS code, description, source, and version, everything you need to cite on a compliant commercial invoice.&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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;"hscode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6912000000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description_en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ceramic tableware, kitchenware..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTSUS"&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="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;&lt;em&gt;Tips: This is where a lightweight LLM call earns its keep. Pass your full Xero line item description in, get a clean 2–3 word commodity term out, then feed that into HS Ping. It removes the last manual step without overcomplicating the stack.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API #3 — Xero again (write HS code back to invoice)
&lt;/h2&gt;

&lt;p&gt;Once you have the HS code, you update each line item on the draft invoice in Xero. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Description&lt;/code&gt; field is the natural place to append the code. Most commercial invoice formats expect it there or in an adjacent field.&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;POST https://api.xero.com/api.xro/2.0/Invoices/{InvoiceID}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"LineItems"&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;"LineItemID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;existing_line_item_id&amp;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;"Description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ceramic mug — HS 6912.00.0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"UnitAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.50&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;Once updated, you can set the invoice status to &lt;code&gt;SUBMITTED&lt;/code&gt; or &lt;code&gt;APPROVED&lt;/code&gt; to lock it before delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting Everything Together
&lt;/h2&gt;

&lt;p&gt;Your app fetches the draft invoice from Xero, calls HS Ping per line item to retrieve the HS code, then writes it back to the Xero invoice.&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.amazonaws.com%2Fuploads%2Farticles%2Fqohm97f4uegphsyjfakw.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fqohm97f4uegphsyjfakw.png" alt=" " width="693" height="822"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Note
&lt;/h2&gt;

&lt;p&gt;Xero handles the invoice, HS Ping handles the classification; but duty calculation, denied party screening, and filing the actual customs declaration are downstream problems this workflow doesn't solve. &lt;/p&gt;

&lt;p&gt;With the completed invoice ready, your freight forwarder can easily take it from here.&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>Build an Automated Order Fulfilment Workflow in Shopify with These 3 APIs</title>
      <dc:creator>choong</dc:creator>
      <pubDate>Mon, 16 Mar 2026 05:48:43 +0000</pubDate>
      <link>https://dev.to/choong-devsan/build-an-automated-order-fulfilment-workflow-in-shopify-with-these-3-apis-4iln</link>
      <guid>https://dev.to/choong-devsan/build-an-automated-order-fulfilment-workflow-in-shopify-with-these-3-apis-4iln</guid>
      <description>&lt;p&gt;(Fulfilment? Fulfillment?)&lt;/p&gt;

&lt;p&gt;Anyways, since I started with Shopify clients, I have observed many of them fulfill orders the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They log into the dashboard&lt;/li&gt;
&lt;li&gt;Check the order&lt;/li&gt;
&lt;li&gt;Manually copy the address into a separate shipping portal like Shippo&lt;/li&gt;
&lt;li&gt;Generate a shipping label&lt;/li&gt;
&lt;li&gt;Download it&lt;/li&gt;
&lt;li&gt;Print it&lt;/li&gt;
&lt;li&gt;Finally, manually send a confirmation email to the customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat for every order.&lt;/p&gt;

&lt;p&gt;It works, until it doesn't. When you're doing 50 orders a day, one person can't keep up, and mistakes start to cost you real money.&lt;/p&gt;

&lt;p&gt;Here's how you can automate the whole thing with 3 APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Order placed on Shopify → Shippo generates the shipping label automatically → Resend sends the customer a confirmation email with tracking info.&lt;/p&gt;

&lt;p&gt;Log in once or twice a day to review your dashboard. Everything else has already been done for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  API #1 — Shopify (your system of record)
&lt;/h2&gt;

&lt;p&gt;Shopify is where the order lives. &lt;/p&gt;

&lt;p&gt;Rather than polling for new orders, you subscribe to &lt;a href="https://shopify.dev/docs/apps/build/webhooks" rel="noopener noreferrer"&gt;the &lt;code&gt;orders/create&lt;/code&gt; webhook topic&lt;/a&gt;, which fires a POST to your endpoint the moment a customer checks out.&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;POST https://{shop}.myshopify.com/admin/api/2025-01/webhooks.json

{
  "webhook": {
    "topic": "orders/create",
    "address": "https://your-app.com/webhooks/orders",
    "format": "json"
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload gives you everything: customer name, shipping address, line items, order ID. &lt;/p&gt;

&lt;p&gt;That's the input for the next step.&lt;/p&gt;

&lt;p&gt;Tips: As of April 2025, Shopify requires new public apps to use the GraphQL Admin API. If you're building a private or custom app, the REST endpoint above still works fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #2 — Shippo (shipping label generation)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://goshippo.com/" rel="noopener noreferrer"&gt;Shippo&lt;/a&gt; connects to 85+ carriers and gives you a &lt;a href="https://docs.goshippo.com/docs/guides_general/generate_shipping_label" rel="noopener noreferrer"&gt;single API&lt;/a&gt; to generate labels across all of them. &lt;/p&gt;

&lt;p&gt;Pricing is pay-as-you-go, labels start at around $0.05 per label fee on top of the carrier rate.&lt;/p&gt;

&lt;p&gt;You call it in two steps: create a shipment object (which returns available rates), then purchase the label against your chosen rate.&lt;/p&gt;

&lt;p&gt;Step 1 — Create shipment, get rates&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="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://api.goshippo.com/shipments/&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;"address_from"&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="err"&gt;...from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Shopify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;settings&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;"address_to"&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="err"&gt;...from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Shopify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload&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;"parcels"&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;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"distance_unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mass_unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lb"&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;Step 2 — Purchase label&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="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://api.goshippo.com/transactions/&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;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;rate_object_id from step 1&amp;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;"label_file_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;"PDF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;The response gives you &lt;code&gt;label_url&lt;/code&gt; (the printable PDF) and &lt;code&gt;tracking_number&lt;/code&gt;. Both go into the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  API #3 — Resend (customer notification)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; is a developer-first transactional email API. Free tier covers 3,000 emails/month, no daily cap on paid plans, and &lt;a href="https://resend.com/docs/api-reference/emails/send-email" rel="noopener noreferrer"&gt;the API&lt;/a&gt; is genuinely one of the cleanest out there right now.&lt;/p&gt;

&lt;p&gt;You fire a single POST once you have the tracking number from Shippo:&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;POST https://api.resend.com/emails

{
  "from": "orders@yourstore.com",
  "to": ["customer@email.com"],
  "subject": "Your order is on its way 📦",
  "html": "&amp;lt;p&amp;gt;Hi [name], your order has shipped. Track it here: [tracking_url]&amp;lt;/p&amp;gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tips: Using the same endpoint, you can fire a second email asking for a review 48–72 hours later.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Everything Works Together
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Frmj4k2q6lndjwly2os7p.png" 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.amazonaws.com%2Fuploads%2Farticles%2Frmj4k2q6lndjwly2os7p.png" alt=" " width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your app listens on a webhook endpoint. &lt;/p&gt;

&lt;p&gt;When a customer creates a new order, Shopify fires &lt;code&gt;orders/create&lt;/code&gt;, where you extract the shipping address, call Shippo to generate a label, then call Resend with the tracking number. &lt;/p&gt;

&lt;p&gt;The whole chain runs in a few seconds.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A quick note on LLMs: if your catalog has inconsistent product weights or dimensions (common with handmade or variable goods), an LLM can sit between Shopify and Shippo to infer parcel specs from product descriptions — removing the last bit of manual input. Worth knowing, but not required to get started.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How about Shopify's built-in tools?
&lt;/h2&gt;

&lt;p&gt;Shopify does have native shipping and email notification features, they work fine for the average merchants who are just getting started.&lt;/p&gt;

&lt;p&gt;Once your order volume hits a certain threshold, you will need to customize the logic, rate-shop across carriers programmatically, or plug in your own notification timing and content. &lt;/p&gt;

&lt;p&gt;All of these cannot be done easily without an app or a paid plan upgrade. &lt;/p&gt;

&lt;p&gt;Building it yourself with these three APIs gives you full control and costs less at volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final note: What this doesn't cover
&lt;/h2&gt;

&lt;p&gt;Admittedly, inventory sync, returns, and international customs are separate problems that are not covered here.&lt;/p&gt;

</description>
      <category>api</category>
      <category>shopify</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
