[{"data":1,"prerenderedAt":9489},["ShallowReactive",2],{"navigation":3,"/blog/software-development/migrating-from-travisci-to-github-actions-post":734,"/blog/software-development/migrating-from-travisci-to-github-actions-surround":905,"/blog/software-development/migrating-from-travisci-to-github-actions-related":910},[4,70,207,312,721],{"title":5,"path":6,"stem":7,"children":8,"page":69},"Case Study","/blog/case-study","blog/case-study",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65],{"title":10,"path":11,"stem":12},"Ambistream – Multi-Layer Streaming Platform","/blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea","blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea",{"title":14,"path":15,"stem":16},"Custom Chromecast & AirPlay Casting App","/blog/case-study/chromecast-airplay-casting-app-case-study","blog/case-study/chromecast-airplay-casting-app-case-study",{"title":18,"path":19,"stem":20},"Direct Music Licensing Platform Case Study","/blog/case-study/dlm-music-catalog-case-study","blog/case-study/dlm-music-catalog-case-study",{"title":22,"path":23,"stem":24},"Assembly Instructions App - Galeco Mobile App","/blog/case-study/galeco-mobile-app-case-study","blog/case-study/galeco-mobile-app-case-study",{"title":26,"path":27,"stem":28},"MemoSonic: Building an Audio Memory Game","/blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter","blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter",{"title":30,"path":31,"stem":32},"Loyalty Program App for Shopping Mall","/blog/case-study/loyalty-program-application-case-study","blog/case-study/loyalty-program-application-case-study",{"title":34,"path":35,"stem":36},"Mobile Application for Music Catalogs","/blog/case-study/mobile-app-for-music-catalog","blog/case-study/mobile-app-for-music-catalog",{"title":38,"path":39,"stem":40},"Universal Music Data Parser for 20+ Platforms","/blog/case-study/musicdata-lab-universal-music-data-parser-case-study","blog/case-study/musicdata-lab-universal-music-data-parser-case-study",{"title":42,"path":43,"stem":44},"Panther ML/AI Pricing Recommendation Tool","/blog/case-study/panther-pricing-recommendation-tool-case-study","blog/case-study/panther-pricing-recommendation-tool-case-study",{"title":46,"path":47,"stem":48},"4D Grupa Roofing Wholesalers Platform","/blog/case-study/roofing-wholesalers-website-case-study","blog/case-study/roofing-wholesalers-website-case-study",{"title":50,"path":51,"stem":52},"Talent Alpha HR Frontend Platform","/blog/case-study/talent-alpha-hr-platform-case-study","blog/case-study/talent-alpha-hr-platform-case-study",{"title":54,"path":55,"stem":56},"Ticketing & Events Platform Development","/blog/case-study/ticketing-events-platform-case-study","blog/case-study/ticketing-events-platform-case-study",{"title":58,"path":59,"stem":60},"Turn Fans into Superfans — Roadie.co","/blog/case-study/turn-fans-into-superfans-roadie-co","blog/case-study/turn-fans-into-superfans-roadie-co",{"title":62,"path":63,"stem":64},"Walkative 2.0 Global Booking Engine","/blog/case-study/walkative-2-booking-platform-case-study","blog/case-study/walkative-2-booking-platform-case-study",{"title":66,"path":67,"stem":68},"Why Merch Is the New Royalty Check, and How Tech Is Closing the Gap","/blog/case-study/why-merch-is-the-new-royalty-check","blog/case-study/why-merch-is-the-new-royalty-check",false,{"title":71,"path":72,"stem":73,"children":74,"page":69},"Music Data","/blog/music-data","blog/music-data",[75,79,83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171,175,179,183,187,191,195,199,203],{"title":76,"path":77,"stem":78},"13 Distributors, 5 File Formats, Zero Standards -The Reality of Music Royalty Data","/blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data","blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data",{"title":80,"path":81,"stem":82},"830 Ways to Say Spotify - Normalizing Music Streaming Data","/blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data","blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data",{"title":84,"path":85,"stem":86},"Why Music Companies Need AI-Powered Analytics (And How We Built One)","/blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama","blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama",{"title":88,"path":89,"stem":90},"AI Rehearsal: Spaced Repetition for Your Musical Ideas","/blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas","blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas",{"title":92,"path":93,"stem":94},"Audio Project Organization Is a Mess — Here's Why","/blog/music-data/audio-project-organization-mess","blog/music-data/audio-project-organization-mess",{"title":96,"path":97,"stem":98},"Why Audio Search Is Still Broken and How to Fix It with Embeddings","/blog/music-data/audio-search-broken-fix-with-embeddings","blog/music-data/audio-search-broken-fix-with-embeddings",{"title":100,"path":101,"stem":102},"AI Song Structure Analysis: Intro, Verse, Chorus","/blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus","blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus",{"title":104,"path":105,"stem":106},"The Broken Feedback Loop in Music Collaboration","/blog/music-data/broken-feedback-loop-music-collaboration","blog/music-data/broken-feedback-loop-music-collaboration",{"title":108,"path":109,"stem":110},"Building a Claude Skill for DDEX Validation: Automate Music Metadata Checks with AI","/blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata","blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata",{"title":112,"path":113,"stem":114},"Building a Custom Music Delivery Platform on the Revelator API","/blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api","blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api",{"title":116,"path":117,"stem":118},"Building a Suno AI Remix App with Nuxt & Firebase","/blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase","blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase",{"title":120,"path":121,"stem":122},"C2PA & DDEX: Authenticity Meets Rights in the Age of AI Music","/blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music","blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music",{"title":124,"path":125,"stem":126},"C2PA in Music: A Claude MCP for Reading Content Provenance","/blog/music-data/c2pa-in-music-mcp","blog/music-data/c2pa-in-music-mcp",{"title":128,"path":129,"stem":130},"Data Modeling in MongoDB Using Design Patterns","/blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns","blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns",{"title":132,"path":133,"stem":134},"Office Hours with MusicTech Lab's DDEX Expert","/blog/music-data/ddex-office-hours-musictech","blog/music-data/ddex-office-hours-musictech",{"title":136,"path":137,"stem":138},"DDEX Open Source Projects Review","/blog/music-data/ddex-open-source-projects-review","blog/music-data/ddex-open-source-projects-review",{"title":140,"path":141,"stem":142},"Extracting Data from Ableton .als and .asd Files","/blog/music-data/extracting-data-from-ableton-als-asd-files","blog/music-data/extracting-data-from-ableton-als-asd-files",{"title":144,"path":145,"stem":146},"Maciej Dulski on Sound Connections Podcast","/blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast","blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast",{"title":148,"path":149,"stem":150},"How to Transcribe Video to Text Using OpenAI Whisper","/blog/music-data/how-to-transcribe-video-to-text-using-whisper","blog/music-data/how-to-transcribe-video-to-text-using-whisper",{"title":152,"path":153,"stem":154},"Epidemic Sound MCP with Claude for Devs","/blog/music-data/how-to-use-epidemic-sound-mcp-with-claude","blog/music-data/how-to-use-epidemic-sound-mcp-with-claude",{"title":156,"path":157,"stem":158},"Hybrid Database Model in Django for Speed","/blog/music-data/hybrid-database-model-in-django-as-a-performance-booster","blog/music-data/hybrid-database-model-in-django-as-a-performance-booster",{"title":160,"path":161,"stem":162},"Introduction to generating DDEX file using Python","/blog/music-data/introduction-to-generating-ddex-file-using-python","blog/music-data/introduction-to-generating-ddex-file-using-python",{"title":164,"path":165,"stem":166},"Maintaining Music Tech Tools: The SLA Dilemma for Small Teams","/blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams","blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams",{"title":168,"path":169,"stem":170},"Querying Bandcamp Revenue Reports with Natural Language — Meet mtl-bandcamp-mcp","/blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard","blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard",{"title":172,"path":173,"stem":174},"mtl-metadata-mcp: Open Source Audio Metadata Embedding for Claude Code","/blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding","blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding",{"title":176,"path":177,"stem":178},"MusicTech Resources for Builders","/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders","blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders",{"title":180,"path":181,"stem":182},"Poland's Creative Tech and MusicTech Rise","/blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it","blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it",{"title":184,"path":185,"stem":186},"Batch ISRC Enrichment That Turns Messy Catalogs Into Clean Data","/blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz","blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz",{"title":188,"path":189,"stem":190},"Music Self-Publishing: The Emuze.me Story","/blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me","blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me",{"title":192,"path":193,"stem":194},"Understanding the API First Approach","/blog/music-data/understanding-the-api-first-approach","blog/music-data/understanding-the-api-first-approach",{"title":196,"path":197,"stem":198},"The Voice Memo Graveyard Problem","/blog/music-data/voice-memo-graveyard-problem","blog/music-data/voice-memo-graveyard-problem",{"title":200,"path":201,"stem":202},"Which Database for Music Data? Redshift vs BigQuery vs ClickHouse and When to Use Each","/blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse","blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse",{"title":204,"path":205,"stem":206},"Why we decided to use wavesurfer.js","/blog/music-data/why-we-decided-to-use-wavesurfer","blog/music-data/why-we-decided-to-use-wavesurfer",{"title":208,"path":209,"stem":210,"children":211,"page":69},"Newsletter","/blog/newsletter","blog/newsletter",[212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308],{"title":213,"path":214,"stem":215},"Music Industry Tech Openings (April 2024 Update)","/blog/newsletter/music-industry-tech-openings-april-2024-update","blog/newsletter/music-industry-tech-openings-april-2024-update",{"title":217,"path":218,"stem":219},"Music Industry Tech Openings (April 2025 Update)","/blog/newsletter/music-industry-tech-openings-april-2025-update","blog/newsletter/music-industry-tech-openings-april-2025-update",{"title":221,"path":222,"stem":223},"Music Industry Tech Openings (August 2024 Update)","/blog/newsletter/music-industry-tech-openings-august-2024-update","blog/newsletter/music-industry-tech-openings-august-2024-update",{"title":225,"path":226,"stem":227},"Music Industry Tech Openings (December 2024 Update)","/blog/newsletter/music-industry-tech-openings-december-2024-update","blog/newsletter/music-industry-tech-openings-december-2024-update",{"title":229,"path":230,"stem":231},"Music Industry Tech Openings (February 2025 Update)","/blog/newsletter/music-industry-tech-openings-february-2025-update","blog/newsletter/music-industry-tech-openings-february-2025-update",{"title":233,"path":234,"stem":235},"Music Industry Tech Openings (January 2025 Update)","/blog/newsletter/music-industry-tech-openings-january-2025-update","blog/newsletter/music-industry-tech-openings-january-2025-update",{"title":237,"path":238,"stem":239},"Music Industry Tech Openings (July 2024 Update)","/blog/newsletter/music-industry-tech-openings-july-2024-update","blog/newsletter/music-industry-tech-openings-july-2024-update",{"title":241,"path":242,"stem":243},"Music Industry Tech Openings (June 2024 Update)","/blog/newsletter/music-industry-tech-openings-june-2024-update","blog/newsletter/music-industry-tech-openings-june-2024-update",{"title":245,"path":246,"stem":247},"Music Industry Tech Openings (March 2025 Update)","/blog/newsletter/music-industry-tech-openings-march-2025-update","blog/newsletter/music-industry-tech-openings-march-2025-update",{"title":249,"path":250,"stem":251},"Music Industry Tech Openings (May 2024 Update)","/blog/newsletter/music-industry-tech-openings-may-2024-update","blog/newsletter/music-industry-tech-openings-may-2024-update",{"title":253,"path":254,"stem":255},"Music Industry Tech Openings (May 2025 Update)","/blog/newsletter/music-industry-tech-openings-may-2025","blog/newsletter/music-industry-tech-openings-may-2025",{"title":257,"path":258,"stem":259},"Music Industry Tech Openings (November 2024 Update)","/blog/newsletter/music-industry-tech-openings-november-2024-update","blog/newsletter/music-industry-tech-openings-november-2024-update",{"title":261,"path":262,"stem":263},"Music Industry Tech Openings (October 2024 Update)","/blog/newsletter/music-industry-tech-openings-october-2024-update","blog/newsletter/music-industry-tech-openings-october-2024-update",{"title":265,"path":266,"stem":267},"Music Industry Tech Openings (September 2024 Update)","/blog/newsletter/music-industry-tech-openings-september-2024-update","blog/newsletter/music-industry-tech-openings-september-2024-update",{"title":269,"path":270,"stem":271},"MusicTech Insights #1 by Maciej Dulski","/blog/newsletter/musictech-insights-1-curated-by-maciej-dulski","blog/newsletter/musictech-insights-1-curated-by-maciej-dulski",{"title":273,"path":274,"stem":275},"Provenance, Not Detection, Is the Durable Answer","/blog/newsletter/musictech-insights-10-curated-by-jim-anderson","blog/newsletter/musictech-insights-10-curated-by-jim-anderson",{"title":277,"path":278,"stem":279},"What CMOs can teach us about innovation in uncertain times","/blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska","blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska",{"title":281,"path":282,"stem":283},"Feeling the MusicTech Momentum","/blog/newsletter/musictech-insights-2-curated-by-maciej-dulski","blog/newsletter/musictech-insights-2-curated-by-maciej-dulski",{"title":285,"path":286,"stem":287},"AI in Music: Hype, Hope, and a Human Touch","/blog/newsletter/musictech-insights-3-curated-by-drew-thurlow","blog/newsletter/musictech-insights-3-curated-by-drew-thurlow",{"title":289,"path":290,"stem":291},"The Music Metadata Conundrum","/blog/newsletter/musictech-insights-4-curated-by-amanda-schupf","blog/newsletter/musictech-insights-4-curated-by-amanda-schupf",{"title":293,"path":294,"stem":295},"7 Rounds in the First 10 Days of November 2025","/blog/newsletter/musictech-insights-5-curated-by-maciej-dulski","blog/newsletter/musictech-insights-5-curated-by-maciej-dulski",{"title":297,"path":298,"stem":299},"The End of an Era: It's All About to Crash","/blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason","blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason",{"title":301,"path":302,"stem":303},"Low-Code Magic Won't Solve MusicTech Reality","/blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk","blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk",{"title":305,"path":306,"stem":307},"The New Economics of Game Music","/blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan","blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan",{"title":309,"path":310,"stem":311},"Music Business Meets Direct-to-Fan","/blog/newsletter/musictech-insights-9-curated-by-yaw-asamani","blog/newsletter/musictech-insights-9-curated-by-yaw-asamani",{"title":313,"path":314,"stem":315,"children":316,"page":69},"Software Development","/blog/software-development","blog/software-development",[317,321,325,329,333,337,341,345,349,353,357,361,365,369,373,377,381,385,389,393,397,401,405,409,413,417,421,425,429,433,437,441,445,449,453,457,461,465,469,473,477,481,485,489,493,497,501,505,509,513,517,521,525,529,533,537,541,545,549,553,557,561,565,569,573,577,581,585,589,593,597,601,605,609,613,617,621,625,629,633,637,641,645,649,653,657,661,665,669,673,677,681,685,689,693,697,701,705,709,713,717],{"title":318,"path":319,"stem":320},"Benefits of Outsourcing Software Development","/blog/software-development/10-benefits-of-outsourcing-software-development-services","blog/software-development/10-benefits-of-outsourcing-software-development-services",{"title":322,"path":323,"stem":324},"10 Steps to Find the Best MVP Developers","/blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea","blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea",{"title":326,"path":327,"stem":328},"1,200 Looms Later: How Async Video Became My Development Superpower","/blog/software-development/1200-looms-how-async-video-became-our-development-superpower","blog/software-development/1200-looms-how-async-video-became-our-development-superpower",{"title":330,"path":331,"stem":332},"Communication Strategy in Outsourcing Projects","/blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project","blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project",{"title":334,"path":335,"stem":336},"7 Best Practices for Outsourcing Software Development","/blog/software-development/7-best-practices-for-outsourcing-software-development","blog/software-development/7-best-practices-for-outsourcing-software-development",{"title":338,"path":339,"stem":340},"9 Reasons Why Saleor.io Is Best for eCommerce","/blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website","blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website",{"title":342,"path":343,"stem":344},"A Look at Bravelab.io’s Clutch 2021 Year In Review","/blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review","blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review",{"title":346,"path":347,"stem":348},"A quick introduction to profit sharing implementation","/blog/software-development/a-quick-introduction-to-profit-sharing-implementation","blog/software-development/a-quick-introduction-to-profit-sharing-implementation",{"title":350,"path":351,"stem":352},"AI Audio Similarity Search: The Future of Sound Library Discovery","/blog/software-development/ai-audio-similarity-search-for-sound-libraries","blog/software-development/ai-audio-similarity-search-for-sound-libraries",{"title":354,"path":355,"stem":356},"Automate Repetitive Tasks for Better Results","/blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance","blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance",{"title":358,"path":359,"stem":360},"Automating Success: The Art of Unified Documentation","/blog/software-development/automating-success-the-art-of-unified-documentation","blog/software-development/automating-success-the-art-of-unified-documentation",{"title":362,"path":363,"stem":364},"Brave 3.0 Website Redesign, Part 2: Solution","/blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution","blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution",{"title":366,"path":367,"stem":368},"Brave 3.0, Part 4: Tech Stack and Recap","/blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap","blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap",{"title":370,"path":371,"stem":372},"Brave 3.0 – redesign process part 1. The Challenge","/blog/software-development/brave-3-0-redesign-process-part-1-challenge","blog/software-development/brave-3-0-redesign-process-part-1-challenge",{"title":374,"path":375,"stem":376},"Brave 3.0 – redesign process, part 3. Lesson learned","/blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned","blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned",{"title":378,"path":379,"stem":380},"Bravelab.io: Top Software Developer by Clutch","/blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":382,"path":383,"stem":384},"Bravelab.io: Top Developer in Poland by Clutch","/blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch","blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch",{"title":386,"path":387,"stem":388},"MusicTech Lab Partners with LALAL.AI","/blog/software-development/bravelab-partners-with-the-audio-lalal-ai","blog/software-development/bravelab-partners-with-the-audio-lalal-ai",{"title":390,"path":391,"stem":392},"MusicTech Lab Partners with The Audio Programmer","/blog/software-development/bravelab-partners-with-the-audio-programmer","blog/software-development/bravelab-partners-with-the-audio-programmer",{"title":394,"path":395,"stem":396},"Bravelab's team about productivity","/blog/software-development/bravelabs-team-about-productivity","blog/software-development/bravelabs-team-about-productivity",{"title":398,"path":399,"stem":400},"Braveloper","/blog/software-development/braveloper","blog/software-development/braveloper",{"title":402,"path":403,"stem":404},"Bravely App: Boost Productivity with Django","/blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick","blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick",{"title":406,"path":407,"stem":408},"DIY MIDI Controller for Ableton with Arduino","/blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino","blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino",{"title":410,"path":411,"stem":412},"C2PA in Ableton: Making AI Music Provenance Visible Inside Your DAW","/blog/software-development/c2pa-in-ableton-max-for-live","blog/software-development/c2pa-in-ableton-max-for-live",{"title":414,"path":415,"stem":416},"Change Detection mechanism in Angular","/blog/software-development/change-detection-mechanism-in-angular","blog/software-development/change-detection-mechanism-in-angular",{"title":418,"path":419,"stem":420},"Communication Channels in Remote Work","/blog/software-development/comparison-of-the-communication-channels-in-remote-work","blog/software-development/comparison-of-the-communication-channels-in-remote-work",{"title":422,"path":423,"stem":424},"Connecting Your Max for Live Device to a Cloud API","/blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api","blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api",{"title":426,"path":427,"stem":428},"From Voice Memo to Studio: The Cross-Platform Problem for Creators","/blog/software-development/cross-platform-problem-for-creators","blog/software-development/cross-platform-problem-for-creators",{"title":430,"path":431,"stem":432},"Cultural transformation through the pandemic era","/blog/software-development/cultural-transformation-through-the-pandemic-era","blog/software-development/cultural-transformation-through-the-pandemic-era",{"title":434,"path":435,"stem":436},"D-Commerce Decoded: Cutting Through the Hype","/blog/software-development/d-commerce-decoded-cutting-through-the-hype","blog/software-development/d-commerce-decoded-cutting-through-the-hype",{"title":438,"path":439,"stem":440},"Dev Meeting 002: Intro to DDD","/blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd","blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd",{"title":442,"path":443,"stem":444},"Dev Meeting 003: Web3 Primer","/blog/software-development/dev-meeting-003-web3-primer","blog/software-development/dev-meeting-003-web3-primer",{"title":446,"path":447,"stem":448},"Dev Meeting 004: Introduction to Event Storming","/blog/software-development/dev-meeting-004-introduction-to-event-storming","blog/software-development/dev-meeting-004-introduction-to-event-storming",{"title":450,"path":451,"stem":452},"Dev Meeting 001: Kubernetes is a Framework","/blog/software-development/dev-meeting-kubernetes-is-a-framework","blog/software-development/dev-meeting-kubernetes-is-a-framework",{"title":454,"path":455,"stem":456},"Did You Know? 10 Developer Tips from Real Codebases","/blog/software-development/did-you-know-dev-tips-part-1","blog/software-development/did-you-know-dev-tips-part-1",{"title":458,"path":459,"stem":460},"10 Surprising MusicTech Facts (Part 2)","/blog/software-development/did-you-know-musictech-facts-part-2","blog/software-development/did-you-know-musictech-facts-part-2",{"title":462,"path":463,"stem":464},"Django-cms and GraphQL","/blog/software-development/django-cms-and-graphql","blog/software-development/django-cms-and-graphql",{"title":466,"path":467,"stem":468},"Does Zappa make it super easy?","/blog/software-development/does-zappa-make-it-super-easy","blog/software-development/does-zappa-make-it-super-easy",{"title":470,"path":471,"stem":472},"Establishing cooperation between Netlify and Bravelab","/blog/software-development/establishing-cooperation-between-netlify-and-bravelab","blog/software-development/establishing-cooperation-between-netlify-and-bravelab",{"title":474,"path":475,"stem":476},"Export Ableton Locators to JSON via Max for Live","/blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live","blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live",{"title":478,"path":479,"stem":480},"IT Outsourcing: Success and Failure Factors","/blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project","blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project",{"title":482,"path":483,"stem":484},"Flutter 2022 Strategy: Analyzing the Roadmap","/blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap","blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap",{"title":486,"path":487,"stem":488},"Git Better #1 — Commit Message Convention","/blog/software-development/git-better-1-see-more-with-a-commit-message-convention","blog/software-development/git-better-1-see-more-with-a-commit-message-convention",{"title":490,"path":491,"stem":492},"Hasura in action. How to use it with Django","/blog/software-development/hasura-in-action","blog/software-development/hasura-in-action",{"title":494,"path":495,"stem":496},"Holacracy why and where we are","/blog/software-development/holacracy-why-and-where-we-are","blog/software-development/holacracy-why-and-where-we-are",{"title":498,"path":499,"stem":500},"How does JavaScript work","/blog/software-development/how-does-javascript-work","blog/software-development/how-does-javascript-work",{"title":502,"path":503,"stem":504},"How important is good UX/UI design?","/blog/software-development/how-important-is-good-ux-ui-design","blog/software-development/how-important-is-good-ux-ui-design",{"title":506,"path":507,"stem":508},"How repetitive tasks impact your business","/blog/software-development/how-repetitive-tasks-impact-your-business","blog/software-development/how-repetitive-tasks-impact-your-business",{"title":510,"path":511,"stem":512},"Becoming a Vue.js Dev: Do Paid Trials Work?","/blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out","blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out",{"title":514,"path":515,"stem":516},"How to Build an MVP in 6 Steps","/blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps","blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps",{"title":518,"path":519,"stem":520},"How to conduct workshops for creative industry?","/blog/software-development/how-to-conduct-workshops-for-creative-industry","blog/software-development/how-to-conduct-workshops-for-creative-industry",{"title":522,"path":523,"stem":524},"How to easily create form in Angular","/blog/software-development/how-to-easily-create-form-in-angular","blog/software-development/how-to-easily-create-form-in-angular",{"title":526,"path":527,"stem":528},"How to export orders in Saleor.io to XLSX file","/blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file","blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file",{"title":530,"path":531,"stem":532},"Handling High Loads on E-Commerce Platforms","/blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease","blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease",{"title":534,"path":535,"stem":536},"How to launch Saleor.io shop instance within 40h","/blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h","blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h",{"title":538,"path":539,"stem":540},"First Steps to Build a Business Relationship","/blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship","blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship",{"title":542,"path":543,"stem":544},"Multi-Tenant Apps with Django and Saleor.io","/blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform","blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform",{"title":546,"path":547,"stem":548},"Notion Backup Tool Built in 3 Days with Python","/blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why","blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why",{"title":550,"path":551,"stem":552},"Important new features in Python 3.8","/blog/software-development/important-new-features-in-python-3-8","blog/software-development/important-new-features-in-python-3-8",{"title":554,"path":555,"stem":556},"Installing Proxmox on dedicated server from OVH","/blog/software-development/installing-proxmox-on-dedicated-server-from-ovh","blog/software-development/installing-proxmox-on-dedicated-server-from-ovh",{"title":558,"path":559,"stem":560},"Integrating SignNow E-Signatures into Your Django Application","/blog/software-development/integrating-signnow-e-signatures-into-your-django-application","blog/software-development/integrating-signnow-e-signatures-into-your-django-application",{"title":562,"path":563,"stem":564},"Tempus Metronome and GetSongBPM API","/blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it","blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it",{"title":566,"path":567,"stem":568},"Introducing MusicTech Poland","/blog/software-development/introducing-musictech-poland","blog/software-development/introducing-musictech-poland",{"title":570,"path":571,"stem":572},"Vue.js as a Frontend for Saleor.io","/blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform","blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform",{"title":574,"path":575,"stem":576},"Is your business ready for the cashless era?","/blog/software-development/is-your-business-ready-for-the-cashless-era","blog/software-development/is-your-business-ready-for-the-cashless-era",{"title":578,"path":579,"stem":580},"Is your face ready to buy?","/blog/software-development/is-your-face-ready-to-buy","blog/software-development/is-your-face-ready-to-buy",{"title":582,"path":583,"stem":584},"JS Frameworks: Trends and Opportunities","/blog/software-development/javascript-trending-frameworks-and-market-opportunities","blog/software-development/javascript-trending-frameworks-and-market-opportunities",{"title":586,"path":587,"stem":588},"Kanban Board: Boost Your Team Productivity","/blog/software-development/kanban-board-methodology-hack-your-companys-productivity","blog/software-development/kanban-board-methodology-hack-your-companys-productivity",{"title":590,"path":591,"stem":592},"Verified Human Cert MCP Server: Prove Your Music Is Human-Made, Right from the Terminal","/blog/software-development/mcp-verified-human-cert-open-source","blog/software-development/mcp-verified-human-cert-open-source",{"title":594,"path":595,"stem":596},"Migrating from TravisCI to Github Actions","/blog/software-development/migrating-from-travisci-to-github-actions","blog/software-development/migrating-from-travisci-to-github-actions",{"title":598,"path":599,"stem":600},"MusicTech Lab: Top Software Developer by Clutch","/blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":602,"path":603,"stem":604},"MusicTech Lab x Verified Human: Building a Trust Layer for Human-Made Music","/blog/software-development/musictechlab_blog_verified_human_partnership","blog/software-development/musictechlab_blog_verified_human_partnership",{"title":606,"path":607,"stem":608},"MusicXML: Standard for Music Notation","/blog/software-development/musicxml-standard-for-music-notation-and-education","blog/software-development/musicxml-standard-for-music-notation-and-education",{"title":610,"path":611,"stem":612},"Only a few books but dozens of ideas","/blog/software-development/only-a-few-books-but-dozens-of-ideas","blog/software-development/only-a-few-books-but-dozens-of-ideas",{"title":614,"path":615,"stem":616},"Overdue Invoices and Issue Tracker Integration","/blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system","blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system",{"title":618,"path":619,"stem":620},"Performing SAML SSO using JWT in Django","/blog/software-development/performing-saml-sso-using-jwt-in-django","blog/software-development/performing-saml-sso-using-jwt-in-django",{"title":622,"path":623,"stem":624},"Progressive Web Apps for Mobile Development","/blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application","blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application",{"title":626,"path":627,"stem":628},"Recruitment System: Gmail, Jira, and CRM","/blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm","blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm",{"title":630,"path":631,"stem":632},"Scratch Me: Chrome Extension for Leads","/blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity","blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity",{"title":634,"path":635,"stem":636},"Scratch Me – integration with the Copper CRM","/blog/software-development/scratch-me-integration-with-the-copper-crm","blog/software-development/scratch-me-integration-with-the-copper-crm",{"title":638,"path":639,"stem":640},"SignNow MCP Server: E-Signatures Straight from Claude Code","/blog/software-development/signnow-mcp-server-e-signatures-from-claude-code","blog/software-development/signnow-mcp-server-e-signatures-from-claude-code",{"title":642,"path":643,"stem":644},"Music Industry Tech Openings (March 2024 Update)","/blog/software-development/technical-job-opportunities-in-the-music-industry","blog/software-development/technical-job-opportunities-in-the-music-industry",{"title":646,"path":647,"stem":648},"Thanks app – a Management 3.0 solution","/blog/software-development/thanks-app-a-management-3-0-solution","blog/software-development/thanks-app-a-management-3-0-solution",{"title":650,"path":651,"stem":652},"Colonial Pipeline Case: 7 Security Reminders","/blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders","blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders",{"title":654,"path":655,"stem":656},"The Evolution and Future of E-commerce Platforms","/blog/software-development/the-evolution-and-future-of-e-commerce-platforms","blog/software-development/the-evolution-and-future-of-e-commerce-platforms",{"title":658,"path":659,"stem":660},"The Gender Gap in the Tech Industry","/blog/software-development/the-gender-gap-in-the-tech-industry","blog/software-development/the-gender-gap-in-the-tech-industry",{"title":662,"path":663,"stem":664},"First Attempt to Implement 4DX at Bravelab.io","/blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io","blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io",{"title":666,"path":667,"stem":668},"The WTF Scale: IT Project Complexity","/blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity","blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity",{"title":670,"path":671,"stem":672},"Top 10 articles through the eyes of our developers","/blog/software-development/top-10-articles-through-the-eyes-of-our-developers","blog/software-development/top-10-articles-through-the-eyes-of-our-developers",{"title":674,"path":675,"stem":676},"Top 6 apps made with Flutter","/blog/software-development/top-6-apps-made-with-flutter","blog/software-development/top-6-apps-made-with-flutter",{"title":678,"path":679,"stem":680},"Uber 101: How Uber Made It to the Top","/blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top","blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top",{"title":682,"path":683,"stem":684},"MusicTech Lab Partners with Music Glue","/blog/software-development/unifying-artists-and-audiences-exploring-music-glue","blog/software-development/unifying-artists-and-audiences-exploring-music-glue",{"title":686,"path":687,"stem":688},"Why AI Will Defeat Traditional HR","/blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr","blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr",{"title":690,"path":691,"stem":692},"What is a Discovery Document?","/blog/software-development/what-is-discovery-document","blog/software-development/what-is-discovery-document",{"title":694,"path":695,"stem":696},"What is Flutter, and Why is it Worth Considering?","/blog/software-development/what-is-flutter-and-why-is-it-worth-considering","blog/software-development/what-is-flutter-and-why-is-it-worth-considering",{"title":698,"path":699,"stem":700},"What is a Watermarked Song?","/blog/software-development/what-is-watermarked-song","blog/software-development/what-is-watermarked-song",{"title":702,"path":703,"stem":704},"Choosing a Frontend Framework for the Web","/blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development","blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development",{"title":706,"path":707,"stem":708},"Why DAWs Are the Wrong Tool for Starting a Song","/blog/software-development/why-daws-wrong-tool-for-starting-song","blog/software-development/why-daws-wrong-tool-for-starting-song",{"title":710,"path":711,"stem":712},"Why the Programming World Loves Python","/blog/software-development/why-the-programming-world-loves-python","blog/software-development/why-the-programming-world-loves-python",{"title":714,"path":715,"stem":716},"Why We Don't Build Chat From Scratch (And Neither Should You)","/blog/software-development/why-we-dont-build-chat-from-scratch","blog/software-development/why-we-dont-build-chat-from-scratch",{"title":718,"path":719,"stem":720},"Why we use Sanity.io","/blog/software-development/why-we-use-sanity-io","blog/software-development/why-we-use-sanity-io",{"title":722,"path":723,"stem":724,"children":725,"page":69},"Sportstech","/blog/sportstech","blog/sportstech",[726,730],{"title":727,"path":728,"stem":729},"BeatBuddy Replay: Video Analysis App Challenges","/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter","blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter",{"title":731,"path":732,"stem":733},"How to Create a Watch Face App for Garmin Watch","/blog/sportstech/how-to-create-watch-face-app-for-garmin-watch","blog/sportstech/how-to-create-watch-face-app-for-garmin-watch",{"id":735,"title":594,"authors":736,"badge":741,"body":742,"category":881,"client":741,"date":882,"description":883,"extension":884,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":885,"keyTakeaways":887,"meta":899,"navigation":888,"path":595,"seo":900,"status":741,"stem":596,"tags":901,"teaser":741,"__hash__":904},"posts/blog/software-development/migrating-from-travisci-to-github-actions.md",[737],{"name":738,"avatar":739},"Szymon Zmilczak",{"src":740},"/images/people/szymon-zmilczak.webp",null,{"type":743,"value":744,"toc":872},"minimark",[745,749,754,762,773,777,795,798,802,805,808,811,814,817,821,824,843,849,853,869],[746,747,748],"p",{},"Every project needs CI and there are many solutions to choose from. We'll compare two of them by describing our journey migrating from TravisCI to GitHub Actions.",[750,751,753],"h2",{"id":752},"what-even-are-these-tools","What even are these tools?",[746,755,756,757,761],{},"Both TravisCI and GitHub Actions are continuous integration services. ",[758,759,760],"strong",{},"Continuous Integration (CI)"," requires developers to frequently integrate code into a shared repository. Each change is verified by automated tools — linting, tests, and builds — catching problems early.",[746,763,764,765,768,769,772],{},"CI can be paired with ",[758,766,767],{},"Continuous Delivery (CD)",", which ensures software can be deployed at any time. Automate the release process further and you get ",[758,770,771],{},"continuous deployment",".",[750,774,776],{"id":775},"why-are-we-changing","Why are we changing?",[778,779,786,791],"div",{"className":780},[781,782,783,784,785],"grid","grid-cols-1","md:grid-cols-2","gap-4","my-8",[787,788],"spotlight-card",{"description":789,"title":790},"$129/month — unlimited build time, up to 2 concurrent jobs","TravisCI Startup",[787,792],{"description":793,"title":794},"Included in $88/month GitHub Team — 10,000 minutes, up to 60 concurrent jobs","GitHub Actions (with Team)",[746,796,797],{},"We were using about 5,500 minutes of builds per month — well within GitHub’s free limit. Even paying for all that time would cost only $44. The decision was simple.",[750,799,801],{"id":800},"what-was-easy","What was easy?",[746,803,804],{},"Both tools are similar in concept: do a couple of things when something happens and report the results. For both tools, specifying what happens is done via special YAML files stored in the repository along with the project source code. Setting when something happens on TravisCI is done by pushing the correct button on the website. That’s different for Github Actions: you set it in the YAML configuration file.",[746,806,807],{},"The concept of determining what to do and in which order is similar in both tools, but the implementation is different. In TravisCI, you can add blocks of code to predefined steps that are executed in a specific order. In Github Actions, you can create any number of actions, which contain jobs with inside steps (blocks of code). We created one action that is composed of three jobs: testing, building, and deployment. Contents of TravisCI steps were distributed among these jobs in a form of steps. Focusing deployment step only on specific branches and parametrizing target was easy because both platforms support conditional step execution in their configuration format.",[746,809,810],{},"Specifying the required environment is also similar — set languages and versions in the config file and everything is taken care of. Both platforms support multiple operating systems and matrix builds. One difference: repository contents are always available in TravisCI, but GitHub Actions requires an explicit checkout step.",[746,812,813],{},"Handling secrets works the same way — create them on the website and they become environment variables in the build. Both tools sanitize output by censoring secrets if printed.",[746,815,816],{},"The build badge was also straightforward to migrate. Both platforms expose build status as an image URL you can embed in READMEs and Slack channels.",[750,818,820],{"id":819},"what-wasnt-easy","What wasn’t easy?",[746,822,823],{},"Not everything is 100% compatible. Here are the issues we hit:",[778,825,827,831,835,839],{"className":826},[781,782,783,784,785],[787,828],{"description":829,"title":830},"TravisCI has `BRANCH`. GitHub Actions has `GITHUB_REF` which includes extra data — we had to extract the branch name from the ref string.","Branch Name",[787,832],{"description":833,"title":834},"TravisCI provides `TRAVIS_BUILD_NUMBER`. GitHub Actions has no equivalent — we count commits on the branch instead.","Build Number",[787,836],{"description":837,"title":838},"Each step runs in a separate environment. The workaround: print values to stdout in a specific format to set global env vars.","Passing Values Between Steps",[787,840],{"description":841,"title":842},"TravisCI lets you SSH into a failed build. GitHub Actions had no equivalent — we resorted to printf-debugging.","No Build Debugging",[844,845,846],"warning",{},[746,847,848],{},"At the time of writing, GitHub Actions lacked automatic build cancellation. In TravisCI, pushing a new commit cancels the in-progress build on the same branch. GitHub Actions ran all builds, wasting minutes on intermediate commits.",[750,850,852],{"id":851},"does-it-even-work","Does it even work?",[778,854,857,861,865],{"className":855},[781,782,856,784,785],"md:grid-cols-3",[787,858],{"description":859,"title":860},"Total time to learn and migrate two projects","4 hours",[787,862],{"description":863,"title":864},"Savings from dropping TravisCI","$1,500/year",[787,866],{"description":867,"title":868},"Config grew due to GitHub's more explicit format","45 → 60 lines",[746,870,871],{},"Execution times are about the same, but builds spin up faster. Despite some missing features, GitHub Actions is mature enough for production use.",{"title":873,"searchDepth":874,"depth":874,"links":875},"",2,[876,877,878,879,880],{"id":752,"depth":874,"text":753},{"id":775,"depth":874,"text":776},{"id":800,"depth":874,"text":801},{"id":819,"depth":874,"text":820},{"id":851,"depth":874,"text":852},"software-development","2019-12-06T00:00:00.000Z","A practical comparison of GitHub Actions vs TravisCI with a step-by-step migration guide. Learn why we switched and how to move your CI/CD pipeline smoothly.","md",{"src":886},"/images/blog/musictechlab_blog_migrating-from-travisci-to-github-actions.webp",{"enabled":888,"items":889},true,[890,893,896],{"text":891,"icon":892},"Migrating two projects from TravisCI to GitHub Actions took only 4 hours total.","i-lucide-clock",{"text":894,"icon":895},"The switch saved $1,500 per year by using GitHub Actions minutes included in the Team plan.","i-lucide-dollar-sign",{"text":897,"icon":898},"GitHub Actions config is more explicit (45 to 60 lines) but builds spin up faster.","i-lucide-rocket",{},{"title":594,"description":883},[902,903],"devops","development","R5XO18JHnUgeNeP1kYF1atA9K5mlizVlRLS1lSnZZ3s",[906,908],{"title":590,"path":591,"stem":592,"description":907,"children":-1},"We open-sourced an MCP server that queries the Verified Human Cert registry. Verify human-made music certifications by ISRC, artist, track, or cert number directly from Claude Code.",{"title":598,"path":599,"stem":600,"description":909,"children":-1},"Clutch.co named MusicTech Lab a top custom software development company in Poland, recognizing us as a leading provider for the second consecutive year.",[911,1644,1787,2240],{"id":912,"title":546,"authors":913,"badge":919,"body":922,"category":881,"client":741,"date":1623,"description":1624,"extension":884,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":1625,"keyTakeaways":1627,"meta":1640,"navigation":888,"path":547,"seo":1641,"status":741,"stem":548,"tags":1642,"teaser":741,"__hash__":1643,"score":1296},"posts/blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why.md",[914],{"name":915,"to":916,"avatar":917},"Mariusz Smenżyk","https://www.linkedin.com/in/mariusz-smenzyk/",{"src":918},"/images/people/mariusz-smenzyk2.webp",{"label":920,"color":921},"Open Source","#4CAF50",{"type":743,"value":923,"toc":1601},[924,931,934,937,941,944,949,952,957,960,965,968,971,975,978,997,1000,1004,1007,1012,1015,1020,1023,1028,1031,1036,1039,1043,1051,1057,1063,1069,1074,1105,1109,1112,1116,1154,1157,1161,1164,1190,1193,1196,1200,1204,1207,1387,1391,1398,1402,1405,1413,1417,1420,1452,1455,1459,1462,1467,1470,1475,1478,1483,1486,1490,1495,1498,1503,1506,1511,1522,1527,1530,1534,1537,1566,1569,1573,1576,1579,1582,1585,1597],[746,925,926,927,930],{},"At MusicTech Lab, we've used Notion extensively for internal documentation, project management, and knowledge sharing. But we made a strategic decision: ",[758,928,929],{},"we're moving away from Notion",". Not because it's a bad tool - it's excellent - but because our workflow has evolved.",[746,932,933],{},"We now store project data directly with our clients as a standard practice. Keeping a separate Notion workspace created fragmentation and potential security concerns. But before leaving, we needed to preserve years of accumulated knowledge.",[746,935,936],{},"This is the story of how we built a custom Python tool that exports Notion content to Markdown - and why we're releasing it as open source.",[750,938,940],{"id":939},"why-were-leaving-notion","Why We're Leaving Notion",[746,942,943],{},"Our decision came down to three factors:",[746,945,946],{},[758,947,948],{},"1. Client-first data storage",[746,950,951],{},"We've adopted a policy of storing all project documentation directly in our clients' systems. Whether that's their GitHub, Confluence, or internal wikis - the data lives where the client can access and own it. Maintaining a parallel Notion workspace created duplication and sync headaches.",[746,953,954],{},[758,955,956],{},"2. Data ownership concerns",[746,958,959],{},"As a company working with music industry clients, we handle sensitive business information. Having that data in a third-party SaaS, even one as reputable as Notion, introduced unnecessary risk. Direct client storage means clearer data ownership and simpler compliance.",[746,961,962],{},[758,963,964],{},"3. Reducing tool sprawl",[746,966,967],{},"Every tool in your stack is a potential point of friction. By eliminating Notion and using client-native tools, we reduced context switching and simplified onboarding for new team members.",[746,969,970],{},"But we still had 4000+ pages of internal processes, meeting notes, and institutional knowledge that we couldn't just abandon.",[750,972,974],{"id":973},"the-problem","The Problem",[746,976,977],{},"Notion's native export is... functional, but painful:",[979,980,981,985,988,991,994],"ul",{},[982,983,984],"li",{},"Manual process - no automation possible",[982,986,987],{},"Messy folder structures with random IDs",[982,989,990],{},"Broken image links (Notion URLs expire)",[982,992,993],{},"Lost hierarchy - parent-child relationships disappear",[982,995,996],{},"No frontmatter for static site integration",[746,998,999],{},"We needed a way to extract everything in a format that would remain useful for years - clean Markdown files that could live in Git, be searched easily, and optionally published on our website.",[750,1001,1003],{"id":1002},"the-challenge","The Challenge",[746,1005,1006],{},"Building a proper Notion export tool revealed several non-obvious challenges:",[746,1008,1009],{},[758,1010,1011],{},"Hierarchical structure",[746,1013,1014],{},"Notion pages can be infinitely nested. The API returns flat lists, not trees. Preserving parent-child relationships for navigation required building our own hierarchy map.",[746,1016,1017],{},[758,1018,1019],{},"Rich content types",[746,1021,1022],{},"Notion has toggles, callouts, databases, embeds, tables, code blocks, and 20+ block types. Each needs specific Markdown conversion logic.",[746,1024,1025],{},[758,1026,1027],{},"Expiring media URLs",[746,1029,1030],{},"Notion's image and file URLs expire after approximately 1 hour. You cannot reference them directly - they must be downloaded and stored locally.",[746,1032,1033],{},[758,1034,1035],{},"Database rendering",[746,1037,1038],{},"Notion databases contain valuable structured information. They needed to be converted to readable Markdown tables with all property types preserved.",[750,1040,1042],{"id":1041},"our-solution","Our Solution",[746,1044,1045,1046,1050],{},"We built ",[1047,1048,1049],"code",{},"notion-sync",", a Python CLI tool with three main components:",[746,1052,1053,1056],{},[758,1054,1055],{},"NotionClientWrapper"," - Handles all API communication with pagination, error handling, and recursive page discovery.",[746,1058,1059,1062],{},[758,1060,1061],{},"MarkdownBuilder"," - Converts Notion blocks to clean Markdown, downloads assets locally, and generates YAML frontmatter for static site generators.",[746,1064,1065,1068],{},[758,1066,1067],{},"sync_notion.py"," - Orchestrates the process and provides a flexible CLI interface.",[1070,1071,1073],"h3",{"id":1072},"key-features","Key Features",[979,1075,1076,1082,1088,1094,1099],{},[982,1077,1078,1081],{},[758,1079,1080],{},"Recursive page crawling"," - Discovers and syncs all nested content automatically",[982,1083,1084,1087],{},[758,1085,1086],{},"Asset downloading"," - Images, PDFs, and videos are saved locally with content-based hashing",[982,1089,1090,1093],{},[758,1091,1092],{},"Hierarchy preservation"," - YAML frontmatter with parent/child relationships",[982,1095,1096,1098],{},[758,1097,1035],{}," - Converts to Markdown tables with all property types",[982,1100,1101,1104],{},[758,1102,1103],{},"Flexible CLI"," - Full sync, single page branch, database-only, and test modes",[750,1106,1108],{"id":1107},"tech-stack","Tech Stack",[746,1110,1111],{},"We chose simplicity over cleverness:",[1070,1113,1115],{"id":1114},"sync-tool-python","Sync Tool (Python)",[979,1117,1118,1124,1130,1136,1142,1148],{},[982,1119,1120,1123],{},[758,1121,1122],{},"Python 3.11"," - Excellent for scripting and API work",[982,1125,1126,1129],{},[758,1127,1128],{},"notion-client"," - Official Notion SDK",[982,1131,1132,1135],{},[758,1133,1134],{},"requests"," - HTTP client for asset downloading",[982,1137,1138,1141],{},[758,1139,1140],{},"PyYAML"," - Frontmatter generation",[982,1143,1144,1147],{},[758,1145,1146],{},"python-dotenv"," - Environment configuration",[982,1149,1150,1153],{},[758,1151,1152],{},"Poetry"," - Dependency management",[746,1155,1156],{},"The sync tool is under 700 lines across three files. No async complexity, no heavy frameworks - just straightforward code that's easy to understand and modify.",[1070,1158,1160],{"id":1159},"documentation-site-vue-nuxt","Documentation Site (Vue + Nuxt)",[746,1162,1163],{},"The synced Markdown files power a documentation site built with:",[979,1165,1166,1172,1178,1184],{},[982,1167,1168,1171],{},[758,1169,1170],{},"Vue 3"," - Modern reactive framework",[982,1173,1174,1177],{},[758,1175,1176],{},"Nuxt 3"," - Full-stack Vue framework with excellent DX",[982,1179,1180,1183],{},[758,1181,1182],{},"Nuxt Content"," - Markdown-based CMS that reads our synced files directly",[982,1185,1186,1189],{},[758,1187,1188],{},"Nuxt UI"," - Beautiful, accessible component library",[746,1191,1192],{},"This combination is particularly powerful: Nuxt Content automatically parses our YAML frontmatter and builds navigation from the hierarchy metadata. The synced Markdown files become a fully navigable documentation site with zero additional configuration.",[746,1194,1195],{},"The Vue + Nuxt ecosystem is our go-to for content-heavy sites. It's fast, SEO-friendly, and the developer experience is outstanding.",[750,1197,1199],{"id":1198},"obstacles-we-overcame","Obstacles We Overcame",[1070,1201,1203],{"id":1202},"recursive-block-fetching","Recursive Block Fetching",[746,1205,1206],{},"Notion's API returns blocks without their children. We implemented recursive fetching:",[1208,1209,1213],"pre",{"className":1210,"code":1211,"language":1212,"meta":873,"style":873},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","def process_blocks_recursive(blocks_list, indent=0):\n    for block in blocks_list:\n        builder.process_block(block, indent)\n\n        if block.get(\"has_children\", False):\n            child_blocks = notion_client.get_blocks(block[\"id\"])\n            process_blocks_recursive(child_blocks, indent + 1)\n","python",[1047,1214,1215,1252,1271,1294,1300,1330,1363],{"__ignoreMap":873},[1216,1217,1220,1224,1228,1232,1236,1239,1242,1245,1249],"span",{"class":1218,"line":1219},"line",1,[1216,1221,1223],{"class":1222},"spNyl","def",[1216,1225,1227],{"class":1226},"s2Zo4"," process_blocks_recursive",[1216,1229,1231],{"class":1230},"sMK4o","(",[1216,1233,1235],{"class":1234},"sHdIc","blocks_list",[1216,1237,1238],{"class":1230},",",[1216,1240,1241],{"class":1234}," indent",[1216,1243,1244],{"class":1230},"=",[1216,1246,1248],{"class":1247},"sbssI","0",[1216,1250,1251],{"class":1230},"):\n",[1216,1253,1254,1258,1262,1265,1268],{"class":1218,"line":874},[1216,1255,1257],{"class":1256},"s7zQu","    for",[1216,1259,1261],{"class":1260},"sTEyZ"," block ",[1216,1263,1264],{"class":1256},"in",[1216,1266,1267],{"class":1260}," blocks_list",[1216,1269,1270],{"class":1230},":\n",[1216,1272,1274,1277,1279,1282,1284,1287,1289,1291],{"class":1218,"line":1273},3,[1216,1275,1276],{"class":1260},"        builder",[1216,1278,772],{"class":1230},[1216,1280,1281],{"class":1226},"process_block",[1216,1283,1231],{"class":1230},[1216,1285,1286],{"class":1226},"block",[1216,1288,1238],{"class":1230},[1216,1290,1241],{"class":1226},[1216,1292,1293],{"class":1230},")\n",[1216,1295,1297],{"class":1218,"line":1296},4,[1216,1298,1299],{"emptyLinePlaceholder":888},"\n",[1216,1301,1303,1306,1309,1311,1314,1316,1319,1323,1325,1327],{"class":1218,"line":1302},5,[1216,1304,1305],{"class":1256},"        if",[1216,1307,1308],{"class":1260}," block",[1216,1310,772],{"class":1230},[1216,1312,1313],{"class":1226},"get",[1216,1315,1231],{"class":1230},[1216,1317,1318],{"class":1230},"\"",[1216,1320,1322],{"class":1321},"sfazB","has_children",[1216,1324,1318],{"class":1230},[1216,1326,1238],{"class":1230},[1216,1328,1329],{"class":1230}," False):\n",[1216,1331,1333,1336,1338,1341,1343,1346,1348,1350,1353,1355,1358,1360],{"class":1218,"line":1332},6,[1216,1334,1335],{"class":1260},"            child_blocks ",[1216,1337,1244],{"class":1230},[1216,1339,1340],{"class":1260}," notion_client",[1216,1342,772],{"class":1230},[1216,1344,1345],{"class":1226},"get_blocks",[1216,1347,1231],{"class":1230},[1216,1349,1286],{"class":1226},[1216,1351,1352],{"class":1230},"[",[1216,1354,1318],{"class":1230},[1216,1356,1357],{"class":1321},"id",[1216,1359,1318],{"class":1230},[1216,1361,1362],{"class":1230},"])\n",[1216,1364,1366,1369,1371,1374,1376,1379,1382,1385],{"class":1218,"line":1365},7,[1216,1367,1368],{"class":1226},"            process_blocks_recursive",[1216,1370,1231],{"class":1230},[1216,1372,1373],{"class":1226},"child_blocks",[1216,1375,1238],{"class":1230},[1216,1377,1378],{"class":1226}," indent ",[1216,1380,1381],{"class":1230},"+",[1216,1383,1384],{"class":1247}," 1",[1216,1386,1293],{"class":1230},[1070,1388,1390],{"id":1389},"database-permission-gaps","Database Permission Gaps",[746,1392,1393,1394,1397],{},"Not all databases visible in search are actually accessible for querying. We added a ",[1047,1395,1396],{},"--test-database-access"," mode that probes each database before attempting sync.",[1070,1399,1401],{"id":1400},"large-workspace-performance","Large Workspace Performance",[746,1403,1404],{},"Syncing 4000+ pages needed visibility. We added progress bars and timing breakdowns:",[1208,1406,1411],{"className":1407,"code":1409,"language":1410},[1408],"language-text","[=================>              ] 53.2% (213/400)\n\nTiming breakdown:\n   Collecting pages:    12.3s\n   Collecting metadata: 45.2s\n   Syncing pages:       3m 21.5s\n","text",[1047,1412,1409],{"__ignoreMap":873},[750,1414,1416],{"id":1415},"results","Results",[746,1418,1419],{},"After 3 days of development:",[979,1421,1422,1428,1434,1440,1446],{},[982,1423,1424,1427],{},[758,1425,1426],{},"4000+ pages synced"," in under 2 hours",[982,1429,1430,1433],{},[758,1431,1432],{},"Zero manual intervention"," - runs via console",[982,1435,1436,1439],{},[758,1437,1438],{},"Full asset backup"," - all images and files stored locally",[982,1441,1442,1445],{},[758,1443,1444],{},"Navigation-ready output"," - integrates with Nuxt Content",[982,1447,1448,1451],{},[758,1449,1450],{},"Complete database support"," - renders as Markdown tables",[746,1453,1454],{},"Our entire Notion workspace is now a Git repository of Markdown files. Searchable, versionable, and completely under our control.",[750,1456,1458],{"id":1457},"why-open-source","Why Open Source?",[746,1460,1461],{},"We're going to release this tool publicly because:",[746,1463,1464],{},[758,1465,1466],{},"1. Others face the same problem",[746,1468,1469],{},"Notion lock-in is real. Whether you're migrating to another tool, need proper backups, or want to publish content statically - the native export doesn't cut it.",[746,1471,1472],{},[758,1473,1474],{},"2. It's not our core business",[746,1476,1477],{},"We're a software development company, not a SaaS vendor. This tool solves our problem and might solve yours too. Open sourcing it costs us nothing and helps the community.",[746,1479,1480],{},[758,1481,1482],{},"3. Community improvements",[746,1484,1485],{},"The tool works for our use case but could be extended. Incremental sync, bidirectional editing, different output formats - contributions are welcome.",[750,1487,1489],{"id":1488},"lessons-learned","Lessons Learned",[746,1491,1492],{},[758,1493,1494],{},"Start with the API documentation",[746,1496,1497],{},"Notion's block structure is more complex than it looks. Reading the docs thoroughly before coding saved refactoring time.",[746,1499,1500],{},[758,1501,1502],{},"Handle expiring URLs immediately",[746,1504,1505],{},"We made downloading assets a core feature from day one. This saved us from discovering the URL expiration issue in production.",[746,1507,1508],{},[758,1509,1510],{},"Build for incremental testing",[746,1512,1513,1514,1517,1518,1521],{},"The ",[1047,1515,1516],{},"--limit"," and ",[1047,1519,1520],{},"--page"," flags made development fast. Testing with 5 pages instead of 400 is a massive time saver.",[746,1523,1524],{},[758,1525,1526],{},"Simple beats clever",[746,1528,1529],{},"Synchronous, straightforward code over async complexity. The sync runs nightly - saving 30 seconds with async isn't worth the debugging overhead.",[750,1531,1533],{"id":1532},"get-the-tool","Get the Tool",[746,1535,1536],{},"The tool will be available on GitHub shortly. It's designed to be self-contained:",[1538,1539,1540,1550,1553,1560],"ol",{},[982,1541,1542,1543],{},"Create a Notion integration at ",[1544,1545,1549],"a",{"href":1546,"rel":1547},"https://www.notion.so/my-integrations",[1548],"nofollow","notion.so/my-integrations",[982,1551,1552],{},"Share your root pages with the integration",[982,1554,1555,1556,1559],{},"Set up ",[1047,1557,1558],{},".env"," with your API key and root page IDs",[982,1561,1562,1563],{},"Run ",[1047,1564,1565],{},"poetry run notion-sync",[746,1567,1568],{},"Watch our GitHub for the release announcement.",[750,1570,1572],{"id":1571},"conclusion","Conclusion",[746,1574,1575],{},"Leaving a tool like Notion after years of use is daunting. The fear of losing institutional knowledge is real. But with the right migration tooling, it becomes manageable.",[746,1577,1578],{},"Our new approach - storing documentation directly with clients - is cleaner and more secure. The Notion export tool ensured we didn't lose anything in the transition.",[746,1580,1581],{},"If you're considering a similar move, or just want reliable Notion backups, the tool is there for you. Fork it, adapt it, contribute back.",[1583,1584],"hr",{},[746,1586,1587],{},[1588,1589,1590,1591,1596],"em",{},"Building developer tools or planning a platform migration? ",[1544,1592,1595],{"href":1593,"rel":1594},"https://musictechlab.io/contact",[1548],"Contact MusicTech Lab"," - we help companies solve complex technical challenges in the music industry.",[1598,1599,1600],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":873,"searchDepth":874,"depth":874,"links":1602},[1603,1604,1605,1606,1609,1613,1618,1619,1620,1621,1622],{"id":939,"depth":874,"text":940},{"id":973,"depth":874,"text":974},{"id":1002,"depth":874,"text":1003},{"id":1041,"depth":874,"text":1042,"children":1607},[1608],{"id":1072,"depth":1273,"text":1073},{"id":1107,"depth":874,"text":1108,"children":1610},[1611,1612],{"id":1114,"depth":1273,"text":1115},{"id":1159,"depth":1273,"text":1160},{"id":1198,"depth":874,"text":1199,"children":1614},[1615,1616,1617],{"id":1202,"depth":1273,"text":1203},{"id":1389,"depth":1273,"text":1390},{"id":1400,"depth":1273,"text":1401},{"id":1415,"depth":874,"text":1416},{"id":1457,"depth":874,"text":1458},{"id":1488,"depth":874,"text":1489},{"id":1532,"depth":874,"text":1533},{"id":1571,"depth":874,"text":1572},"2024-12-23T00:00:00.000Z","How we built an automated Notion-to-Markdown sync tool in 3 days, why we left Notion, and why we open-sourced the solution.",{"src":1626},"/images/blog/musictechlab_blog_notion-to-markdown-sync-tool.webp",{"enabled":888,"items":1628},[1629,1632,1634,1637],{"text":1630,"icon":1631},"4,000+ Notion pages were synced to Markdown in under 2 hours with a custom Python tool.","i-lucide-zap",{"text":1633,"icon":892},"Notion image URLs expire after about 1 hour, so assets must be downloaded and stored locally.",{"text":1635,"icon":1636},"Synchronous, simple code beat async complexity for a nightly batch job under 700 lines.","i-lucide-code",{"text":1638,"icon":1639},"Storing docs in client systems eliminated Notion lock-in and simplified data ownership.","i-lucide-lock",{},{"title":546,"description":1624},[903,902],"GWlq_ZfrgW_CtpBmxRBz9fTsuRY4qYgxYGGl3Px8tF0",{"id":1645,"title":450,"authors":1646,"badge":741,"body":1649,"category":881,"client":741,"date":1765,"description":1766,"extension":884,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":1767,"keyTakeaways":1769,"meta":1783,"navigation":888,"path":451,"seo":1784,"status":741,"stem":452,"tags":1785,"teaser":741,"__hash__":1786,"score":1296},"posts/blog/software-development/dev-meeting-kubernetes-is-a-framework.md",[1647],{"name":915,"to":916,"avatar":1648},{"src":918},{"type":743,"value":1650,"toc":1759},[1651,1655,1658,1661,1676,1680,1691,1698,1702,1719,1729,1733,1740],[750,1652,1654],{"id":1653},"what-is-kubernetes","What is Kubernetes?",[746,1656,1657],{},"Kubernetes (K8s) is a portable, extensible, open-source platform for managing containerised workloads and services. It supports declarative configuration and automation. Google open-sourced the project in 2014, building on over 15 years of experience running production workloads at scale.",[746,1659,1660],{},"The name originates from Greek, meaning \"helmsman\" or \"pilot.\" K8s is shorthand — counting the eight letters between \"K\" and \"s.\"",[778,1662,1664,1668,1672],{"className":1663},[781,782,856,784,785],[787,1665],{"description":1666,"title":1667},"Automates deployment, scaling, and management of containerised applications across clusters.","Container Orchestration",[787,1669],{"description":1670,"title":1671},"Define desired state in YAML — Kubernetes ensures the cluster matches it.","Declarative Config",[787,1673],{"description":1674,"title":1675},"Restarts failed containers, replaces nodes, and reschedules workloads automatically.","Self-Healing",[750,1677,1679],{"id":1678},"googles-problem-with-scale","Google's Problem with Scale",[979,1681,1682,1685,1688],{},[982,1683,1684],{},"Google ran millions of data jobs across datacenters — for search, indexing, and analytics",[982,1686,1687],{},"As the business expanded, managing multiple tenants and diverse workloads became increasingly difficult",[982,1689,1690],{},"They needed a standard way to schedule resources automatically at massive scale",[746,1692,1693,1694,1697],{},"This led to ",[758,1695,1696],{},"Borg",", Google's internal cluster manager — the direct predecessor to Kubernetes.",[750,1699,1701],{"id":1700},"how-kubernetes-was-born","How Kubernetes Was Born",[979,1703,1704,1707,1713,1716],{},[982,1705,1706],{},"Google extracted its resource management approach from Borg into an open-source project",[982,1708,1709,1710],{},"Together with Intel and Red Hat, they founded the ",[758,1711,1712],{},"Cloud Native Computing Foundation (CNCF)",[982,1714,1715],{},"CNCF became the governance body standardising container orchestration and the broader cloud-native ecosystem",[982,1717,1718],{},"Kubernetes quickly became the industry standard for deploying and managing containers",[1720,1721,1722],"tip",{},[746,1723,1724,1725,1728],{},"Kubernetes is not just a tool — it's a ",[758,1726,1727],{},"framework",". Through Operators and Custom Resource Definitions (CRDs), teams can extend it to manage any type of workload, from databases to ML pipelines.",[750,1730,1732],{"id":1731},"summary","Summary",[746,1734,1735,1736,1739],{},"Many thanks to ",[758,1737,1738],{},"Marcin Karkocha"," for presenting on Kubernetes at our Dev Meeting. The live demo showed theory in action — deploying and scaling containers in real time.",[778,1741,1746,1754],{"className":1742},[1743,1744,1745,785],"flex","flex-wrap","gap-3",[1747,1748],"u-button",{"color":1749,"label":1750,"target":1751,"to":1752,"variant":1753},"primary","Marcin Karkocha on LinkedIn","_blank","https://www.linkedin.com/in/mkarkocha/","subtle",[1747,1755],{"color":1756,"label":1757,"target":1751,"to":1758,"variant":1753},"neutral","Kubernetes Docs","https://kubernetes.io/docs/home/",{"title":873,"searchDepth":874,"depth":874,"links":1760},[1761,1762,1763,1764],{"id":1653,"depth":874,"text":1654},{"id":1678,"depth":874,"text":1679},{"id":1700,"depth":874,"text":1701},{"id":1731,"depth":874,"text":1732},"2023-01-05T00:00:00.000Z","Exploring Kubernetes as a framework: from Google's scaling challenges and Borg to Operators, CRDs, and modern container orchestration basics.",{"src":1768},"/images/blog/musictechlab_blog_dev-meeting-kubernetes-is-a-framework.webp",{"enabled":888,"items":1770},[1771,1774,1777,1780],{"text":1772,"icon":1773},"Kubernetes evolved from Google's internal Borg system after 15+ years of production use.","i-lucide-blocks",{"text":1775,"icon":1776},"It is a framework, not just a tool. CRDs and Operators extend it to any workload.","i-lucide-layers",{"text":1778,"icon":1779},"Self-healing: auto-restarts failed containers and reschedules workloads across nodes.","i-lucide-shield",{"text":1781,"icon":1782},"CNCF governs Kubernetes and the broader cloud-native ecosystem.","i-lucide-globe",{},{"title":450,"description":1766},[902,903],"RcS-TiAWU6Us-_9EdUKIrZKVB0qAQggp7AK2p-0wOAo",{"id":1788,"title":486,"authors":1789,"badge":741,"body":1794,"category":881,"client":741,"date":2222,"description":2223,"extension":884,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":2224,"keyTakeaways":2226,"meta":2236,"navigation":888,"path":487,"seo":2237,"status":741,"stem":488,"tags":2238,"teaser":741,"__hash__":2239,"score":1296},"posts/blog/software-development/git-better-1-see-more-with-a-commit-message-convention.md",[1790],{"name":1791,"avatar":1792},"Paweł Glimos",{"src":1793},"/images/people/pawel-glimos.webp",{"type":743,"value":1795,"toc":2208},[1796,1799,1801,1805,1808,1814,1817,1835,1838,1840,1844,1847,1856,1862,1972,1978,2042,2044,2048,2051,2057,2060,2064,2097,2101,2114,2118,2125,2131,2135,2141,2152,2154,2158,2161,2183,2187,2193,2195,2199,2202,2205],[746,1797,1798],{},"Many of us don't pay much attention to commit messages. But when we need to look for a specific change, or we are wondering why an addition was made, problems appear. What if I told you, there is a simple and easy way to greatly improve the experience with git history.",[1583,1800],{},[750,1802,1804],{"id":1803},"troubling-reality","Troubling Reality",[746,1806,1807],{},"Let's be honest, we all have seen (and been a part of) such git logs:",[1208,1809,1812],{"className":1810,"code":1811,"language":1410},[1408],"ad8621a Fix a bug in the feature\n16b36c6 Addressed a PR comment\n23ad9ad You can now download form correctly through the main website\n21672sd Typing for mypy\n",[1047,1813,1811],{"__ignoreMap":873},[746,1815,1816],{},"These messages try to describe what changes have been made, but ultimately are not much of use. Why?",[778,1818,1820,1825,1830],{"className":1819},[781,782,856,784,785],[787,1821],{"description":1822,"icon":1823,"title":1824},"They don't point to the place in the code where the change was made.","i-lucide-map-pin-off","No Location",[787,1826],{"description":1827,"icon":1828,"title":1829},"They don't specify what type of change was made, making it impossible to filter.","i-lucide-filter-x","No Type",[787,1831],{"description":1832,"icon":1833,"title":1834},"Each one is written in a different style — no pattern, no structure.","i-lucide-shuffle","Inconsistent Style",[746,1836,1837],{},"All of these problems may be solved by adopting a git commit message convention into the project, and today I am going to show you one — Karma.",[1583,1839],{},[750,1841,1843],{"id":1842},"what-is-karma","What Is Karma?",[746,1845,1846],{},"Today, we are not talking about famous religious-based \"you get what you do\". However, that analogy is quite fitting as karma will sooner or later get you, if you are not using proper commit messages.",[746,1848,1849,1850,1855],{},"The Karma I want to show you is a git commit message convention, birthed from the ",[1544,1851,1854],{"href":1852,"rel":1853},"https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#",[1548],"AngularJS Git Commit Conventions google document",", adapted to be used with any project and language. It aims to provide more useful, clear and precise information when browsing the git history, while also helping to easily achieve other goals:",[746,1857,1858,1861],{},[758,1859,1860],{},"Automatically generate changelog"," — When your git messages follow a convention, it is much easier to generate a changelog:",[1208,1863,1867],{"className":1864,"code":1865,"language":1866,"meta":873,"style":873},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git log \u003Clast tag> HEAD --pretty=format:%s\n# All commits since the last release\n\n23ad9ad feat(proxy): Added support for bi-directional streams\n21672sd fix(proto): Fixed missing type annotation\n","bash",[1047,1868,1869,1899,1905,1909,1943],{"__ignoreMap":873},[1216,1870,1871,1875,1878,1881,1884,1887,1890,1893,1896],{"class":1218,"line":1219},[1216,1872,1874],{"class":1873},"sBMFI","git",[1216,1876,1877],{"class":1321}," log",[1216,1879,1880],{"class":1230}," \u003C",[1216,1882,1883],{"class":1321},"last",[1216,1885,1886],{"class":1321}," ta",[1216,1888,1889],{"class":1260},"g",[1216,1891,1892],{"class":1230},">",[1216,1894,1895],{"class":1321}," HEAD",[1216,1897,1898],{"class":1321}," --pretty=format:%s\n",[1216,1900,1901],{"class":1218,"line":874},[1216,1902,1904],{"class":1903},"sHwdD","# All commits since the last release\n",[1216,1906,1907],{"class":1218,"line":1273},[1216,1908,1299],{"emptyLinePlaceholder":888},[1216,1910,1911,1914,1917,1919,1922,1925,1928,1931,1934,1937,1940],{"class":1218,"line":1296},[1216,1912,1913],{"class":1873},"23ad9ad",[1216,1915,1916],{"class":1321}," feat",[1216,1918,1231],{"class":1230},[1216,1920,1921],{"class":1873},"proxy",[1216,1923,1924],{"class":1230},")",[1216,1926,1927],{"class":1321},":",[1216,1929,1930],{"class":1321}," Added",[1216,1932,1933],{"class":1321}," support",[1216,1935,1936],{"class":1321}," for",[1216,1938,1939],{"class":1321}," bi-directional",[1216,1941,1942],{"class":1321}," streams\n",[1216,1944,1945,1948,1951,1953,1956,1958,1960,1963,1966,1969],{"class":1218,"line":1302},[1216,1946,1947],{"class":1873},"21672sd",[1216,1949,1950],{"class":1321}," fix",[1216,1952,1231],{"class":1230},[1216,1954,1955],{"class":1873},"proto",[1216,1957,1924],{"class":1230},[1216,1959,1927],{"class":1321},[1216,1961,1962],{"class":1321}," Fixed",[1216,1964,1965],{"class":1321}," missing",[1216,1967,1968],{"class":1321}," type",[1216,1970,1971],{"class":1321}," annotation\n",[746,1973,1974,1977],{},[758,1975,1976],{},"Filter out unimportant commits"," — Using unified descriptions we can easily look through the history and pick only the interesting ones:",[1208,1979,1981],{"className":1864,"code":1980,"language":1866,"meta":873,"style":873},"git log \u003Clast release> HEAD --grep feat\n# All new features since the last release\n\n23ad9ad feat(proxy): Added support for bi-directional streams\n",[1047,1982,1983,2009,2014,2018],{"__ignoreMap":873},[1216,1984,1985,1987,1989,1991,1993,1996,1999,2001,2003,2006],{"class":1218,"line":1219},[1216,1986,1874],{"class":1873},[1216,1988,1877],{"class":1321},[1216,1990,1880],{"class":1230},[1216,1992,1883],{"class":1321},[1216,1994,1995],{"class":1321}," releas",[1216,1997,1998],{"class":1260},"e",[1216,2000,1892],{"class":1230},[1216,2002,1895],{"class":1321},[1216,2004,2005],{"class":1321}," --grep",[1216,2007,2008],{"class":1321}," feat\n",[1216,2010,2011],{"class":1218,"line":874},[1216,2012,2013],{"class":1903},"# All new features since the last release\n",[1216,2015,2016],{"class":1218,"line":1273},[1216,2017,1299],{"emptyLinePlaceholder":888},[1216,2019,2020,2022,2024,2026,2028,2030,2032,2034,2036,2038,2040],{"class":1218,"line":1296},[1216,2021,1913],{"class":1873},[1216,2023,1916],{"class":1321},[1216,2025,1231],{"class":1230},[1216,2027,1921],{"class":1873},[1216,2029,1924],{"class":1230},[1216,2031,1927],{"class":1321},[1216,2033,1930],{"class":1321},[1216,2035,1933],{"class":1321},[1216,2037,1936],{"class":1321},[1216,2039,1939],{"class":1321},[1216,2041,1942],{"class":1321},[1583,2043],{},[750,2045,2047],{"id":2046},"how-to-use-it","How to Use It?",[746,2049,2050],{},"The general structure of a Karma based message is:",[1208,2052,2055],{"className":2053,"code":2054,"language":1410},[1408],"\u003Ctype>(\u003Cscope>): \u003Csubject>\n\n\u003Cbody>\n\n\u003Cfooter>\n",[1047,2056,2054],{"__ignoreMap":873},[746,2058,2059],{},"It is really simple to remember and once you start using it, you will almost never have to consult the reference. Each line can have a maximum of 80 characters and the second line must always be blank.",[1070,2061,2063],{"id":2062},"allowed-types","Allowed Types",[778,2065,2068,2073,2078,2083,2087,2092],{"className":2066},[781,2067,856,784,785],"grid-cols-2",[787,2069],{"description":2070,"icon":2071,"title":2072},"New features, or ones that changed behaviour.","i-lucide-sparkles","feat",[787,2074],{"description":2075,"icon":2076,"title":2077},"Bug fixes in ready features.","i-lucide-bug","fix",[787,2079],{"description":2080,"icon":2081,"title":2082},"Documentation, both in and outside code.","i-lucide-book-open","docs",[787,2084],{"description":2085,"icon":2086,"title":1598},"Formatting, typos — no code change.","i-lucide-paintbrush",[787,2088],{"description":2089,"icon":2090,"title":2091},"Refactoring production code.","i-lucide-refresh-cw","refactor",[787,2093],{"description":2094,"icon":2095,"title":2096},"All things test related.","i-lucide-test-tubes","test",[1070,2098,2100],{"id":2099},"scope","Scope",[746,2102,2103,2104,2107,2108,2107,2110,2113],{},"The scope is a description of what part of the code was affected — for example: ",[1047,2105,2106],{},"service",", ",[1047,2109,1921],{},[1047,2111,2112],{},"runner",", etc.",[1070,2115,2117],{"id":2116},"body-footer","Body & Footer",[746,2119,2120,2121,2124],{},"In the ",[758,2122,2123],{},"body",", include a sentence on why the changes were made. Be descriptive! If someone looked this far inside your commit, they will appreciate every bit of information they can find.",[746,2126,2120,2127,2130],{},[758,2128,2129],{},"footer",", you can reference issues. Multiple issues may be used at the same time.",[1070,2132,2134],{"id":2133},"full-example","Full Example",[1208,2136,2139],{"className":2137,"code":2138,"language":1410},[1408],"fix(service): Verify the content type that is returned to the adapter\n\nAPI can respond with text/plain content type instead of JSON.\nTo ensure proper operation it is verified before further processing.\n\nCloses #112\n",[1047,2140,2138],{"__ignoreMap":873},[1720,2142,2143],{},[746,2144,2145,2146,2151],{},"Further and more precise explanations can be found on the official ",[1544,2147,2150],{"href":2148,"rel":2149},"https://karma-runner.github.io/4.0/dev/git-commit-msg.html",[1548],"Karma website",", which I highly recommend as a reference.",[1583,2153],{},[750,2155,2157],{"id":2156},"why-should-i-bother-if-i-never-use-git-log","Why Should I Bother If I Never Use Git Log?",[746,2159,2160],{},"You may say: \"What you wrote looks nice, but I honestly can't remember the last time I actually used git log.\" And I have to agree — during everyday work it rarely happens that we need to use git log extensively. Here are some additional thoughts:",[778,2162,2164,2168,2173,2178],{"className":2163},[781,782,783,784,785],[787,2165],{"description":2166,"icon":1779,"title":2167},"Good commit messages are like backup — you think you don't need one until you do.","Like Backup",[787,2169],{"description":2170,"icon":2171,"title":2172},"As long as you state the rules and stick to them, it will be beneficial. Even just the first line of Karma is enough in most scenarios.","i-lucide-sliders","Fully Customizable",[787,2174],{"description":2175,"icon":2176,"title":2177},"Think about a situation where you take over a project. Which kind of commit messages would you prefer? Maintain your project so you're not ashamed when someone else reads it.","i-lucide-arrow-right-left","Handover-Ready",[787,2179],{"description":2180,"icon":2181,"title":2182},"Maybe you would be using your git history more if it was better structured, not the other way around.","i-lucide-search","Better History = More Usage",[1070,2184,2186],{"id":2185},"convention-levels","Convention Levels",[1208,2188,2191],{"className":2189,"code":2190,"language":1410},[1408],"minimal    fix(service): Verify the content type returned to the adapter\n\nambitious  API can respond with text/plain content type instead of JSON.\n           To ensure proper operation it is verified before further processing.\n\nfull       Fixes #112\n",[1047,2192,2190],{"__ignoreMap":873},[1583,2194],{},[750,2196,2198],{"id":2197},"closing-off","Closing Off",[746,2200,2201],{},"If you are convinced, share the idea with your team and try sticking to Karma rules for your next project. You can also convert to the convention in an ongoing one. Changing git messages will rarely break anything in your environment, so it doesn't hurt to try — it is never too late to Git Better.",[746,2203,2204],{},"This article is a part of the ongoing series \"Git Better with MusicTech Lab\". In the next one, we are gonna cover cherry-picking — when it is a good practice and how to avoid common errors using it.",[1598,2206,2207],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":873,"searchDepth":874,"depth":874,"links":2209},[2210,2211,2212,2218,2221],{"id":1803,"depth":874,"text":1804},{"id":1842,"depth":874,"text":1843},{"id":2046,"depth":874,"text":2047,"children":2213},[2214,2215,2216,2217],{"id":2062,"depth":1273,"text":2063},{"id":2099,"depth":1273,"text":2100},{"id":2116,"depth":1273,"text":2117},{"id":2133,"depth":1273,"text":2134},{"id":2156,"depth":874,"text":2157,"children":2219},[2220],{"id":2185,"depth":1273,"text":2186},{"id":2197,"depth":874,"text":2198},"2019-12-27T00:00:00.000Z","Using a consistent style of commit messages can increase the effectiveness of a team that develops bespoke software.",{"src":2225},"/images/blog/musictechlab_blog_git-better-1-see-more-with-a-commit-message-convention.webp",{"enabled":888,"items":2227},[2228,2231,2234],{"text":2229,"icon":2230},"Karma convention adds type and scope to every commit for instant context.","i-lucide-git-branch",{"text":2232,"icon":2233},"Structured messages enable auto-generated changelogs with one git command.","i-lucide-terminal",{"text":2235,"icon":1636},"Filtering by type (feat, fix, chore) makes git history instantly searchable.",{},{"title":486,"description":2223},[902,903],"QP6th8QrePf6Wdk3hdfaocqMZbCq4aW2gCDsO0CapMU",{"id":2241,"title":558,"authors":2242,"badge":2245,"body":2248,"category":881,"client":741,"date":9470,"description":9471,"extension":884,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":9472,"keyTakeaways":9475,"meta":9485,"navigation":888,"path":559,"seo":9486,"status":741,"stem":560,"tags":9487,"teaser":741,"__hash__":9488,"score":1273},"posts/blog/software-development/integrating-signnow-e-signatures-into-your-django-application.md",[2243],{"name":915,"to":916,"avatar":2244},{"src":918},{"label":2246,"color":2247},"API Integration","#6366f1",{"type":743,"value":2249,"toc":9446},[2250,2253,2268,2271,2275,2278,2358,2361,2365,2368,2400,2403,2440,2444,2472,2478,2550,2564,2567,2772,2776,2782,2786,2789,3784,3805,3809,3812,4560,4564,4567,4581,4584,4985,4989,4992,5402,5406,5416,6158,6161,6167,6171,6174,7483,7504,7508,7511,8208,8213,8216,8316,8319,8372,8376,8379,8764,8768,8771,9243,9246,9249,9253,9263,9267,9270,9274,9284,9288,9291,9295,9307,9309,9312,9436,9443],[746,2251,2252],{},"Electronic signatures have gone from \"nice to have\" to a hard requirement for any platform that deals with contracts, agreements, or compliance documents. Whether you're onboarding beta testers, sending NDAs, or processing deposit confirmations, manually emailing PDFs and chasing wet signatures simply doesn't scale.",[746,2254,2255,2256,2261,2262,2267],{},"At ",[1544,2257,2260],{"href":2258,"rel":2259},"https://beatbuddy.pro",[1548],"BeatBuddy",", we needed a way to automatically send documents for signature as part of our tester onboarding flow - without leaving the Django admin. We chose ",[1544,2263,2266],{"href":2264,"rel":2265},"https://www.signnow.com/developers",[1548],"airSlate SignNow"," for its developer-friendly REST API, generous sandbox environment, and competitive pricing.",[746,2269,2270],{},"This article walks through how we built the integration end-to-end: authenticating with OAuth2, uploading documents, sending signing invites, handling webhooks, and managing the full document lifecycle - all from within a Django + Celery stack.",[750,2272,2274],{"id":2273},"why-signnow","Why SignNow?",[746,2276,2277],{},"Before diving into code, here's why we picked SignNow over alternatives like DocuSign or HelloSign:",[2279,2280,2281,2294],"table",{},[2282,2283,2284],"thead",{},[2285,2286,2287,2291],"tr",{},[2288,2289,2290],"th",{},"Criteria",[2288,2292,2293],{},"SignNow",[2295,2296,2297,2308,2318,2328,2338,2348],"tbody",{},[2285,2298,2299,2305],{},[2300,2301,2302],"td",{},[758,2303,2304],{},"Sandbox",[2300,2306,2307],{},"Free, 2,000 signature invites for testing",[2285,2309,2310,2315],{},[2300,2311,2312],{},[758,2313,2314],{},"API style",[2300,2316,2317],{},"Clean REST API with JSON payloads",[2285,2319,2320,2325],{},[2300,2321,2322],{},[758,2323,2324],{},"Authentication",[2300,2326,2327],{},"Standard OAuth2 (password grant)",[2285,2329,2330,2335],{},[2300,2331,2332],{},[758,2333,2334],{},"Webhooks",[2300,2336,2337],{},"Per-document event subscriptions",[2285,2339,2340,2345],{},[2300,2341,2342],{},[758,2343,2344],{},"Pricing",[2300,2346,2347],{},"Significantly cheaper than DocuSign at scale",[2285,2349,2350,2355],{},[2300,2351,2352],{},[758,2353,2354],{},"SDKs",[2300,2356,2357],{},"Official SDKs for Python, Node.js, PHP, Java, C#",[746,2359,2360],{},"For our use case - programmatically sending documents for a single signer - SignNow's API was straightforward and well-documented.",[750,2362,2364],{"id":2363},"architecture-overview","Architecture overview",[746,2366,2367],{},"Here's how the integration fits into our Django application:",[1208,2369,2373],{"className":2370,"code":2371,"language":2372,"meta":873,"style":873},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","flowchart TD\n    A[\"Django Admin (trigger sign)\"] --> B[\"Celery Worker (async pipeline)\"]\n    B --> C[\"SignNow API (upload, sign)\"]\n    C -- \"webhook\" --> D[\"Webhook View (POST receiver)\"]\n    D --> E[\"Celery Worker (download PDF)\"]\n","mermaid",[1047,2374,2375,2380,2385,2390,2395],{"__ignoreMap":873},[1216,2376,2377],{"class":1218,"line":1219},[1216,2378,2379],{"class":1260},"flowchart TD\n",[1216,2381,2382],{"class":1218,"line":874},[1216,2383,2384],{"class":1260},"    A[\"Django Admin (trigger sign)\"] --> B[\"Celery Worker (async pipeline)\"]\n",[1216,2386,2387],{"class":1218,"line":1273},[1216,2388,2389],{"class":1260},"    B --> C[\"SignNow API (upload, sign)\"]\n",[1216,2391,2392],{"class":1218,"line":1296},[1216,2393,2394],{"class":1260},"    C -- \"webhook\" --> D[\"Webhook View (POST receiver)\"]\n",[1216,2396,2397],{"class":1218,"line":1302},[1216,2398,2399],{"class":1260},"    D --> E[\"Celery Worker (download PDF)\"]\n",[746,2401,2402],{},"The key design decisions:",[1538,2404,2405,2411,2424,2430],{},[982,2406,2407,2410],{},[758,2408,2409],{},"Async everything"," - All SignNow API calls happen in Celery tasks, never in the request cycle",[982,2412,2413,2416,2417,2420,2421],{},[758,2414,2415],{},"Generic relations"," - The signing tracker (",[1047,2418,2419],{},"SignableDocument",") can attach to any Django model via ",[1047,2422,2423],{},"ContentType",[982,2425,2426,2429],{},[758,2427,2428],{},"Idempotent operations"," - The pipeline gracefully handles retries and duplicate webhook events",[982,2431,2432,2435,2436,2439],{},[758,2433,2434],{},"Service layer pattern"," - A thin ",[1047,2437,2438],{},"SignNowService"," class wraps all raw API calls",[750,2441,2443],{"id":2442},"step-1-get-your-signnow-api-credentials","Step 1: Get your SignNow API credentials",[1538,2445,2446,2453,2462],{},[982,2447,2448,2449],{},"Create a free sandbox account at ",[1544,2450,2452],{"href":2264,"rel":2451},[1548],"signnow.com/developers",[982,2454,2455,2456,1517,2459],{},"In the API dashboard, create a new application to get your ",[758,2457,2458],{},"Client ID",[758,2460,2461],{},"Client Secret",[982,2463,2464,2465,2468,2469,1924],{},"Note your sandbox base URL: ",[1047,2466,2467],{},"https://api-eval.signnow.com"," (production uses ",[1047,2470,2471],{},"https://api.signnow.com",[746,2473,2474,2475,2477],{},"Add these to your ",[1047,2476,1558],{}," file:",[1208,2479,2481],{"className":1864,"code":2480,"language":1866,"meta":873,"style":873},"# SignNow e-signature (https://www.signnow.com/developers)\nSIGNNOW_API_BASE_URL=https://api-eval.signnow.com  # Use api.signnow.com for production\nSIGNNOW_BASIC_AUTH=  # Base64-encoded client_id:client_secret\nSIGNNOW_USERNAME=    # Your SignNow account email\nSIGNNOW_PASSWORD=    # Your SignNow account password\nSIGNNOW_WEBHOOK_SECRET=  # For verifying webhook payloads\nSIGNNOW_WEBHOOK_CALLBACK_URL=  # Your public webhook endpoint\n",[1047,2482,2483,2488,2500,2510,2520,2530,2540],{"__ignoreMap":873},[1216,2484,2485],{"class":1218,"line":1219},[1216,2486,2487],{"class":1903},"# SignNow e-signature (https://www.signnow.com/developers)\n",[1216,2489,2490,2493,2495,2497],{"class":1218,"line":874},[1216,2491,2492],{"class":1260},"SIGNNOW_API_BASE_URL",[1216,2494,1244],{"class":1230},[1216,2496,2467],{"class":1321},[1216,2498,2499],{"class":1903},"  # Use api.signnow.com for production\n",[1216,2501,2502,2505,2507],{"class":1218,"line":1273},[1216,2503,2504],{"class":1260},"SIGNNOW_BASIC_AUTH",[1216,2506,1244],{"class":1230},[1216,2508,2509],{"class":1903},"  # Base64-encoded client_id:client_secret\n",[1216,2511,2512,2515,2517],{"class":1218,"line":1296},[1216,2513,2514],{"class":1260},"SIGNNOW_USERNAME",[1216,2516,1244],{"class":1230},[1216,2518,2519],{"class":1903},"    # Your SignNow account email\n",[1216,2521,2522,2525,2527],{"class":1218,"line":1302},[1216,2523,2524],{"class":1260},"SIGNNOW_PASSWORD",[1216,2526,1244],{"class":1230},[1216,2528,2529],{"class":1903},"    # Your SignNow account password\n",[1216,2531,2532,2535,2537],{"class":1218,"line":1332},[1216,2533,2534],{"class":1260},"SIGNNOW_WEBHOOK_SECRET",[1216,2536,1244],{"class":1230},[1216,2538,2539],{"class":1903},"  # For verifying webhook payloads\n",[1216,2541,2542,2545,2547],{"class":1218,"line":1365},[1216,2543,2544],{"class":1260},"SIGNNOW_WEBHOOK_CALLBACK_URL",[1216,2546,1244],{"class":1230},[1216,2548,2549],{"class":1903},"  # Your public webhook endpoint\n",[844,2551,2552],{},[746,2553,1513,2554,2556,2557,2560,2561],{},[1047,2555,2504],{}," value must be the Base64 encoding of ",[1047,2558,2559],{},"client_id:client_secret",". You can generate it with: ",[1047,2562,2563],{},"echo -n \"your_client_id:your_client_secret\" | base64",[746,2565,2566],{},"Load these in your Django settings:",[1208,2568,2571],{"className":1210,"code":2569,"filename":2570,"language":1212,"meta":873,"style":873},"# SignNow e-signature\nSIGNNOW_API_BASE_URL = env.str(\"SIGNNOW_API_BASE_URL\", default=\"https://api.signnow.com\")\nSIGNNOW_BASIC_AUTH = env.str(\"SIGNNOW_BASIC_AUTH\", default=\"\")\nSIGNNOW_USERNAME = env.str(\"SIGNNOW_USERNAME\", default=\"\")\nSIGNNOW_PASSWORD = env.str(\"SIGNNOW_PASSWORD\", default=\"\")\nSIGNNOW_WEBHOOK_SECRET = env.str(\"SIGNNOW_WEBHOOK_SECRET\", default=\"\")\nSIGNNOW_WEBHOOK_CALLBACK_URL = env.str(\"SIGNNOW_WEBHOOK_CALLBACK_URL\", default=\"\")\n","settings/base.py",[1047,2572,2573,2578,2616,2648,2679,2710,2741],{"__ignoreMap":873},[1216,2574,2575],{"class":1218,"line":1219},[1216,2576,2577],{"class":1903},"# SignNow e-signature\n",[1216,2579,2580,2583,2585,2588,2590,2593,2595,2597,2599,2601,2603,2606,2608,2610,2612,2614],{"class":1218,"line":874},[1216,2581,2582],{"class":1260},"SIGNNOW_API_BASE_URL ",[1216,2584,1244],{"class":1230},[1216,2586,2587],{"class":1260}," env",[1216,2589,772],{"class":1230},[1216,2591,2592],{"class":1226},"str",[1216,2594,1231],{"class":1230},[1216,2596,1318],{"class":1230},[1216,2598,2492],{"class":1321},[1216,2600,1318],{"class":1230},[1216,2602,1238],{"class":1230},[1216,2604,2605],{"class":1234}," default",[1216,2607,1244],{"class":1230},[1216,2609,1318],{"class":1230},[1216,2611,2471],{"class":1321},[1216,2613,1318],{"class":1230},[1216,2615,1293],{"class":1230},[1216,2617,2618,2621,2623,2625,2627,2629,2631,2633,2635,2637,2639,2641,2643,2646],{"class":1218,"line":1273},[1216,2619,2620],{"class":1260},"SIGNNOW_BASIC_AUTH ",[1216,2622,1244],{"class":1230},[1216,2624,2587],{"class":1260},[1216,2626,772],{"class":1230},[1216,2628,2592],{"class":1226},[1216,2630,1231],{"class":1230},[1216,2632,1318],{"class":1230},[1216,2634,2504],{"class":1321},[1216,2636,1318],{"class":1230},[1216,2638,1238],{"class":1230},[1216,2640,2605],{"class":1234},[1216,2642,1244],{"class":1230},[1216,2644,2645],{"class":1230},"\"\"",[1216,2647,1293],{"class":1230},[1216,2649,2650,2653,2655,2657,2659,2661,2663,2665,2667,2669,2671,2673,2675,2677],{"class":1218,"line":1296},[1216,2651,2652],{"class":1260},"SIGNNOW_USERNAME ",[1216,2654,1244],{"class":1230},[1216,2656,2587],{"class":1260},[1216,2658,772],{"class":1230},[1216,2660,2592],{"class":1226},[1216,2662,1231],{"class":1230},[1216,2664,1318],{"class":1230},[1216,2666,2514],{"class":1321},[1216,2668,1318],{"class":1230},[1216,2670,1238],{"class":1230},[1216,2672,2605],{"class":1234},[1216,2674,1244],{"class":1230},[1216,2676,2645],{"class":1230},[1216,2678,1293],{"class":1230},[1216,2680,2681,2684,2686,2688,2690,2692,2694,2696,2698,2700,2702,2704,2706,2708],{"class":1218,"line":1302},[1216,2682,2683],{"class":1260},"SIGNNOW_PASSWORD ",[1216,2685,1244],{"class":1230},[1216,2687,2587],{"class":1260},[1216,2689,772],{"class":1230},[1216,2691,2592],{"class":1226},[1216,2693,1231],{"class":1230},[1216,2695,1318],{"class":1230},[1216,2697,2524],{"class":1321},[1216,2699,1318],{"class":1230},[1216,2701,1238],{"class":1230},[1216,2703,2605],{"class":1234},[1216,2705,1244],{"class":1230},[1216,2707,2645],{"class":1230},[1216,2709,1293],{"class":1230},[1216,2711,2712,2715,2717,2719,2721,2723,2725,2727,2729,2731,2733,2735,2737,2739],{"class":1218,"line":1332},[1216,2713,2714],{"class":1260},"SIGNNOW_WEBHOOK_SECRET ",[1216,2716,1244],{"class":1230},[1216,2718,2587],{"class":1260},[1216,2720,772],{"class":1230},[1216,2722,2592],{"class":1226},[1216,2724,1231],{"class":1230},[1216,2726,1318],{"class":1230},[1216,2728,2534],{"class":1321},[1216,2730,1318],{"class":1230},[1216,2732,1238],{"class":1230},[1216,2734,2605],{"class":1234},[1216,2736,1244],{"class":1230},[1216,2738,2645],{"class":1230},[1216,2740,1293],{"class":1230},[1216,2742,2743,2746,2748,2750,2752,2754,2756,2758,2760,2762,2764,2766,2768,2770],{"class":1218,"line":1365},[1216,2744,2745],{"class":1260},"SIGNNOW_WEBHOOK_CALLBACK_URL ",[1216,2747,1244],{"class":1230},[1216,2749,2587],{"class":1260},[1216,2751,772],{"class":1230},[1216,2753,2592],{"class":1226},[1216,2755,1231],{"class":1230},[1216,2757,1318],{"class":1230},[1216,2759,2544],{"class":1321},[1216,2761,1318],{"class":1230},[1216,2763,1238],{"class":1230},[1216,2765,2605],{"class":1234},[1216,2767,1244],{"class":1230},[1216,2769,2645],{"class":1230},[1216,2771,1293],{"class":1230},[750,2773,2775],{"id":2774},"step-2-build-the-api-client-service-layer","Step 2: Build the API client (service layer)",[746,2777,2778,2779,2781],{},"Rather than scattering HTTP calls throughout the codebase, we encapsulated all SignNow API interactions in a single ",[1047,2780,2438],{}," class. This makes testing, error handling, and future changes much simpler.",[1070,2783,2785],{"id":2784},"oauth2-authentication-with-token-caching","OAuth2 authentication with token caching",[746,2787,2788],{},"SignNow uses the OAuth2 password grant to obtain access tokens. Tokens expire after a configurable period (typically 1 hour), so we cache them in Redis with a safety buffer:",[1208,2790,2793],{"className":1210,"code":2791,"filename":2792,"language":1212,"meta":873,"style":873},"import httpx\nfrom django.conf import settings\nfrom django.core.cache import cache\n\nSIGNNOW_TOKEN_CACHE_KEY = \"signnow_access_token\"\nSIGNNOW_TOKEN_TTL_BUFFER = 300  # Refresh 5 min before expiry\n\n\nclass SignNowService:\n    \"\"\"Raw REST client for the SignNow API using httpx.\"\"\"\n\n    _initialized = False\n    _base_url = \"\"\n    _basic_auth = \"\"\n    _username = \"\"\n    _password = \"\"\n\n    @classmethod\n    def _ensure_initialized(cls) -> bool:\n        \"\"\"Initialize SignNow configuration from settings.\"\"\"\n        if cls._initialized:\n            return True\n\n        cls._base_url = getattr(settings, \"SIGNNOW_API_BASE_URL\", \"\")\n        cls._basic_auth = getattr(settings, \"SIGNNOW_BASIC_AUTH\", \"\")\n        cls._username = getattr(settings, \"SIGNNOW_USERNAME\", \"\")\n        cls._password = getattr(settings, \"SIGNNOW_PASSWORD\", \"\")\n\n        if not all([cls._base_url, cls._basic_auth, cls._username, cls._password]):\n            logger.warning(\"SignNow API not fully configured - missing required settings\")\n            return False\n\n        cls._initialized = True\n        return True\n\n    @classmethod\n    def _get_access_token(cls) -> str | None:\n        \"\"\"Get a valid access token, using cache or requesting a new one.\"\"\"\n        cached = cache.get(SIGNNOW_TOKEN_CACHE_KEY)\n        if cached:\n            return cached\n\n        with httpx.Client(timeout=30.0) as client:\n            resp = client.post(\n                f\"{cls._base_url}/oauth2/token\",\n                data={\n                    \"grant_type\": \"password\",\n                    \"username\": cls._username,\n                    \"password\": cls._password,\n                    \"scope\": \"*\",\n                },\n                headers={\n                    \"Authorization\": f\"Basic {cls._basic_auth}\",\n                    \"Content-Type\": \"application/x-www-form-urlencoded\",\n                },\n            )\n            resp.raise_for_status()\n            data = resp.json()\n\n            token = data[\"access_token\"]\n            expires_in = data.get(\"expires_in\", 3600)\n            ttl = max(expires_in - SIGNNOW_TOKEN_TTL_BUFFER, 60)\n            cache.set(SIGNNOW_TOKEN_CACHE_KEY, token, timeout=ttl)\n\n            return token\n","apps/signnow/services/signnow_service.py",[1047,2794,2795,2803,2821,2842,2846,2862,2875,2879,2884,2895,2907,2912,2923,2934,2944,2954,2964,2969,2978,3002,3013,3029,3038,3043,3080,3112,3144,3176,3181,3228,3249,3256,3261,3274,3282,3287,3294,3319,3329,3351,3361,3369,3374,3408,3426,3452,3461,3483,3503,3522,3542,3548,3556,3588,3609,3614,3620,3634,3652,3657,3680,3710,3739,3771,3776],{"__ignoreMap":873},[1216,2796,2797,2800],{"class":1218,"line":1219},[1216,2798,2799],{"class":1256},"import",[1216,2801,2802],{"class":1260}," httpx\n",[1216,2804,2805,2808,2811,2813,2816,2818],{"class":1218,"line":874},[1216,2806,2807],{"class":1256},"from",[1216,2809,2810],{"class":1260}," django",[1216,2812,772],{"class":1230},[1216,2814,2815],{"class":1260},"conf ",[1216,2817,2799],{"class":1256},[1216,2819,2820],{"class":1260}," settings\n",[1216,2822,2823,2825,2827,2829,2832,2834,2837,2839],{"class":1218,"line":1273},[1216,2824,2807],{"class":1256},[1216,2826,2810],{"class":1260},[1216,2828,772],{"class":1230},[1216,2830,2831],{"class":1260},"core",[1216,2833,772],{"class":1230},[1216,2835,2836],{"class":1260},"cache ",[1216,2838,2799],{"class":1256},[1216,2840,2841],{"class":1260}," cache\n",[1216,2843,2844],{"class":1218,"line":1296},[1216,2845,1299],{"emptyLinePlaceholder":888},[1216,2847,2848,2851,2853,2856,2859],{"class":1218,"line":1302},[1216,2849,2850],{"class":1260},"SIGNNOW_TOKEN_CACHE_KEY ",[1216,2852,1244],{"class":1230},[1216,2854,2855],{"class":1230}," \"",[1216,2857,2858],{"class":1321},"signnow_access_token",[1216,2860,2861],{"class":1230},"\"\n",[1216,2863,2864,2867,2869,2872],{"class":1218,"line":1332},[1216,2865,2866],{"class":1260},"SIGNNOW_TOKEN_TTL_BUFFER ",[1216,2868,1244],{"class":1230},[1216,2870,2871],{"class":1247}," 300",[1216,2873,2874],{"class":1903},"  # Refresh 5 min before expiry\n",[1216,2876,2877],{"class":1218,"line":1365},[1216,2878,1299],{"emptyLinePlaceholder":888},[1216,2880,2882],{"class":1218,"line":2881},8,[1216,2883,1299],{"emptyLinePlaceholder":888},[1216,2885,2887,2890,2893],{"class":1218,"line":2886},9,[1216,2888,2889],{"class":1222},"class",[1216,2891,2892],{"class":1873}," SignNowService",[1216,2894,1270],{"class":1230},[1216,2896,2898,2901,2904],{"class":1218,"line":2897},10,[1216,2899,2900],{"class":1256},"    \"\"\"",[1216,2902,2903],{"class":1903},"Raw REST client for the SignNow API using httpx.",[1216,2905,2906],{"class":1256},"\"\"\"\n",[1216,2908,2910],{"class":1218,"line":2909},11,[1216,2911,1299],{"emptyLinePlaceholder":888},[1216,2913,2915,2918,2920],{"class":1218,"line":2914},12,[1216,2916,2917],{"class":1260},"    _initialized ",[1216,2919,1244],{"class":1230},[1216,2921,2922],{"class":1230}," False\n",[1216,2924,2926,2929,2931],{"class":1218,"line":2925},13,[1216,2927,2928],{"class":1260},"    _base_url ",[1216,2930,1244],{"class":1230},[1216,2932,2933],{"class":1230}," \"\"\n",[1216,2935,2937,2940,2942],{"class":1218,"line":2936},14,[1216,2938,2939],{"class":1260},"    _basic_auth ",[1216,2941,1244],{"class":1230},[1216,2943,2933],{"class":1230},[1216,2945,2947,2950,2952],{"class":1218,"line":2946},15,[1216,2948,2949],{"class":1260},"    _username ",[1216,2951,1244],{"class":1230},[1216,2953,2933],{"class":1230},[1216,2955,2957,2960,2962],{"class":1218,"line":2956},16,[1216,2958,2959],{"class":1260},"    _password ",[1216,2961,1244],{"class":1230},[1216,2963,2933],{"class":1230},[1216,2965,2967],{"class":1218,"line":2966},17,[1216,2968,1299],{"emptyLinePlaceholder":888},[1216,2970,2972,2975],{"class":1218,"line":2971},18,[1216,2973,2974],{"class":1230},"    @",[1216,2976,2977],{"class":1873},"classmethod\n",[1216,2979,2981,2984,2987,2989,2992,2994,2997,3000],{"class":1218,"line":2980},19,[1216,2982,2983],{"class":1222},"    def",[1216,2985,2986],{"class":1226}," _ensure_initialized",[1216,2988,1231],{"class":1230},[1216,2990,2991],{"class":1234},"cls",[1216,2993,1924],{"class":1230},[1216,2995,2996],{"class":1230}," ->",[1216,2998,2999],{"class":1873}," bool",[1216,3001,1270],{"class":1230},[1216,3003,3005,3008,3011],{"class":1218,"line":3004},20,[1216,3006,3007],{"class":1256},"        \"\"\"",[1216,3009,3010],{"class":1903},"Initialize SignNow configuration from settings.",[1216,3012,2906],{"class":1256},[1216,3014,3016,3018,3021,3023,3027],{"class":1218,"line":3015},21,[1216,3017,1305],{"class":1256},[1216,3019,3020],{"class":1260}," cls",[1216,3022,772],{"class":1230},[1216,3024,3026],{"class":3025},"swJcz","_initialized",[1216,3028,1270],{"class":1230},[1216,3030,3032,3035],{"class":1218,"line":3031},22,[1216,3033,3034],{"class":1256},"            return",[1216,3036,3037],{"class":1230}," True\n",[1216,3039,3041],{"class":1218,"line":3040},23,[1216,3042,1299],{"emptyLinePlaceholder":888},[1216,3044,3046,3049,3051,3054,3057,3060,3062,3065,3067,3069,3071,3073,3075,3078],{"class":1218,"line":3045},24,[1216,3047,3048],{"class":1260},"        cls",[1216,3050,772],{"class":1230},[1216,3052,3053],{"class":3025},"_base_url",[1216,3055,3056],{"class":1230}," =",[1216,3058,3059],{"class":1226}," getattr",[1216,3061,1231],{"class":1230},[1216,3063,3064],{"class":1226},"settings",[1216,3066,1238],{"class":1230},[1216,3068,2855],{"class":1230},[1216,3070,2492],{"class":1321},[1216,3072,1318],{"class":1230},[1216,3074,1238],{"class":1230},[1216,3076,3077],{"class":1230}," \"\"",[1216,3079,1293],{"class":1230},[1216,3081,3083,3085,3087,3090,3092,3094,3096,3098,3100,3102,3104,3106,3108,3110],{"class":1218,"line":3082},25,[1216,3084,3048],{"class":1260},[1216,3086,772],{"class":1230},[1216,3088,3089],{"class":3025},"_basic_auth",[1216,3091,3056],{"class":1230},[1216,3093,3059],{"class":1226},[1216,3095,1231],{"class":1230},[1216,3097,3064],{"class":1226},[1216,3099,1238],{"class":1230},[1216,3101,2855],{"class":1230},[1216,3103,2504],{"class":1321},[1216,3105,1318],{"class":1230},[1216,3107,1238],{"class":1230},[1216,3109,3077],{"class":1230},[1216,3111,1293],{"class":1230},[1216,3113,3115,3117,3119,3122,3124,3126,3128,3130,3132,3134,3136,3138,3140,3142],{"class":1218,"line":3114},26,[1216,3116,3048],{"class":1260},[1216,3118,772],{"class":1230},[1216,3120,3121],{"class":3025},"_username",[1216,3123,3056],{"class":1230},[1216,3125,3059],{"class":1226},[1216,3127,1231],{"class":1230},[1216,3129,3064],{"class":1226},[1216,3131,1238],{"class":1230},[1216,3133,2855],{"class":1230},[1216,3135,2514],{"class":1321},[1216,3137,1318],{"class":1230},[1216,3139,1238],{"class":1230},[1216,3141,3077],{"class":1230},[1216,3143,1293],{"class":1230},[1216,3145,3147,3149,3151,3154,3156,3158,3160,3162,3164,3166,3168,3170,3172,3174],{"class":1218,"line":3146},27,[1216,3148,3048],{"class":1260},[1216,3150,772],{"class":1230},[1216,3152,3153],{"class":3025},"_password",[1216,3155,3056],{"class":1230},[1216,3157,3059],{"class":1226},[1216,3159,1231],{"class":1230},[1216,3161,3064],{"class":1226},[1216,3163,1238],{"class":1230},[1216,3165,2855],{"class":1230},[1216,3167,2524],{"class":1321},[1216,3169,1318],{"class":1230},[1216,3171,1238],{"class":1230},[1216,3173,3077],{"class":1230},[1216,3175,1293],{"class":1230},[1216,3177,3179],{"class":1218,"line":3178},28,[1216,3180,1299],{"emptyLinePlaceholder":888},[1216,3182,3184,3186,3189,3192,3195,3197,3199,3201,3203,3205,3207,3209,3211,3213,3215,3217,3219,3221,3223,3225],{"class":1218,"line":3183},29,[1216,3185,1305],{"class":1256},[1216,3187,3188],{"class":1230}," not",[1216,3190,3191],{"class":1226}," all",[1216,3193,3194],{"class":1230},"([",[1216,3196,2991],{"class":1260},[1216,3198,772],{"class":1230},[1216,3200,3053],{"class":3025},[1216,3202,1238],{"class":1230},[1216,3204,3020],{"class":1260},[1216,3206,772],{"class":1230},[1216,3208,3089],{"class":3025},[1216,3210,1238],{"class":1230},[1216,3212,3020],{"class":1260},[1216,3214,772],{"class":1230},[1216,3216,3121],{"class":3025},[1216,3218,1238],{"class":1230},[1216,3220,3020],{"class":1260},[1216,3222,772],{"class":1230},[1216,3224,3153],{"class":3025},[1216,3226,3227],{"class":1230},"]):\n",[1216,3229,3231,3234,3236,3238,3240,3242,3245,3247],{"class":1218,"line":3230},30,[1216,3232,3233],{"class":1260},"            logger",[1216,3235,772],{"class":1230},[1216,3237,844],{"class":1226},[1216,3239,1231],{"class":1230},[1216,3241,1318],{"class":1230},[1216,3243,3244],{"class":1321},"SignNow API not fully configured - missing required settings",[1216,3246,1318],{"class":1230},[1216,3248,1293],{"class":1230},[1216,3250,3252,3254],{"class":1218,"line":3251},31,[1216,3253,3034],{"class":1256},[1216,3255,2922],{"class":1230},[1216,3257,3259],{"class":1218,"line":3258},32,[1216,3260,1299],{"emptyLinePlaceholder":888},[1216,3262,3264,3266,3268,3270,3272],{"class":1218,"line":3263},33,[1216,3265,3048],{"class":1260},[1216,3267,772],{"class":1230},[1216,3269,3026],{"class":3025},[1216,3271,3056],{"class":1230},[1216,3273,3037],{"class":1230},[1216,3275,3277,3280],{"class":1218,"line":3276},34,[1216,3278,3279],{"class":1256},"        return",[1216,3281,3037],{"class":1230},[1216,3283,3285],{"class":1218,"line":3284},35,[1216,3286,1299],{"emptyLinePlaceholder":888},[1216,3288,3290,3292],{"class":1218,"line":3289},36,[1216,3291,2974],{"class":1230},[1216,3293,2977],{"class":1873},[1216,3295,3297,3299,3302,3304,3306,3308,3310,3313,3316],{"class":1218,"line":3296},37,[1216,3298,2983],{"class":1222},[1216,3300,3301],{"class":1226}," _get_access_token",[1216,3303,1231],{"class":1230},[1216,3305,2991],{"class":1234},[1216,3307,1924],{"class":1230},[1216,3309,2996],{"class":1230},[1216,3311,3312],{"class":1873}," str",[1216,3314,3315],{"class":1230}," |",[1216,3317,3318],{"class":1230}," None:\n",[1216,3320,3322,3324,3327],{"class":1218,"line":3321},38,[1216,3323,3007],{"class":1256},[1216,3325,3326],{"class":1903},"Get a valid access token, using cache or requesting a new one.",[1216,3328,2906],{"class":1256},[1216,3330,3332,3335,3337,3340,3342,3344,3346,3349],{"class":1218,"line":3331},39,[1216,3333,3334],{"class":1260},"        cached ",[1216,3336,1244],{"class":1230},[1216,3338,3339],{"class":1260}," cache",[1216,3341,772],{"class":1230},[1216,3343,1313],{"class":1226},[1216,3345,1231],{"class":1230},[1216,3347,3348],{"class":1226},"SIGNNOW_TOKEN_CACHE_KEY",[1216,3350,1293],{"class":1230},[1216,3352,3354,3356,3359],{"class":1218,"line":3353},40,[1216,3355,1305],{"class":1256},[1216,3357,3358],{"class":1260}," cached",[1216,3360,1270],{"class":1230},[1216,3362,3364,3366],{"class":1218,"line":3363},41,[1216,3365,3034],{"class":1256},[1216,3367,3368],{"class":1260}," cached\n",[1216,3370,3372],{"class":1218,"line":3371},42,[1216,3373,1299],{"emptyLinePlaceholder":888},[1216,3375,3377,3380,3383,3385,3388,3390,3393,3395,3398,3400,3403,3406],{"class":1218,"line":3376},43,[1216,3378,3379],{"class":1256},"        with",[1216,3381,3382],{"class":1260}," httpx",[1216,3384,772],{"class":1230},[1216,3386,3387],{"class":1226},"Client",[1216,3389,1231],{"class":1230},[1216,3391,3392],{"class":1234},"timeout",[1216,3394,1244],{"class":1230},[1216,3396,3397],{"class":1247},"30.0",[1216,3399,1924],{"class":1230},[1216,3401,3402],{"class":1256}," as",[1216,3404,3405],{"class":1260}," client",[1216,3407,1270],{"class":1230},[1216,3409,3411,3414,3416,3418,3420,3423],{"class":1218,"line":3410},44,[1216,3412,3413],{"class":1260},"            resp ",[1216,3415,1244],{"class":1230},[1216,3417,3405],{"class":1260},[1216,3419,772],{"class":1230},[1216,3421,3422],{"class":1226},"post",[1216,3424,3425],{"class":1230},"(\n",[1216,3427,3429,3432,3434,3437,3439,3441,3443,3446,3449],{"class":1218,"line":3428},45,[1216,3430,3431],{"class":1222},"                f",[1216,3433,1318],{"class":1321},[1216,3435,3436],{"class":1247},"{",[1216,3438,2991],{"class":1260},[1216,3440,772],{"class":1230},[1216,3442,3053],{"class":3025},[1216,3444,3445],{"class":1247},"}",[1216,3447,3448],{"class":1321},"/oauth2/token\"",[1216,3450,3451],{"class":1230},",\n",[1216,3453,3455,3458],{"class":1218,"line":3454},46,[1216,3456,3457],{"class":1234},"                data",[1216,3459,3460],{"class":1230},"={\n",[1216,3462,3464,3467,3470,3472,3474,3476,3479,3481],{"class":1218,"line":3463},47,[1216,3465,3466],{"class":1230},"                    \"",[1216,3468,3469],{"class":1321},"grant_type",[1216,3471,1318],{"class":1230},[1216,3473,1927],{"class":1230},[1216,3475,2855],{"class":1230},[1216,3477,3478],{"class":1321},"password",[1216,3480,1318],{"class":1230},[1216,3482,3451],{"class":1230},[1216,3484,3486,3488,3491,3493,3495,3497,3499,3501],{"class":1218,"line":3485},48,[1216,3487,3466],{"class":1230},[1216,3489,3490],{"class":1321},"username",[1216,3492,1318],{"class":1230},[1216,3494,1927],{"class":1230},[1216,3496,3020],{"class":1260},[1216,3498,772],{"class":1230},[1216,3500,3121],{"class":3025},[1216,3502,3451],{"class":1230},[1216,3504,3506,3508,3510,3512,3514,3516,3518,3520],{"class":1218,"line":3505},49,[1216,3507,3466],{"class":1230},[1216,3509,3478],{"class":1321},[1216,3511,1318],{"class":1230},[1216,3513,1927],{"class":1230},[1216,3515,3020],{"class":1260},[1216,3517,772],{"class":1230},[1216,3519,3153],{"class":3025},[1216,3521,3451],{"class":1230},[1216,3523,3525,3527,3529,3531,3533,3535,3538,3540],{"class":1218,"line":3524},50,[1216,3526,3466],{"class":1230},[1216,3528,2099],{"class":1321},[1216,3530,1318],{"class":1230},[1216,3532,1927],{"class":1230},[1216,3534,2855],{"class":1230},[1216,3536,3537],{"class":1321},"*",[1216,3539,1318],{"class":1230},[1216,3541,3451],{"class":1230},[1216,3543,3545],{"class":1218,"line":3544},51,[1216,3546,3547],{"class":1230},"                },\n",[1216,3549,3551,3554],{"class":1218,"line":3550},52,[1216,3552,3553],{"class":1234},"                headers",[1216,3555,3460],{"class":1230},[1216,3557,3559,3561,3564,3566,3568,3571,3574,3576,3578,3580,3582,3584,3586],{"class":1218,"line":3558},53,[1216,3560,3466],{"class":1230},[1216,3562,3563],{"class":1321},"Authorization",[1216,3565,1318],{"class":1230},[1216,3567,1927],{"class":1230},[1216,3569,3570],{"class":1222}," f",[1216,3572,3573],{"class":1321},"\"Basic ",[1216,3575,3436],{"class":1247},[1216,3577,2991],{"class":1260},[1216,3579,772],{"class":1230},[1216,3581,3089],{"class":3025},[1216,3583,3445],{"class":1247},[1216,3585,1318],{"class":1321},[1216,3587,3451],{"class":1230},[1216,3589,3591,3593,3596,3598,3600,3602,3605,3607],{"class":1218,"line":3590},54,[1216,3592,3466],{"class":1230},[1216,3594,3595],{"class":1321},"Content-Type",[1216,3597,1318],{"class":1230},[1216,3599,1927],{"class":1230},[1216,3601,2855],{"class":1230},[1216,3603,3604],{"class":1321},"application/x-www-form-urlencoded",[1216,3606,1318],{"class":1230},[1216,3608,3451],{"class":1230},[1216,3610,3612],{"class":1218,"line":3611},55,[1216,3613,3547],{"class":1230},[1216,3615,3617],{"class":1218,"line":3616},56,[1216,3618,3619],{"class":1230},"            )\n",[1216,3621,3623,3626,3628,3631],{"class":1218,"line":3622},57,[1216,3624,3625],{"class":1260},"            resp",[1216,3627,772],{"class":1230},[1216,3629,3630],{"class":1226},"raise_for_status",[1216,3632,3633],{"class":1230},"()\n",[1216,3635,3637,3640,3642,3645,3647,3650],{"class":1218,"line":3636},58,[1216,3638,3639],{"class":1260},"            data ",[1216,3641,1244],{"class":1230},[1216,3643,3644],{"class":1260}," resp",[1216,3646,772],{"class":1230},[1216,3648,3649],{"class":1226},"json",[1216,3651,3633],{"class":1230},[1216,3653,3655],{"class":1218,"line":3654},59,[1216,3656,1299],{"emptyLinePlaceholder":888},[1216,3658,3660,3663,3665,3668,3670,3672,3675,3677],{"class":1218,"line":3659},60,[1216,3661,3662],{"class":1260},"            token ",[1216,3664,1244],{"class":1230},[1216,3666,3667],{"class":1260}," data",[1216,3669,1352],{"class":1230},[1216,3671,1318],{"class":1230},[1216,3673,3674],{"class":1321},"access_token",[1216,3676,1318],{"class":1230},[1216,3678,3679],{"class":1230},"]\n",[1216,3681,3683,3686,3688,3690,3692,3694,3696,3698,3701,3703,3705,3708],{"class":1218,"line":3682},61,[1216,3684,3685],{"class":1260},"            expires_in ",[1216,3687,1244],{"class":1230},[1216,3689,3667],{"class":1260},[1216,3691,772],{"class":1230},[1216,3693,1313],{"class":1226},[1216,3695,1231],{"class":1230},[1216,3697,1318],{"class":1230},[1216,3699,3700],{"class":1321},"expires_in",[1216,3702,1318],{"class":1230},[1216,3704,1238],{"class":1230},[1216,3706,3707],{"class":1247}," 3600",[1216,3709,1293],{"class":1230},[1216,3711,3713,3716,3718,3721,3723,3726,3729,3732,3734,3737],{"class":1218,"line":3712},62,[1216,3714,3715],{"class":1260},"            ttl ",[1216,3717,1244],{"class":1230},[1216,3719,3720],{"class":1226}," max",[1216,3722,1231],{"class":1230},[1216,3724,3725],{"class":1226},"expires_in ",[1216,3727,3728],{"class":1230},"-",[1216,3730,3731],{"class":1226}," SIGNNOW_TOKEN_TTL_BUFFER",[1216,3733,1238],{"class":1230},[1216,3735,3736],{"class":1247}," 60",[1216,3738,1293],{"class":1230},[1216,3740,3742,3745,3747,3750,3752,3754,3756,3759,3761,3764,3766,3769],{"class":1218,"line":3741},63,[1216,3743,3744],{"class":1260},"            cache",[1216,3746,772],{"class":1230},[1216,3748,3749],{"class":1226},"set",[1216,3751,1231],{"class":1230},[1216,3753,3348],{"class":1226},[1216,3755,1238],{"class":1230},[1216,3757,3758],{"class":1226}," token",[1216,3760,1238],{"class":1230},[1216,3762,3763],{"class":1234}," timeout",[1216,3765,1244],{"class":1230},[1216,3767,3768],{"class":1226},"ttl",[1216,3770,1293],{"class":1230},[1216,3772,3774],{"class":1218,"line":3773},64,[1216,3775,1299],{"emptyLinePlaceholder":888},[1216,3777,3779,3781],{"class":1218,"line":3778},65,[1216,3780,3034],{"class":1256},[1216,3782,3783],{"class":1260}," token\n",[1720,3785,3786],{},[746,3787,3788,3789,3794,3795,3797,3798,3800,3801,3804],{},"We use ",[1544,3790,3793],{"href":3791,"rel":3792},"https://www.python-httpx.org/",[1548],"httpx"," instead of ",[1047,3796,1134],{}," for its modern API, built-in timeout support, and async capabilities. If you later need to make concurrent API calls, ",[1047,3799,3793],{}," supports ",[1047,3802,3803],{},"AsyncClient"," out of the box.",[1070,3806,3808],{"id":3807},"document-operations","Document operations",[746,3810,3811],{},"With authentication handled, the core API methods are straightforward:",[1208,3813,3815],{"className":1210,"code":3814,"filename":2792,"language":1212,"meta":873,"style":873},"@classmethod\ndef upload_document(cls, pdf_bytes: bytes, filename: str) -> dict | None:\n    \"\"\"Upload a PDF to SignNow. Returns {\"id\": \"document_id\"} on success.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    with httpx.Client(timeout=60.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/document\",\n            headers=cls._auth_headers(token),\n            files={\"file\": (filename, pdf_bytes, \"application/pdf\")},\n        )\n        resp.raise_for_status()\n        return {\"id\": resp.json().get(\"id\", \"\")}\n\n@classmethod\ndef add_signature_fields(cls, document_id: str, fields: list[dict]) -> bool:\n    \"\"\"Add signature/text fields to an uploaded document.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return False\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.put(\n            f\"{cls._base_url}/document/{document_id}\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json={\"fields\": fields},\n        )\n        resp.raise_for_status()\n        return True\n\n@classmethod\ndef download_signed_document(cls, document_id: str) -> bytes | None:\n    \"\"\"Download the signed (collapsed) PDF.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    with httpx.Client(timeout=60.0) as client:\n        resp = client.get(\n            f\"{cls._base_url}/document/{document_id}/download\",\n            headers=cls._auth_headers(token),\n            params={\"type\": \"collapsed\"},\n        )\n        resp.raise_for_status()\n        return resp.content\n",[1047,3816,3817,3824,3865,3874,3890,3901,3908,3912,3940,3955,3977,3999,4038,4043,4054,4095,4099,4105,4149,4158,4172,4182,4188,4192,4218,4233,4263,4301,4321,4325,4335,4341,4345,4351,4380,4389,4403,4413,4419,4423,4449,4463,4492,4510,4535,4539,4549],{"__ignoreMap":873},[1216,3818,3819,3822],{"class":1218,"line":1219},[1216,3820,3821],{"class":1230},"@",[1216,3823,2977],{"class":1873},[1216,3825,3826,3828,3831,3833,3835,3837,3840,3842,3845,3847,3850,3852,3854,3856,3858,3861,3863],{"class":1218,"line":874},[1216,3827,1223],{"class":1222},[1216,3829,3830],{"class":1226}," upload_document",[1216,3832,1231],{"class":1230},[1216,3834,2991],{"class":1234},[1216,3836,1238],{"class":1230},[1216,3838,3839],{"class":1234}," pdf_bytes",[1216,3841,1927],{"class":1230},[1216,3843,3844],{"class":1873}," bytes",[1216,3846,1238],{"class":1230},[1216,3848,3849],{"class":1234}," filename",[1216,3851,1927],{"class":1230},[1216,3853,3312],{"class":1873},[1216,3855,1924],{"class":1230},[1216,3857,2996],{"class":1230},[1216,3859,3860],{"class":1873}," dict",[1216,3862,3315],{"class":1230},[1216,3864,3318],{"class":1230},[1216,3866,3867,3869,3872],{"class":1218,"line":1273},[1216,3868,2900],{"class":1256},[1216,3870,3871],{"class":1903},"Upload a PDF to SignNow. Returns {\"id\": \"document_id\"} on success.",[1216,3873,2906],{"class":1256},[1216,3875,3876,3879,3881,3883,3885,3888],{"class":1218,"line":1296},[1216,3877,3878],{"class":1260},"    token ",[1216,3880,1244],{"class":1230},[1216,3882,3020],{"class":1260},[1216,3884,772],{"class":1230},[1216,3886,3887],{"class":1226},"_get_access_token",[1216,3889,3633],{"class":1230},[1216,3891,3892,3895,3897,3899],{"class":1218,"line":1302},[1216,3893,3894],{"class":1256},"    if",[1216,3896,3188],{"class":1230},[1216,3898,3758],{"class":1260},[1216,3900,1270],{"class":1230},[1216,3902,3903,3905],{"class":1218,"line":1332},[1216,3904,3279],{"class":1256},[1216,3906,3907],{"class":1230}," None\n",[1216,3909,3910],{"class":1218,"line":1365},[1216,3911,1299],{"emptyLinePlaceholder":888},[1216,3913,3914,3917,3919,3921,3923,3925,3927,3929,3932,3934,3936,3938],{"class":1218,"line":2881},[1216,3915,3916],{"class":1256},"    with",[1216,3918,3382],{"class":1260},[1216,3920,772],{"class":1230},[1216,3922,3387],{"class":1226},[1216,3924,1231],{"class":1230},[1216,3926,3392],{"class":1234},[1216,3928,1244],{"class":1230},[1216,3930,3931],{"class":1247},"60.0",[1216,3933,1924],{"class":1230},[1216,3935,3402],{"class":1256},[1216,3937,3405],{"class":1260},[1216,3939,1270],{"class":1230},[1216,3941,3942,3945,3947,3949,3951,3953],{"class":1218,"line":2886},[1216,3943,3944],{"class":1260},"        resp ",[1216,3946,1244],{"class":1230},[1216,3948,3405],{"class":1260},[1216,3950,772],{"class":1230},[1216,3952,3422],{"class":1226},[1216,3954,3425],{"class":1230},[1216,3956,3957,3960,3962,3964,3966,3968,3970,3972,3975],{"class":1218,"line":2897},[1216,3958,3959],{"class":1222},"            f",[1216,3961,1318],{"class":1321},[1216,3963,3436],{"class":1247},[1216,3965,2991],{"class":1260},[1216,3967,772],{"class":1230},[1216,3969,3053],{"class":3025},[1216,3971,3445],{"class":1247},[1216,3973,3974],{"class":1321},"/document\"",[1216,3976,3451],{"class":1230},[1216,3978,3979,3982,3984,3986,3988,3991,3993,3996],{"class":1218,"line":2909},[1216,3980,3981],{"class":1234},"            headers",[1216,3983,1244],{"class":1230},[1216,3985,2991],{"class":1260},[1216,3987,772],{"class":1230},[1216,3989,3990],{"class":1226},"_auth_headers",[1216,3992,1231],{"class":1230},[1216,3994,3995],{"class":1226},"token",[1216,3997,3998],{"class":1230},"),\n",[1216,4000,4001,4004,4007,4009,4012,4014,4016,4019,4022,4024,4026,4028,4030,4033,4035],{"class":1218,"line":2914},[1216,4002,4003],{"class":1234},"            files",[1216,4005,4006],{"class":1230},"={",[1216,4008,1318],{"class":1230},[1216,4010,4011],{"class":1321},"file",[1216,4013,1318],{"class":1230},[1216,4015,1927],{"class":1230},[1216,4017,4018],{"class":1230}," (",[1216,4020,4021],{"class":1226},"filename",[1216,4023,1238],{"class":1230},[1216,4025,3839],{"class":1226},[1216,4027,1238],{"class":1230},[1216,4029,2855],{"class":1230},[1216,4031,4032],{"class":1321},"application/pdf",[1216,4034,1318],{"class":1230},[1216,4036,4037],{"class":1230},")},\n",[1216,4039,4040],{"class":1218,"line":2925},[1216,4041,4042],{"class":1230},"        )\n",[1216,4044,4045,4048,4050,4052],{"class":1218,"line":2936},[1216,4046,4047],{"class":1260},"        resp",[1216,4049,772],{"class":1230},[1216,4051,3630],{"class":1226},[1216,4053,3633],{"class":1230},[1216,4055,4056,4058,4061,4063,4065,4067,4069,4071,4073,4075,4078,4080,4082,4084,4086,4088,4090,4092],{"class":1218,"line":2946},[1216,4057,3279],{"class":1256},[1216,4059,4060],{"class":1230}," {",[1216,4062,1318],{"class":1230},[1216,4064,1357],{"class":1321},[1216,4066,1318],{"class":1230},[1216,4068,1927],{"class":1230},[1216,4070,3644],{"class":1260},[1216,4072,772],{"class":1230},[1216,4074,3649],{"class":1226},[1216,4076,4077],{"class":1230},"().",[1216,4079,1313],{"class":1226},[1216,4081,1231],{"class":1230},[1216,4083,1318],{"class":1230},[1216,4085,1357],{"class":1321},[1216,4087,1318],{"class":1230},[1216,4089,1238],{"class":1230},[1216,4091,3077],{"class":1230},[1216,4093,4094],{"class":1230},")}\n",[1216,4096,4097],{"class":1218,"line":2956},[1216,4098,1299],{"emptyLinePlaceholder":888},[1216,4100,4101,4103],{"class":1218,"line":2966},[1216,4102,3821],{"class":1230},[1216,4104,2977],{"class":1873},[1216,4106,4107,4109,4112,4114,4116,4118,4121,4123,4125,4127,4130,4132,4135,4137,4140,4143,4145,4147],{"class":1218,"line":2971},[1216,4108,1223],{"class":1222},[1216,4110,4111],{"class":1226}," add_signature_fields",[1216,4113,1231],{"class":1230},[1216,4115,2991],{"class":1234},[1216,4117,1238],{"class":1230},[1216,4119,4120],{"class":1234}," document_id",[1216,4122,1927],{"class":1230},[1216,4124,3312],{"class":1873},[1216,4126,1238],{"class":1230},[1216,4128,4129],{"class":1234}," fields",[1216,4131,1927],{"class":1230},[1216,4133,4134],{"class":1260}," list",[1216,4136,1352],{"class":1230},[1216,4138,4139],{"class":1873},"dict",[1216,4141,4142],{"class":1230},"])",[1216,4144,2996],{"class":1230},[1216,4146,2999],{"class":1873},[1216,4148,1270],{"class":1230},[1216,4150,4151,4153,4156],{"class":1218,"line":2980},[1216,4152,2900],{"class":1256},[1216,4154,4155],{"class":1903},"Add signature/text fields to an uploaded document.",[1216,4157,2906],{"class":1256},[1216,4159,4160,4162,4164,4166,4168,4170],{"class":1218,"line":3004},[1216,4161,3878],{"class":1260},[1216,4163,1244],{"class":1230},[1216,4165,3020],{"class":1260},[1216,4167,772],{"class":1230},[1216,4169,3887],{"class":1226},[1216,4171,3633],{"class":1230},[1216,4173,4174,4176,4178,4180],{"class":1218,"line":3015},[1216,4175,3894],{"class":1256},[1216,4177,3188],{"class":1230},[1216,4179,3758],{"class":1260},[1216,4181,1270],{"class":1230},[1216,4183,4184,4186],{"class":1218,"line":3031},[1216,4185,3279],{"class":1256},[1216,4187,2922],{"class":1230},[1216,4189,4190],{"class":1218,"line":3040},[1216,4191,1299],{"emptyLinePlaceholder":888},[1216,4193,4194,4196,4198,4200,4202,4204,4206,4208,4210,4212,4214,4216],{"class":1218,"line":3045},[1216,4195,3916],{"class":1256},[1216,4197,3382],{"class":1260},[1216,4199,772],{"class":1230},[1216,4201,3387],{"class":1226},[1216,4203,1231],{"class":1230},[1216,4205,3392],{"class":1234},[1216,4207,1244],{"class":1230},[1216,4209,3397],{"class":1247},[1216,4211,1924],{"class":1230},[1216,4213,3402],{"class":1256},[1216,4215,3405],{"class":1260},[1216,4217,1270],{"class":1230},[1216,4219,4220,4222,4224,4226,4228,4231],{"class":1218,"line":3082},[1216,4221,3944],{"class":1260},[1216,4223,1244],{"class":1230},[1216,4225,3405],{"class":1260},[1216,4227,772],{"class":1230},[1216,4229,4230],{"class":1226},"put",[1216,4232,3425],{"class":1230},[1216,4234,4235,4237,4239,4241,4243,4245,4247,4249,4252,4254,4257,4259,4261],{"class":1218,"line":3114},[1216,4236,3959],{"class":1222},[1216,4238,1318],{"class":1321},[1216,4240,3436],{"class":1247},[1216,4242,2991],{"class":1260},[1216,4244,772],{"class":1230},[1216,4246,3053],{"class":3025},[1216,4248,3445],{"class":1247},[1216,4250,4251],{"class":1321},"/document/",[1216,4253,3436],{"class":1247},[1216,4255,4256],{"class":1226},"document_id",[1216,4258,3445],{"class":1247},[1216,4260,1318],{"class":1321},[1216,4262,3451],{"class":1230},[1216,4264,4265,4267,4270,4272,4274,4276,4278,4280,4283,4285,4287,4289,4291,4293,4296,4298],{"class":1218,"line":3146},[1216,4266,3981],{"class":1234},[1216,4268,4269],{"class":1230},"={**",[1216,4271,2991],{"class":1260},[1216,4273,772],{"class":1230},[1216,4275,3990],{"class":1226},[1216,4277,1231],{"class":1230},[1216,4279,3995],{"class":1226},[1216,4281,4282],{"class":1230},"),",[1216,4284,2855],{"class":1230},[1216,4286,3595],{"class":1321},[1216,4288,1318],{"class":1230},[1216,4290,1927],{"class":1230},[1216,4292,2855],{"class":1230},[1216,4294,4295],{"class":1321},"application/json",[1216,4297,1318],{"class":1230},[1216,4299,4300],{"class":1230},"},\n",[1216,4302,4303,4306,4308,4310,4313,4315,4317,4319],{"class":1218,"line":3178},[1216,4304,4305],{"class":1234},"            json",[1216,4307,4006],{"class":1230},[1216,4309,1318],{"class":1230},[1216,4311,4312],{"class":1321},"fields",[1216,4314,1318],{"class":1230},[1216,4316,1927],{"class":1230},[1216,4318,4129],{"class":1226},[1216,4320,4300],{"class":1230},[1216,4322,4323],{"class":1218,"line":3183},[1216,4324,4042],{"class":1230},[1216,4326,4327,4329,4331,4333],{"class":1218,"line":3230},[1216,4328,4047],{"class":1260},[1216,4330,772],{"class":1230},[1216,4332,3630],{"class":1226},[1216,4334,3633],{"class":1230},[1216,4336,4337,4339],{"class":1218,"line":3251},[1216,4338,3279],{"class":1256},[1216,4340,3037],{"class":1230},[1216,4342,4343],{"class":1218,"line":3258},[1216,4344,1299],{"emptyLinePlaceholder":888},[1216,4346,4347,4349],{"class":1218,"line":3263},[1216,4348,3821],{"class":1230},[1216,4350,2977],{"class":1873},[1216,4352,4353,4355,4358,4360,4362,4364,4366,4368,4370,4372,4374,4376,4378],{"class":1218,"line":3276},[1216,4354,1223],{"class":1222},[1216,4356,4357],{"class":1226}," download_signed_document",[1216,4359,1231],{"class":1230},[1216,4361,2991],{"class":1234},[1216,4363,1238],{"class":1230},[1216,4365,4120],{"class":1234},[1216,4367,1927],{"class":1230},[1216,4369,3312],{"class":1873},[1216,4371,1924],{"class":1230},[1216,4373,2996],{"class":1230},[1216,4375,3844],{"class":1873},[1216,4377,3315],{"class":1230},[1216,4379,3318],{"class":1230},[1216,4381,4382,4384,4387],{"class":1218,"line":3284},[1216,4383,2900],{"class":1256},[1216,4385,4386],{"class":1903},"Download the signed (collapsed) PDF.",[1216,4388,2906],{"class":1256},[1216,4390,4391,4393,4395,4397,4399,4401],{"class":1218,"line":3289},[1216,4392,3878],{"class":1260},[1216,4394,1244],{"class":1230},[1216,4396,3020],{"class":1260},[1216,4398,772],{"class":1230},[1216,4400,3887],{"class":1226},[1216,4402,3633],{"class":1230},[1216,4404,4405,4407,4409,4411],{"class":1218,"line":3296},[1216,4406,3894],{"class":1256},[1216,4408,3188],{"class":1230},[1216,4410,3758],{"class":1260},[1216,4412,1270],{"class":1230},[1216,4414,4415,4417],{"class":1218,"line":3321},[1216,4416,3279],{"class":1256},[1216,4418,3907],{"class":1230},[1216,4420,4421],{"class":1218,"line":3331},[1216,4422,1299],{"emptyLinePlaceholder":888},[1216,4424,4425,4427,4429,4431,4433,4435,4437,4439,4441,4443,4445,4447],{"class":1218,"line":3353},[1216,4426,3916],{"class":1256},[1216,4428,3382],{"class":1260},[1216,4430,772],{"class":1230},[1216,4432,3387],{"class":1226},[1216,4434,1231],{"class":1230},[1216,4436,3392],{"class":1234},[1216,4438,1244],{"class":1230},[1216,4440,3931],{"class":1247},[1216,4442,1924],{"class":1230},[1216,4444,3402],{"class":1256},[1216,4446,3405],{"class":1260},[1216,4448,1270],{"class":1230},[1216,4450,4451,4453,4455,4457,4459,4461],{"class":1218,"line":3363},[1216,4452,3944],{"class":1260},[1216,4454,1244],{"class":1230},[1216,4456,3405],{"class":1260},[1216,4458,772],{"class":1230},[1216,4460,1313],{"class":1226},[1216,4462,3425],{"class":1230},[1216,4464,4465,4467,4469,4471,4473,4475,4477,4479,4481,4483,4485,4487,4490],{"class":1218,"line":3371},[1216,4466,3959],{"class":1222},[1216,4468,1318],{"class":1321},[1216,4470,3436],{"class":1247},[1216,4472,2991],{"class":1260},[1216,4474,772],{"class":1230},[1216,4476,3053],{"class":3025},[1216,4478,3445],{"class":1247},[1216,4480,4251],{"class":1321},[1216,4482,3436],{"class":1247},[1216,4484,4256],{"class":1226},[1216,4486,3445],{"class":1247},[1216,4488,4489],{"class":1321},"/download\"",[1216,4491,3451],{"class":1230},[1216,4493,4494,4496,4498,4500,4502,4504,4506,4508],{"class":1218,"line":3376},[1216,4495,3981],{"class":1234},[1216,4497,1244],{"class":1230},[1216,4499,2991],{"class":1260},[1216,4501,772],{"class":1230},[1216,4503,3990],{"class":1226},[1216,4505,1231],{"class":1230},[1216,4507,3995],{"class":1226},[1216,4509,3998],{"class":1230},[1216,4511,4512,4515,4517,4519,4522,4524,4526,4528,4531,4533],{"class":1218,"line":3410},[1216,4513,4514],{"class":1234},"            params",[1216,4516,4006],{"class":1230},[1216,4518,1318],{"class":1230},[1216,4520,4521],{"class":1321},"type",[1216,4523,1318],{"class":1230},[1216,4525,1927],{"class":1230},[1216,4527,2855],{"class":1230},[1216,4529,4530],{"class":1321},"collapsed",[1216,4532,1318],{"class":1230},[1216,4534,4300],{"class":1230},[1216,4536,4537],{"class":1218,"line":3428},[1216,4538,4042],{"class":1230},[1216,4540,4541,4543,4545,4547],{"class":1218,"line":3454},[1216,4542,4047],{"class":1260},[1216,4544,772],{"class":1230},[1216,4546,3630],{"class":1226},[1216,4548,3633],{"class":1230},[1216,4550,4551,4553,4555,4557],{"class":1218,"line":3463},[1216,4552,3279],{"class":1256},[1216,4554,3644],{"class":1260},[1216,4556,772],{"class":1230},[1216,4558,4559],{"class":3025},"content\n",[1070,4561,4563],{"id":4562},"sending-invites","Sending invites",[746,4565,4566],{},"SignNow supports two invite types:",[979,4568,4569,4575],{},[982,4570,4571,4574],{},[758,4572,4573],{},"Freeform invites"," - The signer places their signature wherever they want",[982,4576,4577,4580],{},[758,4578,4579],{},"Role-based invites"," - Signature fields are pre-positioned, and signers fill specific roles",[746,4582,4583],{},"We used role-based invites because we wanted to control exactly where the signature appears on each document:",[1208,4585,4587],{"className":1210,"code":4586,"filename":2792,"language":1212,"meta":873,"style":873},"@classmethod\ndef send_role_based_invite(\n    cls,\n    document_id: str,\n    signers: list[dict],\n    from_email: str,\n    subject: str = \"\",\n    message: str = \"\",\n) -> dict | None:\n    \"\"\"\n    Send a role-based invite with specific field assignments.\n    Each signer dict: {\"email\": ..., \"role\": ..., \"role_id\": ..., \"order\": ...}\n    \"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    payload = {\"to\": signers, \"from\": from_email}\n    if subject:\n        payload[\"subject\"] = subject\n    if message:\n        payload[\"message\"] = message\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/document/{document_id}/invite\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json=payload,\n        )\n        resp.raise_for_status()\n        return resp.json()\n",[1047,4588,4589,4595,4604,4611,4622,4638,4649,4664,4679,4691,4696,4701,4706,4710,4724,4734,4740,4744,4781,4790,4812,4821,4841,4845,4871,4885,4914,4948,4959,4963,4973],{"__ignoreMap":873},[1216,4590,4591,4593],{"class":1218,"line":1219},[1216,4592,3821],{"class":1230},[1216,4594,2977],{"class":1873},[1216,4596,4597,4599,4602],{"class":1218,"line":874},[1216,4598,1223],{"class":1222},[1216,4600,4601],{"class":1226}," send_role_based_invite",[1216,4603,3425],{"class":1230},[1216,4605,4606,4609],{"class":1218,"line":1273},[1216,4607,4608],{"class":1234},"    cls",[1216,4610,3451],{"class":1230},[1216,4612,4613,4616,4618,4620],{"class":1218,"line":1296},[1216,4614,4615],{"class":1234},"    document_id",[1216,4617,1927],{"class":1230},[1216,4619,3312],{"class":1873},[1216,4621,3451],{"class":1230},[1216,4623,4624,4627,4629,4631,4633,4635],{"class":1218,"line":1302},[1216,4625,4626],{"class":1234},"    signers",[1216,4628,1927],{"class":1230},[1216,4630,4134],{"class":1260},[1216,4632,1352],{"class":1230},[1216,4634,4139],{"class":1873},[1216,4636,4637],{"class":1230},"],\n",[1216,4639,4640,4643,4645,4647],{"class":1218,"line":1332},[1216,4641,4642],{"class":1234},"    from_email",[1216,4644,1927],{"class":1230},[1216,4646,3312],{"class":1873},[1216,4648,3451],{"class":1230},[1216,4650,4651,4654,4656,4658,4660,4662],{"class":1218,"line":1365},[1216,4652,4653],{"class":1234},"    subject",[1216,4655,1927],{"class":1230},[1216,4657,3312],{"class":1873},[1216,4659,3056],{"class":1230},[1216,4661,3077],{"class":1230},[1216,4663,3451],{"class":1230},[1216,4665,4666,4669,4671,4673,4675,4677],{"class":1218,"line":2881},[1216,4667,4668],{"class":1234},"    message",[1216,4670,1927],{"class":1230},[1216,4672,3312],{"class":1873},[1216,4674,3056],{"class":1230},[1216,4676,3077],{"class":1230},[1216,4678,3451],{"class":1230},[1216,4680,4681,4683,4685,4687,4689],{"class":1218,"line":2886},[1216,4682,1924],{"class":1230},[1216,4684,2996],{"class":1230},[1216,4686,3860],{"class":1873},[1216,4688,3315],{"class":1230},[1216,4690,3318],{"class":1230},[1216,4692,4693],{"class":1218,"line":2897},[1216,4694,4695],{"class":1256},"    \"\"\"\n",[1216,4697,4698],{"class":1218,"line":2909},[1216,4699,4700],{"class":1903},"    Send a role-based invite with specific field assignments.\n",[1216,4702,4703],{"class":1218,"line":2914},[1216,4704,4705],{"class":1903},"    Each signer dict: {\"email\": ..., \"role\": ..., \"role_id\": ..., \"order\": ...}\n",[1216,4707,4708],{"class":1218,"line":2925},[1216,4709,4695],{"class":1256},[1216,4711,4712,4714,4716,4718,4720,4722],{"class":1218,"line":2936},[1216,4713,3878],{"class":1260},[1216,4715,1244],{"class":1230},[1216,4717,3020],{"class":1260},[1216,4719,772],{"class":1230},[1216,4721,3887],{"class":1226},[1216,4723,3633],{"class":1230},[1216,4725,4726,4728,4730,4732],{"class":1218,"line":2946},[1216,4727,3894],{"class":1256},[1216,4729,3188],{"class":1230},[1216,4731,3758],{"class":1260},[1216,4733,1270],{"class":1230},[1216,4735,4736,4738],{"class":1218,"line":2956},[1216,4737,3279],{"class":1256},[1216,4739,3907],{"class":1230},[1216,4741,4742],{"class":1218,"line":2966},[1216,4743,1299],{"emptyLinePlaceholder":888},[1216,4745,4746,4749,4751,4753,4755,4758,4760,4762,4765,4767,4769,4771,4773,4775,4778],{"class":1218,"line":2971},[1216,4747,4748],{"class":1260},"    payload ",[1216,4750,1244],{"class":1230},[1216,4752,4060],{"class":1230},[1216,4754,1318],{"class":1230},[1216,4756,4757],{"class":1321},"to",[1216,4759,1318],{"class":1230},[1216,4761,1927],{"class":1230},[1216,4763,4764],{"class":1260}," signers",[1216,4766,1238],{"class":1230},[1216,4768,2855],{"class":1230},[1216,4770,2807],{"class":1321},[1216,4772,1318],{"class":1230},[1216,4774,1927],{"class":1230},[1216,4776,4777],{"class":1260}," from_email",[1216,4779,4780],{"class":1230},"}\n",[1216,4782,4783,4785,4788],{"class":1218,"line":2980},[1216,4784,3894],{"class":1256},[1216,4786,4787],{"class":1260}," subject",[1216,4789,1270],{"class":1230},[1216,4791,4792,4795,4797,4799,4802,4804,4807,4809],{"class":1218,"line":3004},[1216,4793,4794],{"class":1260},"        payload",[1216,4796,1352],{"class":1230},[1216,4798,1318],{"class":1230},[1216,4800,4801],{"class":1321},"subject",[1216,4803,1318],{"class":1230},[1216,4805,4806],{"class":1230},"]",[1216,4808,3056],{"class":1230},[1216,4810,4811],{"class":1260}," subject\n",[1216,4813,4814,4816,4819],{"class":1218,"line":3015},[1216,4815,3894],{"class":1256},[1216,4817,4818],{"class":1260}," message",[1216,4820,1270],{"class":1230},[1216,4822,4823,4825,4827,4829,4832,4834,4836,4838],{"class":1218,"line":3031},[1216,4824,4794],{"class":1260},[1216,4826,1352],{"class":1230},[1216,4828,1318],{"class":1230},[1216,4830,4831],{"class":1321},"message",[1216,4833,1318],{"class":1230},[1216,4835,4806],{"class":1230},[1216,4837,3056],{"class":1230},[1216,4839,4840],{"class":1260}," message\n",[1216,4842,4843],{"class":1218,"line":3040},[1216,4844,1299],{"emptyLinePlaceholder":888},[1216,4846,4847,4849,4851,4853,4855,4857,4859,4861,4863,4865,4867,4869],{"class":1218,"line":3045},[1216,4848,3916],{"class":1256},[1216,4850,3382],{"class":1260},[1216,4852,772],{"class":1230},[1216,4854,3387],{"class":1226},[1216,4856,1231],{"class":1230},[1216,4858,3392],{"class":1234},[1216,4860,1244],{"class":1230},[1216,4862,3397],{"class":1247},[1216,4864,1924],{"class":1230},[1216,4866,3402],{"class":1256},[1216,4868,3405],{"class":1260},[1216,4870,1270],{"class":1230},[1216,4872,4873,4875,4877,4879,4881,4883],{"class":1218,"line":3082},[1216,4874,3944],{"class":1260},[1216,4876,1244],{"class":1230},[1216,4878,3405],{"class":1260},[1216,4880,772],{"class":1230},[1216,4882,3422],{"class":1226},[1216,4884,3425],{"class":1230},[1216,4886,4887,4889,4891,4893,4895,4897,4899,4901,4903,4905,4907,4909,4912],{"class":1218,"line":3114},[1216,4888,3959],{"class":1222},[1216,4890,1318],{"class":1321},[1216,4892,3436],{"class":1247},[1216,4894,2991],{"class":1260},[1216,4896,772],{"class":1230},[1216,4898,3053],{"class":3025},[1216,4900,3445],{"class":1247},[1216,4902,4251],{"class":1321},[1216,4904,3436],{"class":1247},[1216,4906,4256],{"class":1226},[1216,4908,3445],{"class":1247},[1216,4910,4911],{"class":1321},"/invite\"",[1216,4913,3451],{"class":1230},[1216,4915,4916,4918,4920,4922,4924,4926,4928,4930,4932,4934,4936,4938,4940,4942,4944,4946],{"class":1218,"line":3146},[1216,4917,3981],{"class":1234},[1216,4919,4269],{"class":1230},[1216,4921,2991],{"class":1260},[1216,4923,772],{"class":1230},[1216,4925,3990],{"class":1226},[1216,4927,1231],{"class":1230},[1216,4929,3995],{"class":1226},[1216,4931,4282],{"class":1230},[1216,4933,2855],{"class":1230},[1216,4935,3595],{"class":1321},[1216,4937,1318],{"class":1230},[1216,4939,1927],{"class":1230},[1216,4941,2855],{"class":1230},[1216,4943,4295],{"class":1321},[1216,4945,1318],{"class":1230},[1216,4947,4300],{"class":1230},[1216,4949,4950,4952,4954,4957],{"class":1218,"line":3178},[1216,4951,4305],{"class":1234},[1216,4953,1244],{"class":1230},[1216,4955,4956],{"class":1226},"payload",[1216,4958,3451],{"class":1230},[1216,4960,4961],{"class":1218,"line":3183},[1216,4962,4042],{"class":1230},[1216,4964,4965,4967,4969,4971],{"class":1218,"line":3230},[1216,4966,4047],{"class":1260},[1216,4968,772],{"class":1230},[1216,4970,3630],{"class":1226},[1216,4972,3633],{"class":1230},[1216,4974,4975,4977,4979,4981,4983],{"class":1218,"line":3251},[1216,4976,3279],{"class":1256},[1216,4978,3644],{"class":1260},[1216,4980,772],{"class":1230},[1216,4982,3649],{"class":1226},[1216,4984,3633],{"class":1230},[1070,4986,4988],{"id":4987},"webhook-registration","Webhook registration",[746,4990,4991],{},"To get notified when a document is signed, we register a webhook for each document:",[1208,4993,4995],{"className":1210,"code":4994,"filename":2792,"language":1212,"meta":873,"style":873},"@classmethod\ndef register_webhook(cls, event: str, entity_id: str, callback_url: str) -> bool:\n    \"\"\"Register a webhook callback for a specific event on a document.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return False\n\n    payload = {\n        \"event\": event,\n        \"entity_id\": entity_id,\n        \"action\": \"callback\",\n        \"attributes\": {\n            \"callback\": callback_url,\n            \"use_tls_12\": True,\n        },\n    }\n\n    webhook_secret = getattr(settings, \"SIGNNOW_WEBHOOK_SECRET\", \"\")\n    if webhook_secret:\n        payload[\"attributes\"][\"secret_key\"] = webhook_secret\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/v2/events\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json=payload,\n        )\n        resp.raise_for_status()\n        return True\n",[1047,4996,4997,5003,5049,5058,5072,5082,5088,5092,5101,5117,5132,5152,5165,5180,5194,5199,5204,5208,5235,5244,5273,5277,5303,5317,5338,5372,5382,5386,5396],{"__ignoreMap":873},[1216,4998,4999,5001],{"class":1218,"line":1219},[1216,5000,3821],{"class":1230},[1216,5002,2977],{"class":1873},[1216,5004,5005,5007,5010,5012,5014,5016,5019,5021,5023,5025,5028,5030,5032,5034,5037,5039,5041,5043,5045,5047],{"class":1218,"line":874},[1216,5006,1223],{"class":1222},[1216,5008,5009],{"class":1226}," register_webhook",[1216,5011,1231],{"class":1230},[1216,5013,2991],{"class":1234},[1216,5015,1238],{"class":1230},[1216,5017,5018],{"class":1234}," event",[1216,5020,1927],{"class":1230},[1216,5022,3312],{"class":1873},[1216,5024,1238],{"class":1230},[1216,5026,5027],{"class":1234}," entity_id",[1216,5029,1927],{"class":1230},[1216,5031,3312],{"class":1873},[1216,5033,1238],{"class":1230},[1216,5035,5036],{"class":1234}," callback_url",[1216,5038,1927],{"class":1230},[1216,5040,3312],{"class":1873},[1216,5042,1924],{"class":1230},[1216,5044,2996],{"class":1230},[1216,5046,2999],{"class":1873},[1216,5048,1270],{"class":1230},[1216,5050,5051,5053,5056],{"class":1218,"line":1273},[1216,5052,2900],{"class":1256},[1216,5054,5055],{"class":1903},"Register a webhook callback for a specific event on a document.",[1216,5057,2906],{"class":1256},[1216,5059,5060,5062,5064,5066,5068,5070],{"class":1218,"line":1296},[1216,5061,3878],{"class":1260},[1216,5063,1244],{"class":1230},[1216,5065,3020],{"class":1260},[1216,5067,772],{"class":1230},[1216,5069,3887],{"class":1226},[1216,5071,3633],{"class":1230},[1216,5073,5074,5076,5078,5080],{"class":1218,"line":1302},[1216,5075,3894],{"class":1256},[1216,5077,3188],{"class":1230},[1216,5079,3758],{"class":1260},[1216,5081,1270],{"class":1230},[1216,5083,5084,5086],{"class":1218,"line":1332},[1216,5085,3279],{"class":1256},[1216,5087,2922],{"class":1230},[1216,5089,5090],{"class":1218,"line":1365},[1216,5091,1299],{"emptyLinePlaceholder":888},[1216,5093,5094,5096,5098],{"class":1218,"line":2881},[1216,5095,4748],{"class":1260},[1216,5097,1244],{"class":1230},[1216,5099,5100],{"class":1230}," {\n",[1216,5102,5103,5106,5109,5111,5113,5115],{"class":1218,"line":2886},[1216,5104,5105],{"class":1230},"        \"",[1216,5107,5108],{"class":1321},"event",[1216,5110,1318],{"class":1230},[1216,5112,1927],{"class":1230},[1216,5114,5018],{"class":1260},[1216,5116,3451],{"class":1230},[1216,5118,5119,5121,5124,5126,5128,5130],{"class":1218,"line":2897},[1216,5120,5105],{"class":1230},[1216,5122,5123],{"class":1321},"entity_id",[1216,5125,1318],{"class":1230},[1216,5127,1927],{"class":1230},[1216,5129,5027],{"class":1260},[1216,5131,3451],{"class":1230},[1216,5133,5134,5136,5139,5141,5143,5145,5148,5150],{"class":1218,"line":2909},[1216,5135,5105],{"class":1230},[1216,5137,5138],{"class":1321},"action",[1216,5140,1318],{"class":1230},[1216,5142,1927],{"class":1230},[1216,5144,2855],{"class":1230},[1216,5146,5147],{"class":1321},"callback",[1216,5149,1318],{"class":1230},[1216,5151,3451],{"class":1230},[1216,5153,5154,5156,5159,5161,5163],{"class":1218,"line":2914},[1216,5155,5105],{"class":1230},[1216,5157,5158],{"class":1321},"attributes",[1216,5160,1318],{"class":1230},[1216,5162,1927],{"class":1230},[1216,5164,5100],{"class":1230},[1216,5166,5167,5170,5172,5174,5176,5178],{"class":1218,"line":2925},[1216,5168,5169],{"class":1230},"            \"",[1216,5171,5147],{"class":1321},[1216,5173,1318],{"class":1230},[1216,5175,1927],{"class":1230},[1216,5177,5036],{"class":1260},[1216,5179,3451],{"class":1230},[1216,5181,5182,5184,5187,5189,5191],{"class":1218,"line":2936},[1216,5183,5169],{"class":1230},[1216,5185,5186],{"class":1321},"use_tls_12",[1216,5188,1318],{"class":1230},[1216,5190,1927],{"class":1230},[1216,5192,5193],{"class":1230}," True,\n",[1216,5195,5196],{"class":1218,"line":2946},[1216,5197,5198],{"class":1230},"        },\n",[1216,5200,5201],{"class":1218,"line":2956},[1216,5202,5203],{"class":1230},"    }\n",[1216,5205,5206],{"class":1218,"line":2966},[1216,5207,1299],{"emptyLinePlaceholder":888},[1216,5209,5210,5213,5215,5217,5219,5221,5223,5225,5227,5229,5231,5233],{"class":1218,"line":2971},[1216,5211,5212],{"class":1260},"    webhook_secret ",[1216,5214,1244],{"class":1230},[1216,5216,3059],{"class":1226},[1216,5218,1231],{"class":1230},[1216,5220,3064],{"class":1226},[1216,5222,1238],{"class":1230},[1216,5224,2855],{"class":1230},[1216,5226,2534],{"class":1321},[1216,5228,1318],{"class":1230},[1216,5230,1238],{"class":1230},[1216,5232,3077],{"class":1230},[1216,5234,1293],{"class":1230},[1216,5236,5237,5239,5242],{"class":1218,"line":2980},[1216,5238,3894],{"class":1256},[1216,5240,5241],{"class":1260}," webhook_secret",[1216,5243,1270],{"class":1230},[1216,5245,5246,5248,5250,5252,5254,5256,5259,5261,5264,5266,5268,5270],{"class":1218,"line":3004},[1216,5247,4794],{"class":1260},[1216,5249,1352],{"class":1230},[1216,5251,1318],{"class":1230},[1216,5253,5158],{"class":1321},[1216,5255,1318],{"class":1230},[1216,5257,5258],{"class":1230},"][",[1216,5260,1318],{"class":1230},[1216,5262,5263],{"class":1321},"secret_key",[1216,5265,1318],{"class":1230},[1216,5267,4806],{"class":1230},[1216,5269,3056],{"class":1230},[1216,5271,5272],{"class":1260}," webhook_secret\n",[1216,5274,5275],{"class":1218,"line":3015},[1216,5276,1299],{"emptyLinePlaceholder":888},[1216,5278,5279,5281,5283,5285,5287,5289,5291,5293,5295,5297,5299,5301],{"class":1218,"line":3031},[1216,5280,3916],{"class":1256},[1216,5282,3382],{"class":1260},[1216,5284,772],{"class":1230},[1216,5286,3387],{"class":1226},[1216,5288,1231],{"class":1230},[1216,5290,3392],{"class":1234},[1216,5292,1244],{"class":1230},[1216,5294,3397],{"class":1247},[1216,5296,1924],{"class":1230},[1216,5298,3402],{"class":1256},[1216,5300,3405],{"class":1260},[1216,5302,1270],{"class":1230},[1216,5304,5305,5307,5309,5311,5313,5315],{"class":1218,"line":3040},[1216,5306,3944],{"class":1260},[1216,5308,1244],{"class":1230},[1216,5310,3405],{"class":1260},[1216,5312,772],{"class":1230},[1216,5314,3422],{"class":1226},[1216,5316,3425],{"class":1230},[1216,5318,5319,5321,5323,5325,5327,5329,5331,5333,5336],{"class":1218,"line":3045},[1216,5320,3959],{"class":1222},[1216,5322,1318],{"class":1321},[1216,5324,3436],{"class":1247},[1216,5326,2991],{"class":1260},[1216,5328,772],{"class":1230},[1216,5330,3053],{"class":3025},[1216,5332,3445],{"class":1247},[1216,5334,5335],{"class":1321},"/v2/events\"",[1216,5337,3451],{"class":1230},[1216,5339,5340,5342,5344,5346,5348,5350,5352,5354,5356,5358,5360,5362,5364,5366,5368,5370],{"class":1218,"line":3082},[1216,5341,3981],{"class":1234},[1216,5343,4269],{"class":1230},[1216,5345,2991],{"class":1260},[1216,5347,772],{"class":1230},[1216,5349,3990],{"class":1226},[1216,5351,1231],{"class":1230},[1216,5353,3995],{"class":1226},[1216,5355,4282],{"class":1230},[1216,5357,2855],{"class":1230},[1216,5359,3595],{"class":1321},[1216,5361,1318],{"class":1230},[1216,5363,1927],{"class":1230},[1216,5365,2855],{"class":1230},[1216,5367,4295],{"class":1321},[1216,5369,1318],{"class":1230},[1216,5371,4300],{"class":1230},[1216,5373,5374,5376,5378,5380],{"class":1218,"line":3114},[1216,5375,4305],{"class":1234},[1216,5377,1244],{"class":1230},[1216,5379,4956],{"class":1226},[1216,5381,3451],{"class":1230},[1216,5383,5384],{"class":1218,"line":3146},[1216,5385,4042],{"class":1230},[1216,5387,5388,5390,5392,5394],{"class":1218,"line":3178},[1216,5389,4047],{"class":1260},[1216,5391,772],{"class":1230},[1216,5393,3630],{"class":1226},[1216,5395,3633],{"class":1230},[1216,5397,5398,5400],{"class":1218,"line":3183},[1216,5399,3279],{"class":1256},[1216,5401,3037],{"class":1230},[750,5403,5405],{"id":5404},"step-3-track-document-state-with-a-django-model","Step 3: Track document state with a Django model",[746,5407,5408,5409,5411,5412,5415],{},"We need to track each document's journey through the signing pipeline. The ",[1047,5410,2419],{}," model uses Django's ",[1047,5413,5414],{},"GenericForeignKey"," so it can be attached to any model in the system - a deposit confirmation, an NDA, a beta testing agreement, etc.",[1208,5417,5420],{"className":1210,"code":5418,"filename":5419,"language":1212,"meta":873,"style":873},"from django.contrib.contenttypes.fields import GenericForeignKey\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db import models\n\n\nclass SignableDocument(models.Model):\n    \"\"\"Tracks a document uploaded to SignNow for e-signature.\"\"\"\n\n    class Status(models.TextChoices):\n        PENDING = \"pending\", \"Pending Upload\"\n        UPLOADED = \"uploaded\", \"Uploaded to SignNow\"\n        INVITE_SENT = \"invite_sent\", \"Invite Sent\"\n        SIGNED = \"signed\", \"Signed\"\n        DOWNLOADED = \"downloaded\", \"Signed PDF Downloaded\"\n        FAILED = \"failed\", \"Failed\"\n\n    # Generic relation - attach to any Django model\n    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)\n    object_id = models.PositiveIntegerField()\n    source_document = GenericForeignKey(\"content_type\", \"object_id\")\n\n    # SignNow tracking\n    signnow_document_id = models.CharField(max_length=64, blank=True, db_index=True)\n    signnow_invite_id = models.CharField(max_length=64, blank=True)\n\n    # Signer info\n    signer_email = models.EmailField()\n    signer_name = models.CharField(max_length=255, blank=True)\n\n    # Status & errors\n    status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING)\n    error_message = models.TextField(blank=True)\n\n    # Files\n    original_pdf_name = models.CharField(max_length=512, blank=True)\n    signed_pdf = models.FileField(upload_to=\"signnow/signed/%Y/%m/\", blank=True)\n\n    # Timestamps\n    uploaded_at = models.DateTimeField(null=True, blank=True)\n    invite_sent_at = models.DateTimeField(null=True, blank=True)\n    signed_at = models.DateTimeField(null=True, blank=True)\n    downloaded_at = models.DateTimeField(null=True, blank=True)\n","apps/signnow/models.py",[1047,5421,5422,5448,5472,5488,5492,5496,5515,5524,5528,5547,5570,5593,5616,5639,5662,5685,5689,5694,5729,5745,5775,5779,5784,5822,5849,5853,5858,5874,5902,5906,5911,5963,5984,5988,5993,6021,6055,6059,6064,6089,6112,6135],{"__ignoreMap":873},[1216,5423,5424,5426,5428,5430,5433,5435,5438,5440,5443,5445],{"class":1218,"line":1219},[1216,5425,2807],{"class":1256},[1216,5427,2810],{"class":1260},[1216,5429,772],{"class":1230},[1216,5431,5432],{"class":1260},"contrib",[1216,5434,772],{"class":1230},[1216,5436,5437],{"class":1260},"contenttypes",[1216,5439,772],{"class":1230},[1216,5441,5442],{"class":1260},"fields ",[1216,5444,2799],{"class":1256},[1216,5446,5447],{"class":1260}," GenericForeignKey\n",[1216,5449,5450,5452,5454,5456,5458,5460,5462,5464,5467,5469],{"class":1218,"line":874},[1216,5451,2807],{"class":1256},[1216,5453,2810],{"class":1260},[1216,5455,772],{"class":1230},[1216,5457,5432],{"class":1260},[1216,5459,772],{"class":1230},[1216,5461,5437],{"class":1260},[1216,5463,772],{"class":1230},[1216,5465,5466],{"class":1260},"models ",[1216,5468,2799],{"class":1256},[1216,5470,5471],{"class":1260}," ContentType\n",[1216,5473,5474,5476,5478,5480,5483,5485],{"class":1218,"line":1273},[1216,5475,2807],{"class":1256},[1216,5477,2810],{"class":1260},[1216,5479,772],{"class":1230},[1216,5481,5482],{"class":1260},"db ",[1216,5484,2799],{"class":1256},[1216,5486,5487],{"class":1260}," models\n",[1216,5489,5490],{"class":1218,"line":1296},[1216,5491,1299],{"emptyLinePlaceholder":888},[1216,5493,5494],{"class":1218,"line":1302},[1216,5495,1299],{"emptyLinePlaceholder":888},[1216,5497,5498,5500,5503,5505,5508,5510,5513],{"class":1218,"line":1332},[1216,5499,2889],{"class":1222},[1216,5501,5502],{"class":1873}," SignableDocument",[1216,5504,1231],{"class":1230},[1216,5506,5507],{"class":1873},"models",[1216,5509,772],{"class":1230},[1216,5511,5512],{"class":1873},"Model",[1216,5514,1251],{"class":1230},[1216,5516,5517,5519,5522],{"class":1218,"line":1365},[1216,5518,2900],{"class":1256},[1216,5520,5521],{"class":1903},"Tracks a document uploaded to SignNow for e-signature.",[1216,5523,2906],{"class":1256},[1216,5525,5526],{"class":1218,"line":2881},[1216,5527,1299],{"emptyLinePlaceholder":888},[1216,5529,5530,5533,5536,5538,5540,5542,5545],{"class":1218,"line":2886},[1216,5531,5532],{"class":1222},"    class",[1216,5534,5535],{"class":1873}," Status",[1216,5537,1231],{"class":1230},[1216,5539,5507],{"class":1873},[1216,5541,772],{"class":1230},[1216,5543,5544],{"class":1873},"TextChoices",[1216,5546,1251],{"class":1230},[1216,5548,5549,5552,5554,5556,5559,5561,5563,5565,5568],{"class":1218,"line":2897},[1216,5550,5551],{"class":1260},"        PENDING ",[1216,5553,1244],{"class":1230},[1216,5555,2855],{"class":1230},[1216,5557,5558],{"class":1321},"pending",[1216,5560,1318],{"class":1230},[1216,5562,1238],{"class":1230},[1216,5564,2855],{"class":1230},[1216,5566,5567],{"class":1321},"Pending Upload",[1216,5569,2861],{"class":1230},[1216,5571,5572,5575,5577,5579,5582,5584,5586,5588,5591],{"class":1218,"line":2909},[1216,5573,5574],{"class":1260},"        UPLOADED ",[1216,5576,1244],{"class":1230},[1216,5578,2855],{"class":1230},[1216,5580,5581],{"class":1321},"uploaded",[1216,5583,1318],{"class":1230},[1216,5585,1238],{"class":1230},[1216,5587,2855],{"class":1230},[1216,5589,5590],{"class":1321},"Uploaded to SignNow",[1216,5592,2861],{"class":1230},[1216,5594,5595,5598,5600,5602,5605,5607,5609,5611,5614],{"class":1218,"line":2914},[1216,5596,5597],{"class":1260},"        INVITE_SENT ",[1216,5599,1244],{"class":1230},[1216,5601,2855],{"class":1230},[1216,5603,5604],{"class":1321},"invite_sent",[1216,5606,1318],{"class":1230},[1216,5608,1238],{"class":1230},[1216,5610,2855],{"class":1230},[1216,5612,5613],{"class":1321},"Invite Sent",[1216,5615,2861],{"class":1230},[1216,5617,5618,5621,5623,5625,5628,5630,5632,5634,5637],{"class":1218,"line":2925},[1216,5619,5620],{"class":1260},"        SIGNED ",[1216,5622,1244],{"class":1230},[1216,5624,2855],{"class":1230},[1216,5626,5627],{"class":1321},"signed",[1216,5629,1318],{"class":1230},[1216,5631,1238],{"class":1230},[1216,5633,2855],{"class":1230},[1216,5635,5636],{"class":1321},"Signed",[1216,5638,2861],{"class":1230},[1216,5640,5641,5644,5646,5648,5651,5653,5655,5657,5660],{"class":1218,"line":2936},[1216,5642,5643],{"class":1260},"        DOWNLOADED ",[1216,5645,1244],{"class":1230},[1216,5647,2855],{"class":1230},[1216,5649,5650],{"class":1321},"downloaded",[1216,5652,1318],{"class":1230},[1216,5654,1238],{"class":1230},[1216,5656,2855],{"class":1230},[1216,5658,5659],{"class":1321},"Signed PDF Downloaded",[1216,5661,2861],{"class":1230},[1216,5663,5664,5667,5669,5671,5674,5676,5678,5680,5683],{"class":1218,"line":2946},[1216,5665,5666],{"class":1260},"        FAILED ",[1216,5668,1244],{"class":1230},[1216,5670,2855],{"class":1230},[1216,5672,5673],{"class":1321},"failed",[1216,5675,1318],{"class":1230},[1216,5677,1238],{"class":1230},[1216,5679,2855],{"class":1230},[1216,5681,5682],{"class":1321},"Failed",[1216,5684,2861],{"class":1230},[1216,5686,5687],{"class":1218,"line":2956},[1216,5688,1299],{"emptyLinePlaceholder":888},[1216,5690,5691],{"class":1218,"line":2966},[1216,5692,5693],{"class":1903},"    # Generic relation - attach to any Django model\n",[1216,5695,5696,5699,5701,5704,5706,5709,5711,5713,5715,5718,5720,5722,5724,5727],{"class":1218,"line":2971},[1216,5697,5698],{"class":1260},"    content_type ",[1216,5700,1244],{"class":1230},[1216,5702,5703],{"class":1260}," models",[1216,5705,772],{"class":1230},[1216,5707,5708],{"class":1226},"ForeignKey",[1216,5710,1231],{"class":1230},[1216,5712,2423],{"class":1226},[1216,5714,1238],{"class":1230},[1216,5716,5717],{"class":1234}," on_delete",[1216,5719,1244],{"class":1230},[1216,5721,5507],{"class":1226},[1216,5723,772],{"class":1230},[1216,5725,5726],{"class":3025},"CASCADE",[1216,5728,1293],{"class":1230},[1216,5730,5731,5734,5736,5738,5740,5743],{"class":1218,"line":2980},[1216,5732,5733],{"class":1260},"    object_id ",[1216,5735,1244],{"class":1230},[1216,5737,5703],{"class":1260},[1216,5739,772],{"class":1230},[1216,5741,5742],{"class":1226},"PositiveIntegerField",[1216,5744,3633],{"class":1230},[1216,5746,5747,5750,5752,5755,5757,5759,5762,5764,5766,5768,5771,5773],{"class":1218,"line":3004},[1216,5748,5749],{"class":1260},"    source_document ",[1216,5751,1244],{"class":1230},[1216,5753,5754],{"class":1226}," GenericForeignKey",[1216,5756,1231],{"class":1230},[1216,5758,1318],{"class":1230},[1216,5760,5761],{"class":1321},"content_type",[1216,5763,1318],{"class":1230},[1216,5765,1238],{"class":1230},[1216,5767,2855],{"class":1230},[1216,5769,5770],{"class":1321},"object_id",[1216,5772,1318],{"class":1230},[1216,5774,1293],{"class":1230},[1216,5776,5777],{"class":1218,"line":3015},[1216,5778,1299],{"emptyLinePlaceholder":888},[1216,5780,5781],{"class":1218,"line":3031},[1216,5782,5783],{"class":1903},"    # SignNow tracking\n",[1216,5785,5786,5789,5791,5793,5795,5798,5800,5803,5805,5808,5810,5813,5816,5819],{"class":1218,"line":3040},[1216,5787,5788],{"class":1260},"    signnow_document_id ",[1216,5790,1244],{"class":1230},[1216,5792,5703],{"class":1260},[1216,5794,772],{"class":1230},[1216,5796,5797],{"class":1226},"CharField",[1216,5799,1231],{"class":1230},[1216,5801,5802],{"class":1234},"max_length",[1216,5804,1244],{"class":1230},[1216,5806,5807],{"class":1247},"64",[1216,5809,1238],{"class":1230},[1216,5811,5812],{"class":1234}," blank",[1216,5814,5815],{"class":1230},"=True,",[1216,5817,5818],{"class":1234}," db_index",[1216,5820,5821],{"class":1230},"=True)\n",[1216,5823,5824,5827,5829,5831,5833,5835,5837,5839,5841,5843,5845,5847],{"class":1218,"line":3045},[1216,5825,5826],{"class":1260},"    signnow_invite_id ",[1216,5828,1244],{"class":1230},[1216,5830,5703],{"class":1260},[1216,5832,772],{"class":1230},[1216,5834,5797],{"class":1226},[1216,5836,1231],{"class":1230},[1216,5838,5802],{"class":1234},[1216,5840,1244],{"class":1230},[1216,5842,5807],{"class":1247},[1216,5844,1238],{"class":1230},[1216,5846,5812],{"class":1234},[1216,5848,5821],{"class":1230},[1216,5850,5851],{"class":1218,"line":3082},[1216,5852,1299],{"emptyLinePlaceholder":888},[1216,5854,5855],{"class":1218,"line":3114},[1216,5856,5857],{"class":1903},"    # Signer info\n",[1216,5859,5860,5863,5865,5867,5869,5872],{"class":1218,"line":3146},[1216,5861,5862],{"class":1260},"    signer_email ",[1216,5864,1244],{"class":1230},[1216,5866,5703],{"class":1260},[1216,5868,772],{"class":1230},[1216,5870,5871],{"class":1226},"EmailField",[1216,5873,3633],{"class":1230},[1216,5875,5876,5879,5881,5883,5885,5887,5889,5891,5893,5896,5898,5900],{"class":1218,"line":3178},[1216,5877,5878],{"class":1260},"    signer_name ",[1216,5880,1244],{"class":1230},[1216,5882,5703],{"class":1260},[1216,5884,772],{"class":1230},[1216,5886,5797],{"class":1226},[1216,5888,1231],{"class":1230},[1216,5890,5802],{"class":1234},[1216,5892,1244],{"class":1230},[1216,5894,5895],{"class":1247},"255",[1216,5897,1238],{"class":1230},[1216,5899,5812],{"class":1234},[1216,5901,5821],{"class":1230},[1216,5903,5904],{"class":1218,"line":3183},[1216,5905,1299],{"emptyLinePlaceholder":888},[1216,5907,5908],{"class":1218,"line":3230},[1216,5909,5910],{"class":1903},"    # Status & errors\n",[1216,5912,5913,5916,5918,5920,5922,5924,5926,5928,5930,5933,5935,5938,5940,5943,5945,5948,5950,5952,5954,5956,5958,5961],{"class":1218,"line":3251},[1216,5914,5915],{"class":1260},"    status ",[1216,5917,1244],{"class":1230},[1216,5919,5703],{"class":1260},[1216,5921,772],{"class":1230},[1216,5923,5797],{"class":1226},[1216,5925,1231],{"class":1230},[1216,5927,5802],{"class":1234},[1216,5929,1244],{"class":1230},[1216,5931,5932],{"class":1247},"16",[1216,5934,1238],{"class":1230},[1216,5936,5937],{"class":1234}," choices",[1216,5939,1244],{"class":1230},[1216,5941,5942],{"class":1226},"Status",[1216,5944,772],{"class":1230},[1216,5946,5947],{"class":3025},"choices",[1216,5949,1238],{"class":1230},[1216,5951,2605],{"class":1234},[1216,5953,1244],{"class":1230},[1216,5955,5942],{"class":1226},[1216,5957,772],{"class":1230},[1216,5959,5960],{"class":3025},"PENDING",[1216,5962,1293],{"class":1230},[1216,5964,5965,5968,5970,5972,5974,5977,5979,5982],{"class":1218,"line":3258},[1216,5966,5967],{"class":1260},"    error_message ",[1216,5969,1244],{"class":1230},[1216,5971,5703],{"class":1260},[1216,5973,772],{"class":1230},[1216,5975,5976],{"class":1226},"TextField",[1216,5978,1231],{"class":1230},[1216,5980,5981],{"class":1234},"blank",[1216,5983,5821],{"class":1230},[1216,5985,5986],{"class":1218,"line":3263},[1216,5987,1299],{"emptyLinePlaceholder":888},[1216,5989,5990],{"class":1218,"line":3276},[1216,5991,5992],{"class":1903},"    # Files\n",[1216,5994,5995,5998,6000,6002,6004,6006,6008,6010,6012,6015,6017,6019],{"class":1218,"line":3284},[1216,5996,5997],{"class":1260},"    original_pdf_name ",[1216,5999,1244],{"class":1230},[1216,6001,5703],{"class":1260},[1216,6003,772],{"class":1230},[1216,6005,5797],{"class":1226},[1216,6007,1231],{"class":1230},[1216,6009,5802],{"class":1234},[1216,6011,1244],{"class":1230},[1216,6013,6014],{"class":1247},"512",[1216,6016,1238],{"class":1230},[1216,6018,5812],{"class":1234},[1216,6020,5821],{"class":1230},[1216,6022,6023,6026,6028,6030,6032,6035,6037,6040,6042,6044,6047,6049,6051,6053],{"class":1218,"line":3289},[1216,6024,6025],{"class":1260},"    signed_pdf ",[1216,6027,1244],{"class":1230},[1216,6029,5703],{"class":1260},[1216,6031,772],{"class":1230},[1216,6033,6034],{"class":1226},"FileField",[1216,6036,1231],{"class":1230},[1216,6038,6039],{"class":1234},"upload_to",[1216,6041,1244],{"class":1230},[1216,6043,1318],{"class":1230},[1216,6045,6046],{"class":1321},"signnow/signed/%Y/%m/",[1216,6048,1318],{"class":1230},[1216,6050,1238],{"class":1230},[1216,6052,5812],{"class":1234},[1216,6054,5821],{"class":1230},[1216,6056,6057],{"class":1218,"line":3296},[1216,6058,1299],{"emptyLinePlaceholder":888},[1216,6060,6061],{"class":1218,"line":3321},[1216,6062,6063],{"class":1903},"    # Timestamps\n",[1216,6065,6066,6069,6071,6073,6075,6078,6080,6083,6085,6087],{"class":1218,"line":3331},[1216,6067,6068],{"class":1260},"    uploaded_at ",[1216,6070,1244],{"class":1230},[1216,6072,5703],{"class":1260},[1216,6074,772],{"class":1230},[1216,6076,6077],{"class":1226},"DateTimeField",[1216,6079,1231],{"class":1230},[1216,6081,6082],{"class":1234},"null",[1216,6084,5815],{"class":1230},[1216,6086,5812],{"class":1234},[1216,6088,5821],{"class":1230},[1216,6090,6091,6094,6096,6098,6100,6102,6104,6106,6108,6110],{"class":1218,"line":3353},[1216,6092,6093],{"class":1260},"    invite_sent_at ",[1216,6095,1244],{"class":1230},[1216,6097,5703],{"class":1260},[1216,6099,772],{"class":1230},[1216,6101,6077],{"class":1226},[1216,6103,1231],{"class":1230},[1216,6105,6082],{"class":1234},[1216,6107,5815],{"class":1230},[1216,6109,5812],{"class":1234},[1216,6111,5821],{"class":1230},[1216,6113,6114,6117,6119,6121,6123,6125,6127,6129,6131,6133],{"class":1218,"line":3363},[1216,6115,6116],{"class":1260},"    signed_at ",[1216,6118,1244],{"class":1230},[1216,6120,5703],{"class":1260},[1216,6122,772],{"class":1230},[1216,6124,6077],{"class":1226},[1216,6126,1231],{"class":1230},[1216,6128,6082],{"class":1234},[1216,6130,5815],{"class":1230},[1216,6132,5812],{"class":1234},[1216,6134,5821],{"class":1230},[1216,6136,6137,6140,6142,6144,6146,6148,6150,6152,6154,6156],{"class":1218,"line":3371},[1216,6138,6139],{"class":1260},"    downloaded_at ",[1216,6141,1244],{"class":1230},[1216,6143,5703],{"class":1260},[1216,6145,772],{"class":1230},[1216,6147,6077],{"class":1226},[1216,6149,1231],{"class":1230},[1216,6151,6082],{"class":1234},[1216,6153,5815],{"class":1230},[1216,6155,5812],{"class":1234},[1216,6157,5821],{"class":1230},[746,6159,6160],{},"The status flow is linear and predictable:",[1208,6162,6165],{"className":6163,"code":6164,"language":1410},[1408],"PENDING → UPLOADED → INVITE_SENT → SIGNED → DOWNLOADED\n                                                ↘ FAILED (at any step)\n",[1047,6166,6164],{"__ignoreMap":873},[750,6168,6170],{"id":6169},"step-4-build-the-async-signing-pipeline-with-celery","Step 4: Build the async signing pipeline with Celery",[746,6172,6173],{},"This is where everything comes together. A single Celery task orchestrates the entire signing flow - upload the document, add signature fields, send the invite, and register the webhook:",[1208,6175,6178],{"className":1210,"code":6176,"filename":6177,"language":1212,"meta":873,"style":873},"from celery import shared_task\n\n# Signature field position (calibrate for your PDF layout)\nTESTER_SIGNATURE_FIELD = {\n    \"x\": 350,\n    \"y\": 700,\n    \"width\": 200,\n    \"height\": 50,\n    \"page_number\": 0,\n    \"type\": \"signature\",\n    \"role\": \"Signer 1\",\n    \"required\": True,\n    \"label\": \"Tester Signature\",\n}\n\n\n@shared_task(bind=True, max_retries=3, default_retry_delay=60)\ndef upload_and_send_for_signature(self, signable_document_id: int):\n    \"\"\"Full pipeline: upload PDF → add fields → send invite → register webhook.\"\"\"\n    from signnow.models import SignableDocument\n    from signnow.services.signnow_service import SignNowService\n\n    signable = SignableDocument.objects.select_related(\"content_type\").get(\n        id=signable_document_id\n    )\n\n    try:\n        # 1. Read PDF from storage\n        source = signable.source_document\n        pdf_bytes = source.pdf_file.read()\n        filename = source.pdf_file.name.split(\"/\")[-1]\n\n        # 2. Upload to SignNow\n        if not signable.signnow_document_id:\n            result = SignNowService.upload_document(pdf_bytes, filename)\n            if not result:\n                raise RuntimeError(\"Failed to upload document to SignNow\")\n\n            signable.signnow_document_id = result[\"id\"]\n            signable.status = SignableDocument.Status.UPLOADED\n            signable.uploaded_at = timezone.now()\n            signable.save(update_fields=[\"signnow_document_id\", \"status\", \"uploaded_at\"])\n\n        # 3. Add signature fields\n        SignNowService.add_signature_fields(\n            signable.signnow_document_id,\n            [TESTER_SIGNATURE_FIELD],\n        )\n\n        # 4. Get role_id (assigned when fields were added)\n        doc_data = SignNowService.get_document(signable.signnow_document_id)\n        roles = doc_data.get(\"roles\", [])\n        signer_role = next(r for r in roles if r.get(\"name\") == \"Signer 1\")\n\n        # 5. Send role-based invite\n        signers = [{\n            \"email\": signable.signer_email,\n            \"role\": \"Signer 1\",\n            \"role_id\": signer_role[\"unique_id\"],\n            \"order\": 1,\n        }]\n\n        invite_result = SignNowService.send_role_based_invite(\n            document_id=signable.signnow_document_id,\n            signers=signers,\n            from_email=settings.SIGNNOW_USERNAME,\n        )\n\n        signable.status = SignableDocument.Status.INVITE_SENT\n        signable.invite_sent_at = timezone.now()\n        signable.save(update_fields=[\"status\", \"invite_sent_at\"])\n\n        # 6. Register webhook for document completion\n        callback_url = settings.SIGNNOW_WEBHOOK_CALLBACK_URL\n        if callback_url:\n            SignNowService.register_webhook(\n                event=\"document.complete\",\n                entity_id=signable.signnow_document_id,\n                callback_url=callback_url,\n            )\n\n    except Exception as exc:\n        if self.request.retries >= self.max_retries:\n            signable.status = SignableDocument.Status.FAILED\n            signable.error_message = str(exc)\n            signable.save(update_fields=[\"status\", \"error_message\"])\n            return\n        self.retry(exc=exc)\n","apps/signnow/tasks.py",[1047,6179,6180,6192,6196,6201,6210,6227,6243,6259,6275,6291,6310,6330,6343,6363,6367,6371,6375,6409,6434,6443,6460,6481,6485,6519,6529,6534,6538,6545,6550,6565,6587,6627,6631,6636,6651,6676,6688,6707,6711,6734,6756,6777,6818,6822,6827,6839,6849,6859,6863,6867,6872,6897,6925,6982,6986,6991,7001,7021,7039,7064,7079,7084,7088,7104,7119,7131,7147,7152,7157,7180,7200,7231,7236,7242,7258,7267,7280,7297,7313,7326,7331,7336,7352,7382,7404,7425,7456,7462],{"__ignoreMap":873},[1216,6181,6182,6184,6187,6189],{"class":1218,"line":1219},[1216,6183,2807],{"class":1256},[1216,6185,6186],{"class":1260}," celery ",[1216,6188,2799],{"class":1256},[1216,6190,6191],{"class":1260}," shared_task\n",[1216,6193,6194],{"class":1218,"line":874},[1216,6195,1299],{"emptyLinePlaceholder":888},[1216,6197,6198],{"class":1218,"line":1273},[1216,6199,6200],{"class":1903},"# Signature field position (calibrate for your PDF layout)\n",[1216,6202,6203,6206,6208],{"class":1218,"line":1296},[1216,6204,6205],{"class":1260},"TESTER_SIGNATURE_FIELD ",[1216,6207,1244],{"class":1230},[1216,6209,5100],{"class":1230},[1216,6211,6212,6215,6218,6220,6222,6225],{"class":1218,"line":1302},[1216,6213,6214],{"class":1230},"    \"",[1216,6216,6217],{"class":1321},"x",[1216,6219,1318],{"class":1230},[1216,6221,1927],{"class":1230},[1216,6223,6224],{"class":1247}," 350",[1216,6226,3451],{"class":1230},[1216,6228,6229,6231,6234,6236,6238,6241],{"class":1218,"line":1332},[1216,6230,6214],{"class":1230},[1216,6232,6233],{"class":1321},"y",[1216,6235,1318],{"class":1230},[1216,6237,1927],{"class":1230},[1216,6239,6240],{"class":1247}," 700",[1216,6242,3451],{"class":1230},[1216,6244,6245,6247,6250,6252,6254,6257],{"class":1218,"line":1365},[1216,6246,6214],{"class":1230},[1216,6248,6249],{"class":1321},"width",[1216,6251,1318],{"class":1230},[1216,6253,1927],{"class":1230},[1216,6255,6256],{"class":1247}," 200",[1216,6258,3451],{"class":1230},[1216,6260,6261,6263,6266,6268,6270,6273],{"class":1218,"line":2881},[1216,6262,6214],{"class":1230},[1216,6264,6265],{"class":1321},"height",[1216,6267,1318],{"class":1230},[1216,6269,1927],{"class":1230},[1216,6271,6272],{"class":1247}," 50",[1216,6274,3451],{"class":1230},[1216,6276,6277,6279,6282,6284,6286,6289],{"class":1218,"line":2886},[1216,6278,6214],{"class":1230},[1216,6280,6281],{"class":1321},"page_number",[1216,6283,1318],{"class":1230},[1216,6285,1927],{"class":1230},[1216,6287,6288],{"class":1247}," 0",[1216,6290,3451],{"class":1230},[1216,6292,6293,6295,6297,6299,6301,6303,6306,6308],{"class":1218,"line":2897},[1216,6294,6214],{"class":1230},[1216,6296,4521],{"class":1321},[1216,6298,1318],{"class":1230},[1216,6300,1927],{"class":1230},[1216,6302,2855],{"class":1230},[1216,6304,6305],{"class":1321},"signature",[1216,6307,1318],{"class":1230},[1216,6309,3451],{"class":1230},[1216,6311,6312,6314,6317,6319,6321,6323,6326,6328],{"class":1218,"line":2909},[1216,6313,6214],{"class":1230},[1216,6315,6316],{"class":1321},"role",[1216,6318,1318],{"class":1230},[1216,6320,1927],{"class":1230},[1216,6322,2855],{"class":1230},[1216,6324,6325],{"class":1321},"Signer 1",[1216,6327,1318],{"class":1230},[1216,6329,3451],{"class":1230},[1216,6331,6332,6334,6337,6339,6341],{"class":1218,"line":2914},[1216,6333,6214],{"class":1230},[1216,6335,6336],{"class":1321},"required",[1216,6338,1318],{"class":1230},[1216,6340,1927],{"class":1230},[1216,6342,5193],{"class":1230},[1216,6344,6345,6347,6350,6352,6354,6356,6359,6361],{"class":1218,"line":2925},[1216,6346,6214],{"class":1230},[1216,6348,6349],{"class":1321},"label",[1216,6351,1318],{"class":1230},[1216,6353,1927],{"class":1230},[1216,6355,2855],{"class":1230},[1216,6357,6358],{"class":1321},"Tester Signature",[1216,6360,1318],{"class":1230},[1216,6362,3451],{"class":1230},[1216,6364,6365],{"class":1218,"line":2936},[1216,6366,4780],{"class":1230},[1216,6368,6369],{"class":1218,"line":2946},[1216,6370,1299],{"emptyLinePlaceholder":888},[1216,6372,6373],{"class":1218,"line":2956},[1216,6374,1299],{"emptyLinePlaceholder":888},[1216,6376,6377,6379,6382,6384,6387,6389,6392,6394,6397,6399,6402,6404,6407],{"class":1218,"line":2966},[1216,6378,3821],{"class":1230},[1216,6380,6381],{"class":1226},"shared_task",[1216,6383,1231],{"class":1230},[1216,6385,6386],{"class":1234},"bind",[1216,6388,5815],{"class":1230},[1216,6390,6391],{"class":1234}," max_retries",[1216,6393,1244],{"class":1230},[1216,6395,6396],{"class":1247},"3",[1216,6398,1238],{"class":1230},[1216,6400,6401],{"class":1234}," default_retry_delay",[1216,6403,1244],{"class":1230},[1216,6405,6406],{"class":1247},"60",[1216,6408,1293],{"class":1230},[1216,6410,6411,6413,6416,6418,6422,6424,6427,6429,6432],{"class":1218,"line":2971},[1216,6412,1223],{"class":1222},[1216,6414,6415],{"class":1226}," upload_and_send_for_signature",[1216,6417,1231],{"class":1230},[1216,6419,6421],{"class":6420},"s5tWE","self",[1216,6423,1238],{"class":1230},[1216,6425,6426],{"class":1234}," signable_document_id",[1216,6428,1927],{"class":1230},[1216,6430,6431],{"class":1873}," int",[1216,6433,1251],{"class":1230},[1216,6435,6436,6438,6441],{"class":1218,"line":2980},[1216,6437,2900],{"class":1256},[1216,6439,6440],{"class":1903},"Full pipeline: upload PDF → add fields → send invite → register webhook.",[1216,6442,2906],{"class":1256},[1216,6444,6445,6448,6451,6453,6455,6457],{"class":1218,"line":3004},[1216,6446,6447],{"class":1256},"    from",[1216,6449,6450],{"class":1260}," signnow",[1216,6452,772],{"class":1230},[1216,6454,5466],{"class":1260},[1216,6456,2799],{"class":1256},[1216,6458,6459],{"class":1260}," SignableDocument\n",[1216,6461,6462,6464,6466,6468,6471,6473,6476,6478],{"class":1218,"line":3015},[1216,6463,6447],{"class":1256},[1216,6465,6450],{"class":1260},[1216,6467,772],{"class":1230},[1216,6469,6470],{"class":1260},"services",[1216,6472,772],{"class":1230},[1216,6474,6475],{"class":1260},"signnow_service ",[1216,6477,2799],{"class":1256},[1216,6479,6480],{"class":1260}," SignNowService\n",[1216,6482,6483],{"class":1218,"line":3031},[1216,6484,1299],{"emptyLinePlaceholder":888},[1216,6486,6487,6490,6492,6494,6496,6499,6501,6504,6506,6508,6510,6512,6515,6517],{"class":1218,"line":3040},[1216,6488,6489],{"class":1260},"    signable ",[1216,6491,1244],{"class":1230},[1216,6493,5502],{"class":1260},[1216,6495,772],{"class":1230},[1216,6497,6498],{"class":3025},"objects",[1216,6500,772],{"class":1230},[1216,6502,6503],{"class":1226},"select_related",[1216,6505,1231],{"class":1230},[1216,6507,1318],{"class":1230},[1216,6509,5761],{"class":1321},[1216,6511,1318],{"class":1230},[1216,6513,6514],{"class":1230},").",[1216,6516,1313],{"class":1226},[1216,6518,3425],{"class":1230},[1216,6520,6521,6524,6526],{"class":1218,"line":3045},[1216,6522,6523],{"class":1234},"        id",[1216,6525,1244],{"class":1230},[1216,6527,6528],{"class":1226},"signable_document_id\n",[1216,6530,6531],{"class":1218,"line":3082},[1216,6532,6533],{"class":1230},"    )\n",[1216,6535,6536],{"class":1218,"line":3114},[1216,6537,1299],{"emptyLinePlaceholder":888},[1216,6539,6540,6543],{"class":1218,"line":3146},[1216,6541,6542],{"class":1256},"    try",[1216,6544,1270],{"class":1230},[1216,6546,6547],{"class":1218,"line":3178},[1216,6548,6549],{"class":1903},"        # 1. Read PDF from storage\n",[1216,6551,6552,6555,6557,6560,6562],{"class":1218,"line":3183},[1216,6553,6554],{"class":1260},"        source ",[1216,6556,1244],{"class":1230},[1216,6558,6559],{"class":1260}," signable",[1216,6561,772],{"class":1230},[1216,6563,6564],{"class":3025},"source_document\n",[1216,6566,6567,6570,6572,6575,6577,6580,6582,6585],{"class":1218,"line":3230},[1216,6568,6569],{"class":1260},"        pdf_bytes ",[1216,6571,1244],{"class":1230},[1216,6573,6574],{"class":1260}," source",[1216,6576,772],{"class":1230},[1216,6578,6579],{"class":3025},"pdf_file",[1216,6581,772],{"class":1230},[1216,6583,6584],{"class":1226},"read",[1216,6586,3633],{"class":1230},[1216,6588,6589,6592,6594,6596,6598,6600,6602,6605,6607,6610,6612,6614,6617,6619,6622,6625],{"class":1218,"line":3251},[1216,6590,6591],{"class":1260},"        filename ",[1216,6593,1244],{"class":1230},[1216,6595,6574],{"class":1260},[1216,6597,772],{"class":1230},[1216,6599,6579],{"class":3025},[1216,6601,772],{"class":1230},[1216,6603,6604],{"class":3025},"name",[1216,6606,772],{"class":1230},[1216,6608,6609],{"class":1226},"split",[1216,6611,1231],{"class":1230},[1216,6613,1318],{"class":1230},[1216,6615,6616],{"class":1321},"/",[1216,6618,1318],{"class":1230},[1216,6620,6621],{"class":1230},")[-",[1216,6623,6624],{"class":1247},"1",[1216,6626,3679],{"class":1230},[1216,6628,6629],{"class":1218,"line":3258},[1216,6630,1299],{"emptyLinePlaceholder":888},[1216,6632,6633],{"class":1218,"line":3263},[1216,6634,6635],{"class":1903},"        # 2. Upload to SignNow\n",[1216,6637,6638,6640,6642,6644,6646,6649],{"class":1218,"line":3276},[1216,6639,1305],{"class":1256},[1216,6641,3188],{"class":1230},[1216,6643,6559],{"class":1260},[1216,6645,772],{"class":1230},[1216,6647,6648],{"class":3025},"signnow_document_id",[1216,6650,1270],{"class":1230},[1216,6652,6653,6656,6658,6660,6662,6665,6667,6670,6672,6674],{"class":1218,"line":3284},[1216,6654,6655],{"class":1260},"            result ",[1216,6657,1244],{"class":1230},[1216,6659,2892],{"class":1260},[1216,6661,772],{"class":1230},[1216,6663,6664],{"class":1226},"upload_document",[1216,6666,1231],{"class":1230},[1216,6668,6669],{"class":1226},"pdf_bytes",[1216,6671,1238],{"class":1230},[1216,6673,3849],{"class":1226},[1216,6675,1293],{"class":1230},[1216,6677,6678,6681,6683,6686],{"class":1218,"line":3289},[1216,6679,6680],{"class":1256},"            if",[1216,6682,3188],{"class":1230},[1216,6684,6685],{"class":1260}," result",[1216,6687,1270],{"class":1230},[1216,6689,6690,6693,6696,6698,6700,6703,6705],{"class":1218,"line":3296},[1216,6691,6692],{"class":1256},"                raise",[1216,6694,6695],{"class":1873}," RuntimeError",[1216,6697,1231],{"class":1230},[1216,6699,1318],{"class":1230},[1216,6701,6702],{"class":1321},"Failed to upload document to SignNow",[1216,6704,1318],{"class":1230},[1216,6706,1293],{"class":1230},[1216,6708,6709],{"class":1218,"line":3321},[1216,6710,1299],{"emptyLinePlaceholder":888},[1216,6712,6713,6716,6718,6720,6722,6724,6726,6728,6730,6732],{"class":1218,"line":3331},[1216,6714,6715],{"class":1260},"            signable",[1216,6717,772],{"class":1230},[1216,6719,6648],{"class":3025},[1216,6721,3056],{"class":1230},[1216,6723,6685],{"class":1260},[1216,6725,1352],{"class":1230},[1216,6727,1318],{"class":1230},[1216,6729,1357],{"class":1321},[1216,6731,1318],{"class":1230},[1216,6733,3679],{"class":1230},[1216,6735,6736,6738,6740,6743,6745,6747,6749,6751,6753],{"class":1218,"line":3353},[1216,6737,6715],{"class":1260},[1216,6739,772],{"class":1230},[1216,6741,6742],{"class":3025},"status",[1216,6744,3056],{"class":1230},[1216,6746,5502],{"class":1260},[1216,6748,772],{"class":1230},[1216,6750,5942],{"class":3025},[1216,6752,772],{"class":1230},[1216,6754,6755],{"class":3025},"UPLOADED\n",[1216,6757,6758,6760,6762,6765,6767,6770,6772,6775],{"class":1218,"line":3363},[1216,6759,6715],{"class":1260},[1216,6761,772],{"class":1230},[1216,6763,6764],{"class":3025},"uploaded_at",[1216,6766,3056],{"class":1230},[1216,6768,6769],{"class":1260}," timezone",[1216,6771,772],{"class":1230},[1216,6773,6774],{"class":1226},"now",[1216,6776,3633],{"class":1230},[1216,6778,6779,6781,6783,6786,6788,6791,6794,6796,6798,6800,6802,6804,6806,6808,6810,6812,6814,6816],{"class":1218,"line":3371},[1216,6780,6715],{"class":1260},[1216,6782,772],{"class":1230},[1216,6784,6785],{"class":1226},"save",[1216,6787,1231],{"class":1230},[1216,6789,6790],{"class":1234},"update_fields",[1216,6792,6793],{"class":1230},"=[",[1216,6795,1318],{"class":1230},[1216,6797,6648],{"class":1321},[1216,6799,1318],{"class":1230},[1216,6801,1238],{"class":1230},[1216,6803,2855],{"class":1230},[1216,6805,6742],{"class":1321},[1216,6807,1318],{"class":1230},[1216,6809,1238],{"class":1230},[1216,6811,2855],{"class":1230},[1216,6813,6764],{"class":1321},[1216,6815,1318],{"class":1230},[1216,6817,1362],{"class":1230},[1216,6819,6820],{"class":1218,"line":3376},[1216,6821,1299],{"emptyLinePlaceholder":888},[1216,6823,6824],{"class":1218,"line":3410},[1216,6825,6826],{"class":1903},"        # 3. Add signature fields\n",[1216,6828,6829,6832,6834,6837],{"class":1218,"line":3428},[1216,6830,6831],{"class":1260},"        SignNowService",[1216,6833,772],{"class":1230},[1216,6835,6836],{"class":1226},"add_signature_fields",[1216,6838,3425],{"class":1230},[1216,6840,6841,6843,6845,6847],{"class":1218,"line":3454},[1216,6842,6715],{"class":1226},[1216,6844,772],{"class":1230},[1216,6846,6648],{"class":3025},[1216,6848,3451],{"class":1230},[1216,6850,6851,6854,6857],{"class":1218,"line":3463},[1216,6852,6853],{"class":1230},"            [",[1216,6855,6856],{"class":1226},"TESTER_SIGNATURE_FIELD",[1216,6858,4637],{"class":1230},[1216,6860,6861],{"class":1218,"line":3485},[1216,6862,4042],{"class":1230},[1216,6864,6865],{"class":1218,"line":3505},[1216,6866,1299],{"emptyLinePlaceholder":888},[1216,6868,6869],{"class":1218,"line":3524},[1216,6870,6871],{"class":1903},"        # 4. Get role_id (assigned when fields were added)\n",[1216,6873,6874,6877,6879,6881,6883,6886,6888,6891,6893,6895],{"class":1218,"line":3544},[1216,6875,6876],{"class":1260},"        doc_data ",[1216,6878,1244],{"class":1230},[1216,6880,2892],{"class":1260},[1216,6882,772],{"class":1230},[1216,6884,6885],{"class":1226},"get_document",[1216,6887,1231],{"class":1230},[1216,6889,6890],{"class":1226},"signable",[1216,6892,772],{"class":1230},[1216,6894,6648],{"class":3025},[1216,6896,1293],{"class":1230},[1216,6898,6899,6902,6904,6907,6909,6911,6913,6915,6918,6920,6922],{"class":1218,"line":3550},[1216,6900,6901],{"class":1260},"        roles ",[1216,6903,1244],{"class":1230},[1216,6905,6906],{"class":1260}," doc_data",[1216,6908,772],{"class":1230},[1216,6910,1313],{"class":1226},[1216,6912,1231],{"class":1230},[1216,6914,1318],{"class":1230},[1216,6916,6917],{"class":1321},"roles",[1216,6919,1318],{"class":1230},[1216,6921,1238],{"class":1230},[1216,6923,6924],{"class":1230}," [])\n",[1216,6926,6927,6930,6932,6935,6937,6940,6943,6946,6948,6951,6954,6957,6959,6961,6963,6965,6967,6969,6971,6974,6976,6978,6980],{"class":1218,"line":3558},[1216,6928,6929],{"class":1260},"        signer_role ",[1216,6931,1244],{"class":1230},[1216,6933,6934],{"class":1226}," next",[1216,6936,1231],{"class":1230},[1216,6938,6939],{"class":1226},"r ",[1216,6941,6942],{"class":1256},"for",[1216,6944,6945],{"class":1226}," r ",[1216,6947,1264],{"class":1256},[1216,6949,6950],{"class":1226}," roles ",[1216,6952,6953],{"class":1256},"if",[1216,6955,6956],{"class":1226}," r",[1216,6958,772],{"class":1230},[1216,6960,1313],{"class":1226},[1216,6962,1231],{"class":1230},[1216,6964,1318],{"class":1230},[1216,6966,6604],{"class":1321},[1216,6968,1318],{"class":1230},[1216,6970,1924],{"class":1230},[1216,6972,6973],{"class":1230}," ==",[1216,6975,2855],{"class":1230},[1216,6977,6325],{"class":1321},[1216,6979,1318],{"class":1230},[1216,6981,1293],{"class":1230},[1216,6983,6984],{"class":1218,"line":3590},[1216,6985,1299],{"emptyLinePlaceholder":888},[1216,6987,6988],{"class":1218,"line":3611},[1216,6989,6990],{"class":1903},"        # 5. Send role-based invite\n",[1216,6992,6993,6996,6998],{"class":1218,"line":3616},[1216,6994,6995],{"class":1260},"        signers ",[1216,6997,1244],{"class":1230},[1216,6999,7000],{"class":1230}," [{\n",[1216,7002,7003,7005,7008,7010,7012,7014,7016,7019],{"class":1218,"line":3622},[1216,7004,5169],{"class":1230},[1216,7006,7007],{"class":1321},"email",[1216,7009,1318],{"class":1230},[1216,7011,1927],{"class":1230},[1216,7013,6559],{"class":1260},[1216,7015,772],{"class":1230},[1216,7017,7018],{"class":3025},"signer_email",[1216,7020,3451],{"class":1230},[1216,7022,7023,7025,7027,7029,7031,7033,7035,7037],{"class":1218,"line":3636},[1216,7024,5169],{"class":1230},[1216,7026,6316],{"class":1321},[1216,7028,1318],{"class":1230},[1216,7030,1927],{"class":1230},[1216,7032,2855],{"class":1230},[1216,7034,6325],{"class":1321},[1216,7036,1318],{"class":1230},[1216,7038,3451],{"class":1230},[1216,7040,7041,7043,7046,7048,7050,7053,7055,7057,7060,7062],{"class":1218,"line":3654},[1216,7042,5169],{"class":1230},[1216,7044,7045],{"class":1321},"role_id",[1216,7047,1318],{"class":1230},[1216,7049,1927],{"class":1230},[1216,7051,7052],{"class":1260}," signer_role",[1216,7054,1352],{"class":1230},[1216,7056,1318],{"class":1230},[1216,7058,7059],{"class":1321},"unique_id",[1216,7061,1318],{"class":1230},[1216,7063,4637],{"class":1230},[1216,7065,7066,7068,7071,7073,7075,7077],{"class":1218,"line":3659},[1216,7067,5169],{"class":1230},[1216,7069,7070],{"class":1321},"order",[1216,7072,1318],{"class":1230},[1216,7074,1927],{"class":1230},[1216,7076,1384],{"class":1247},[1216,7078,3451],{"class":1230},[1216,7080,7081],{"class":1218,"line":3682},[1216,7082,7083],{"class":1230},"        }]\n",[1216,7085,7086],{"class":1218,"line":3712},[1216,7087,1299],{"emptyLinePlaceholder":888},[1216,7089,7090,7093,7095,7097,7099,7102],{"class":1218,"line":3741},[1216,7091,7092],{"class":1260},"        invite_result ",[1216,7094,1244],{"class":1230},[1216,7096,2892],{"class":1260},[1216,7098,772],{"class":1230},[1216,7100,7101],{"class":1226},"send_role_based_invite",[1216,7103,3425],{"class":1230},[1216,7105,7106,7109,7111,7113,7115,7117],{"class":1218,"line":3773},[1216,7107,7108],{"class":1234},"            document_id",[1216,7110,1244],{"class":1230},[1216,7112,6890],{"class":1226},[1216,7114,772],{"class":1230},[1216,7116,6648],{"class":3025},[1216,7118,3451],{"class":1230},[1216,7120,7121,7124,7126,7129],{"class":1218,"line":3778},[1216,7122,7123],{"class":1234},"            signers",[1216,7125,1244],{"class":1230},[1216,7127,7128],{"class":1226},"signers",[1216,7130,3451],{"class":1230},[1216,7132,7134,7137,7139,7141,7143,7145],{"class":1218,"line":7133},66,[1216,7135,7136],{"class":1234},"            from_email",[1216,7138,1244],{"class":1230},[1216,7140,3064],{"class":1226},[1216,7142,772],{"class":1230},[1216,7144,2514],{"class":3025},[1216,7146,3451],{"class":1230},[1216,7148,7150],{"class":1218,"line":7149},67,[1216,7151,4042],{"class":1230},[1216,7153,7155],{"class":1218,"line":7154},68,[1216,7156,1299],{"emptyLinePlaceholder":888},[1216,7158,7160,7163,7165,7167,7169,7171,7173,7175,7177],{"class":1218,"line":7159},69,[1216,7161,7162],{"class":1260},"        signable",[1216,7164,772],{"class":1230},[1216,7166,6742],{"class":3025},[1216,7168,3056],{"class":1230},[1216,7170,5502],{"class":1260},[1216,7172,772],{"class":1230},[1216,7174,5942],{"class":3025},[1216,7176,772],{"class":1230},[1216,7178,7179],{"class":3025},"INVITE_SENT\n",[1216,7181,7183,7185,7187,7190,7192,7194,7196,7198],{"class":1218,"line":7182},70,[1216,7184,7162],{"class":1260},[1216,7186,772],{"class":1230},[1216,7188,7189],{"class":3025},"invite_sent_at",[1216,7191,3056],{"class":1230},[1216,7193,6769],{"class":1260},[1216,7195,772],{"class":1230},[1216,7197,6774],{"class":1226},[1216,7199,3633],{"class":1230},[1216,7201,7203,7205,7207,7209,7211,7213,7215,7217,7219,7221,7223,7225,7227,7229],{"class":1218,"line":7202},71,[1216,7204,7162],{"class":1260},[1216,7206,772],{"class":1230},[1216,7208,6785],{"class":1226},[1216,7210,1231],{"class":1230},[1216,7212,6790],{"class":1234},[1216,7214,6793],{"class":1230},[1216,7216,1318],{"class":1230},[1216,7218,6742],{"class":1321},[1216,7220,1318],{"class":1230},[1216,7222,1238],{"class":1230},[1216,7224,2855],{"class":1230},[1216,7226,7189],{"class":1321},[1216,7228,1318],{"class":1230},[1216,7230,1362],{"class":1230},[1216,7232,7234],{"class":1218,"line":7233},72,[1216,7235,1299],{"emptyLinePlaceholder":888},[1216,7237,7239],{"class":1218,"line":7238},73,[1216,7240,7241],{"class":1903},"        # 6. Register webhook for document completion\n",[1216,7243,7245,7248,7250,7253,7255],{"class":1218,"line":7244},74,[1216,7246,7247],{"class":1260},"        callback_url ",[1216,7249,1244],{"class":1230},[1216,7251,7252],{"class":1260}," settings",[1216,7254,772],{"class":1230},[1216,7256,7257],{"class":3025},"SIGNNOW_WEBHOOK_CALLBACK_URL\n",[1216,7259,7261,7263,7265],{"class":1218,"line":7260},75,[1216,7262,1305],{"class":1256},[1216,7264,5036],{"class":1260},[1216,7266,1270],{"class":1230},[1216,7268,7270,7273,7275,7278],{"class":1218,"line":7269},76,[1216,7271,7272],{"class":1260},"            SignNowService",[1216,7274,772],{"class":1230},[1216,7276,7277],{"class":1226},"register_webhook",[1216,7279,3425],{"class":1230},[1216,7281,7283,7286,7288,7290,7293,7295],{"class":1218,"line":7282},77,[1216,7284,7285],{"class":1234},"                event",[1216,7287,1244],{"class":1230},[1216,7289,1318],{"class":1230},[1216,7291,7292],{"class":1321},"document.complete",[1216,7294,1318],{"class":1230},[1216,7296,3451],{"class":1230},[1216,7298,7300,7303,7305,7307,7309,7311],{"class":1218,"line":7299},78,[1216,7301,7302],{"class":1234},"                entity_id",[1216,7304,1244],{"class":1230},[1216,7306,6890],{"class":1226},[1216,7308,772],{"class":1230},[1216,7310,6648],{"class":3025},[1216,7312,3451],{"class":1230},[1216,7314,7316,7319,7321,7324],{"class":1218,"line":7315},79,[1216,7317,7318],{"class":1234},"                callback_url",[1216,7320,1244],{"class":1230},[1216,7322,7323],{"class":1226},"callback_url",[1216,7325,3451],{"class":1230},[1216,7327,7329],{"class":1218,"line":7328},80,[1216,7330,3619],{"class":1230},[1216,7332,7334],{"class":1218,"line":7333},81,[1216,7335,1299],{"emptyLinePlaceholder":888},[1216,7337,7339,7342,7345,7347,7350],{"class":1218,"line":7338},82,[1216,7340,7341],{"class":1256},"    except",[1216,7343,7344],{"class":1873}," Exception",[1216,7346,3402],{"class":1256},[1216,7348,7349],{"class":1260}," exc",[1216,7351,1270],{"class":1230},[1216,7353,7355,7357,7360,7362,7365,7367,7370,7373,7375,7377,7380],{"class":1218,"line":7354},83,[1216,7356,1305],{"class":1256},[1216,7358,7359],{"class":1260}," self",[1216,7361,772],{"class":1230},[1216,7363,7364],{"class":3025},"request",[1216,7366,772],{"class":1230},[1216,7368,7369],{"class":3025},"retries",[1216,7371,7372],{"class":1230}," >=",[1216,7374,7359],{"class":1260},[1216,7376,772],{"class":1230},[1216,7378,7379],{"class":3025},"max_retries",[1216,7381,1270],{"class":1230},[1216,7383,7385,7387,7389,7391,7393,7395,7397,7399,7401],{"class":1218,"line":7384},84,[1216,7386,6715],{"class":1260},[1216,7388,772],{"class":1230},[1216,7390,6742],{"class":3025},[1216,7392,3056],{"class":1230},[1216,7394,5502],{"class":1260},[1216,7396,772],{"class":1230},[1216,7398,5942],{"class":3025},[1216,7400,772],{"class":1230},[1216,7402,7403],{"class":3025},"FAILED\n",[1216,7405,7407,7409,7411,7414,7416,7418,7420,7423],{"class":1218,"line":7406},85,[1216,7408,6715],{"class":1260},[1216,7410,772],{"class":1230},[1216,7412,7413],{"class":3025},"error_message",[1216,7415,3056],{"class":1230},[1216,7417,3312],{"class":1873},[1216,7419,1231],{"class":1230},[1216,7421,7422],{"class":1226},"exc",[1216,7424,1293],{"class":1230},[1216,7426,7428,7430,7432,7434,7436,7438,7440,7442,7444,7446,7448,7450,7452,7454],{"class":1218,"line":7427},86,[1216,7429,6715],{"class":1260},[1216,7431,772],{"class":1230},[1216,7433,6785],{"class":1226},[1216,7435,1231],{"class":1230},[1216,7437,6790],{"class":1234},[1216,7439,6793],{"class":1230},[1216,7441,1318],{"class":1230},[1216,7443,6742],{"class":1321},[1216,7445,1318],{"class":1230},[1216,7447,1238],{"class":1230},[1216,7449,2855],{"class":1230},[1216,7451,7413],{"class":1321},[1216,7453,1318],{"class":1230},[1216,7455,1362],{"class":1230},[1216,7457,7459],{"class":1218,"line":7458},87,[1216,7460,7461],{"class":1256},"            return\n",[1216,7463,7465,7468,7470,7473,7475,7477,7479,7481],{"class":1218,"line":7464},88,[1216,7466,7467],{"class":1260},"        self",[1216,7469,772],{"class":1230},[1216,7471,7472],{"class":1226},"retry",[1216,7474,1231],{"class":1230},[1216,7476,7422],{"class":1234},[1216,7478,1244],{"class":1230},[1216,7480,7422],{"class":1226},[1216,7482,1293],{"class":1230},[7484,7485,7486],"note",{},[746,7487,1513,7488,7490,7491,2107,7493,2107,7495,2107,7497,7499,7500,7503],{},[1047,7489,6856],{}," coordinates (",[1047,7492,6217],{},[1047,7494,6233],{},[1047,7496,6249],{},[1047,7498,6265],{},") define where the signature box appears on the PDF. You'll need to calibrate these values for your specific document layout. SignNow uses a coordinate system where ",[1047,7501,7502],{},"(0,0)"," is the top-left corner of the page.",[750,7505,7507],{"id":7506},"step-5-handle-webhooks","Step 5: Handle webhooks",[746,7509,7510],{},"When the signer completes the document, SignNow sends a POST request to your webhook URL. We verify the payload signature and dispatch a download task:",[1208,7512,7515],{"className":1210,"code":7513,"filename":7514,"language":1212,"meta":873,"style":873},"import hashlib\nimport hmac\n\nfrom rest_framework.permissions import AllowAny\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\n\nclass SignNowWebhookView(APIView):\n    \"\"\"Receives webhook callbacks from SignNow when documents are signed.\"\"\"\n\n    permission_classes = [AllowAny]\n    authentication_classes = []\n\n    def post(self, request):\n        # Verify webhook signature\n        webhook_secret = getattr(settings, \"SIGNNOW_WEBHOOK_SECRET\", \"\")\n        if webhook_secret:\n            received_signature = request.headers.get(\"X-SignNow-Signature\", \"\")\n            expected = hmac.new(\n                webhook_secret.encode(\"utf-8\"),\n                request.body,\n                hashlib.sha256,\n            ).hexdigest()\n\n            if not hmac.compare_digest(expected, received_signature):\n                return Response({\"status\": \"invalid_signature\"}, status=200)\n\n        # Parse event\n        event = request.data.get(\"event\", \"\")\n        meta = request.data.get(\"meta\", {})\n        document_id = meta.get(\"document_id\", \"\")\n\n        if event in (\"document.complete\", \"document.update\"):\n            signable = SignableDocument.objects.get(\n                signnow_document_id=document_id,\n                status__in=[\"uploaded\", \"invite_sent\"],\n            )\n            signable.status = SignableDocument.Status.SIGNED\n            signable.signed_at = timezone.now()\n            signable.save(update_fields=[\"status\", \"signed_at\"])\n\n            # Download the signed PDF asynchronously\n            download_signed_pdf.delay(signable.id)\n\n        # Always return 200 to acknowledge receipt\n        return Response({\"status\": \"ok\"}, status=200)\n","apps/signnow/api/views.py",[1047,7516,7517,7524,7531,7535,7552,7568,7584,7588,7592,7606,7615,7619,7634,7644,7648,7666,7671,7698,7706,7739,7756,7777,7788,7800,7810,7814,7839,7878,7882,7887,7919,7950,7978,7982,8010,8029,8040,8063,8067,8088,8107,8137,8141,8146,8166,8170,8175],{"__ignoreMap":873},[1216,7518,7519,7521],{"class":1218,"line":1219},[1216,7520,2799],{"class":1256},[1216,7522,7523],{"class":1260}," hashlib\n",[1216,7525,7526,7528],{"class":1218,"line":874},[1216,7527,2799],{"class":1256},[1216,7529,7530],{"class":1260}," hmac\n",[1216,7532,7533],{"class":1218,"line":1273},[1216,7534,1299],{"emptyLinePlaceholder":888},[1216,7536,7537,7539,7542,7544,7547,7549],{"class":1218,"line":1296},[1216,7538,2807],{"class":1256},[1216,7540,7541],{"class":1260}," rest_framework",[1216,7543,772],{"class":1230},[1216,7545,7546],{"class":1260},"permissions ",[1216,7548,2799],{"class":1256},[1216,7550,7551],{"class":1260}," AllowAny\n",[1216,7553,7554,7556,7558,7560,7563,7565],{"class":1218,"line":1302},[1216,7555,2807],{"class":1256},[1216,7557,7541],{"class":1260},[1216,7559,772],{"class":1230},[1216,7561,7562],{"class":1260},"response ",[1216,7564,2799],{"class":1256},[1216,7566,7567],{"class":1260}," Response\n",[1216,7569,7570,7572,7574,7576,7579,7581],{"class":1218,"line":1332},[1216,7571,2807],{"class":1256},[1216,7573,7541],{"class":1260},[1216,7575,772],{"class":1230},[1216,7577,7578],{"class":1260},"views ",[1216,7580,2799],{"class":1256},[1216,7582,7583],{"class":1260}," APIView\n",[1216,7585,7586],{"class":1218,"line":1365},[1216,7587,1299],{"emptyLinePlaceholder":888},[1216,7589,7590],{"class":1218,"line":2881},[1216,7591,1299],{"emptyLinePlaceholder":888},[1216,7593,7594,7596,7599,7601,7604],{"class":1218,"line":2886},[1216,7595,2889],{"class":1222},[1216,7597,7598],{"class":1873}," SignNowWebhookView",[1216,7600,1231],{"class":1230},[1216,7602,7603],{"class":1873},"APIView",[1216,7605,1251],{"class":1230},[1216,7607,7608,7610,7613],{"class":1218,"line":2897},[1216,7609,2900],{"class":1256},[1216,7611,7612],{"class":1903},"Receives webhook callbacks from SignNow when documents are signed.",[1216,7614,2906],{"class":1256},[1216,7616,7617],{"class":1218,"line":2909},[1216,7618,1299],{"emptyLinePlaceholder":888},[1216,7620,7621,7624,7626,7629,7632],{"class":1218,"line":2914},[1216,7622,7623],{"class":1260},"    permission_classes ",[1216,7625,1244],{"class":1230},[1216,7627,7628],{"class":1230}," [",[1216,7630,7631],{"class":1260},"AllowAny",[1216,7633,3679],{"class":1230},[1216,7635,7636,7639,7641],{"class":1218,"line":2925},[1216,7637,7638],{"class":1260},"    authentication_classes ",[1216,7640,1244],{"class":1230},[1216,7642,7643],{"class":1230}," []\n",[1216,7645,7646],{"class":1218,"line":2936},[1216,7647,1299],{"emptyLinePlaceholder":888},[1216,7649,7650,7652,7655,7657,7659,7661,7664],{"class":1218,"line":2946},[1216,7651,2983],{"class":1222},[1216,7653,7654],{"class":1226}," post",[1216,7656,1231],{"class":1230},[1216,7658,6421],{"class":6420},[1216,7660,1238],{"class":1230},[1216,7662,7663],{"class":1234}," request",[1216,7665,1251],{"class":1230},[1216,7667,7668],{"class":1218,"line":2956},[1216,7669,7670],{"class":1903},"        # Verify webhook signature\n",[1216,7672,7673,7676,7678,7680,7682,7684,7686,7688,7690,7692,7694,7696],{"class":1218,"line":2966},[1216,7674,7675],{"class":1260},"        webhook_secret ",[1216,7677,1244],{"class":1230},[1216,7679,3059],{"class":1226},[1216,7681,1231],{"class":1230},[1216,7683,3064],{"class":1226},[1216,7685,1238],{"class":1230},[1216,7687,2855],{"class":1230},[1216,7689,2534],{"class":1321},[1216,7691,1318],{"class":1230},[1216,7693,1238],{"class":1230},[1216,7695,3077],{"class":1230},[1216,7697,1293],{"class":1230},[1216,7699,7700,7702,7704],{"class":1218,"line":2971},[1216,7701,1305],{"class":1256},[1216,7703,5241],{"class":1260},[1216,7705,1270],{"class":1230},[1216,7707,7708,7711,7713,7715,7717,7720,7722,7724,7726,7728,7731,7733,7735,7737],{"class":1218,"line":2980},[1216,7709,7710],{"class":1260},"            received_signature ",[1216,7712,1244],{"class":1230},[1216,7714,7663],{"class":1260},[1216,7716,772],{"class":1230},[1216,7718,7719],{"class":3025},"headers",[1216,7721,772],{"class":1230},[1216,7723,1313],{"class":1226},[1216,7725,1231],{"class":1230},[1216,7727,1318],{"class":1230},[1216,7729,7730],{"class":1321},"X-SignNow-Signature",[1216,7732,1318],{"class":1230},[1216,7734,1238],{"class":1230},[1216,7736,3077],{"class":1230},[1216,7738,1293],{"class":1230},[1216,7740,7741,7744,7746,7749,7751,7754],{"class":1218,"line":3004},[1216,7742,7743],{"class":1260},"            expected ",[1216,7745,1244],{"class":1230},[1216,7747,7748],{"class":1260}," hmac",[1216,7750,772],{"class":1230},[1216,7752,7753],{"class":1226},"new",[1216,7755,3425],{"class":1230},[1216,7757,7758,7761,7763,7766,7768,7770,7773,7775],{"class":1218,"line":3015},[1216,7759,7760],{"class":1226},"                webhook_secret",[1216,7762,772],{"class":1230},[1216,7764,7765],{"class":1226},"encode",[1216,7767,1231],{"class":1230},[1216,7769,1318],{"class":1230},[1216,7771,7772],{"class":1321},"utf-8",[1216,7774,1318],{"class":1230},[1216,7776,3998],{"class":1230},[1216,7778,7779,7782,7784,7786],{"class":1218,"line":3031},[1216,7780,7781],{"class":1226},"                request",[1216,7783,772],{"class":1230},[1216,7785,2123],{"class":3025},[1216,7787,3451],{"class":1230},[1216,7789,7790,7793,7795,7798],{"class":1218,"line":3040},[1216,7791,7792],{"class":1226},"                hashlib",[1216,7794,772],{"class":1230},[1216,7796,7797],{"class":3025},"sha256",[1216,7799,3451],{"class":1230},[1216,7801,7802,7805,7808],{"class":1218,"line":3045},[1216,7803,7804],{"class":1230},"            ).",[1216,7806,7807],{"class":1226},"hexdigest",[1216,7809,3633],{"class":1230},[1216,7811,7812],{"class":1218,"line":3082},[1216,7813,1299],{"emptyLinePlaceholder":888},[1216,7815,7816,7818,7820,7822,7824,7827,7829,7832,7834,7837],{"class":1218,"line":3114},[1216,7817,6680],{"class":1256},[1216,7819,3188],{"class":1230},[1216,7821,7748],{"class":1260},[1216,7823,772],{"class":1230},[1216,7825,7826],{"class":1226},"compare_digest",[1216,7828,1231],{"class":1230},[1216,7830,7831],{"class":1226},"expected",[1216,7833,1238],{"class":1230},[1216,7835,7836],{"class":1226}," received_signature",[1216,7838,1251],{"class":1230},[1216,7840,7841,7844,7847,7850,7852,7854,7856,7858,7860,7863,7865,7868,7871,7873,7876],{"class":1218,"line":3146},[1216,7842,7843],{"class":1256},"                return",[1216,7845,7846],{"class":1226}," Response",[1216,7848,7849],{"class":1230},"({",[1216,7851,1318],{"class":1230},[1216,7853,6742],{"class":1321},[1216,7855,1318],{"class":1230},[1216,7857,1927],{"class":1230},[1216,7859,2855],{"class":1230},[1216,7861,7862],{"class":1321},"invalid_signature",[1216,7864,1318],{"class":1230},[1216,7866,7867],{"class":1230},"},",[1216,7869,7870],{"class":1234}," status",[1216,7872,1244],{"class":1230},[1216,7874,7875],{"class":1247},"200",[1216,7877,1293],{"class":1230},[1216,7879,7880],{"class":1218,"line":3178},[1216,7881,1299],{"emptyLinePlaceholder":888},[1216,7883,7884],{"class":1218,"line":3183},[1216,7885,7886],{"class":1903},"        # Parse event\n",[1216,7888,7889,7892,7894,7896,7898,7901,7903,7905,7907,7909,7911,7913,7915,7917],{"class":1218,"line":3230},[1216,7890,7891],{"class":1260},"        event ",[1216,7893,1244],{"class":1230},[1216,7895,7663],{"class":1260},[1216,7897,772],{"class":1230},[1216,7899,7900],{"class":3025},"data",[1216,7902,772],{"class":1230},[1216,7904,1313],{"class":1226},[1216,7906,1231],{"class":1230},[1216,7908,1318],{"class":1230},[1216,7910,5108],{"class":1321},[1216,7912,1318],{"class":1230},[1216,7914,1238],{"class":1230},[1216,7916,3077],{"class":1230},[1216,7918,1293],{"class":1230},[1216,7920,7921,7924,7926,7928,7930,7932,7934,7936,7938,7940,7943,7945,7947],{"class":1218,"line":3251},[1216,7922,7923],{"class":1260},"        meta ",[1216,7925,1244],{"class":1230},[1216,7927,7663],{"class":1260},[1216,7929,772],{"class":1230},[1216,7931,7900],{"class":3025},[1216,7933,772],{"class":1230},[1216,7935,1313],{"class":1226},[1216,7937,1231],{"class":1230},[1216,7939,1318],{"class":1230},[1216,7941,7942],{"class":1321},"meta",[1216,7944,1318],{"class":1230},[1216,7946,1238],{"class":1230},[1216,7948,7949],{"class":1230}," {})\n",[1216,7951,7952,7955,7957,7960,7962,7964,7966,7968,7970,7972,7974,7976],{"class":1218,"line":3258},[1216,7953,7954],{"class":1260},"        document_id ",[1216,7956,1244],{"class":1230},[1216,7958,7959],{"class":1260}," meta",[1216,7961,772],{"class":1230},[1216,7963,1313],{"class":1226},[1216,7965,1231],{"class":1230},[1216,7967,1318],{"class":1230},[1216,7969,4256],{"class":1321},[1216,7971,1318],{"class":1230},[1216,7973,1238],{"class":1230},[1216,7975,3077],{"class":1230},[1216,7977,1293],{"class":1230},[1216,7979,7980],{"class":1218,"line":3263},[1216,7981,1299],{"emptyLinePlaceholder":888},[1216,7983,7984,7986,7989,7991,7993,7995,7997,7999,8001,8003,8006,8008],{"class":1218,"line":3276},[1216,7985,1305],{"class":1256},[1216,7987,7988],{"class":1260}," event ",[1216,7990,1264],{"class":1230},[1216,7992,4018],{"class":1230},[1216,7994,1318],{"class":1230},[1216,7996,7292],{"class":1321},[1216,7998,1318],{"class":1230},[1216,8000,1238],{"class":1230},[1216,8002,2855],{"class":1230},[1216,8004,8005],{"class":1321},"document.update",[1216,8007,1318],{"class":1230},[1216,8009,1251],{"class":1230},[1216,8011,8012,8015,8017,8019,8021,8023,8025,8027],{"class":1218,"line":3284},[1216,8013,8014],{"class":1260},"            signable ",[1216,8016,1244],{"class":1230},[1216,8018,5502],{"class":1260},[1216,8020,772],{"class":1230},[1216,8022,6498],{"class":3025},[1216,8024,772],{"class":1230},[1216,8026,1313],{"class":1226},[1216,8028,3425],{"class":1230},[1216,8030,8031,8034,8036,8038],{"class":1218,"line":3289},[1216,8032,8033],{"class":1234},"                signnow_document_id",[1216,8035,1244],{"class":1230},[1216,8037,4256],{"class":1226},[1216,8039,3451],{"class":1230},[1216,8041,8042,8045,8047,8049,8051,8053,8055,8057,8059,8061],{"class":1218,"line":3296},[1216,8043,8044],{"class":1234},"                status__in",[1216,8046,6793],{"class":1230},[1216,8048,1318],{"class":1230},[1216,8050,5581],{"class":1321},[1216,8052,1318],{"class":1230},[1216,8054,1238],{"class":1230},[1216,8056,2855],{"class":1230},[1216,8058,5604],{"class":1321},[1216,8060,1318],{"class":1230},[1216,8062,4637],{"class":1230},[1216,8064,8065],{"class":1218,"line":3321},[1216,8066,3619],{"class":1230},[1216,8068,8069,8071,8073,8075,8077,8079,8081,8083,8085],{"class":1218,"line":3331},[1216,8070,6715],{"class":1260},[1216,8072,772],{"class":1230},[1216,8074,6742],{"class":3025},[1216,8076,3056],{"class":1230},[1216,8078,5502],{"class":1260},[1216,8080,772],{"class":1230},[1216,8082,5942],{"class":3025},[1216,8084,772],{"class":1230},[1216,8086,8087],{"class":3025},"SIGNED\n",[1216,8089,8090,8092,8094,8097,8099,8101,8103,8105],{"class":1218,"line":3353},[1216,8091,6715],{"class":1260},[1216,8093,772],{"class":1230},[1216,8095,8096],{"class":3025},"signed_at",[1216,8098,3056],{"class":1230},[1216,8100,6769],{"class":1260},[1216,8102,772],{"class":1230},[1216,8104,6774],{"class":1226},[1216,8106,3633],{"class":1230},[1216,8108,8109,8111,8113,8115,8117,8119,8121,8123,8125,8127,8129,8131,8133,8135],{"class":1218,"line":3363},[1216,8110,6715],{"class":1260},[1216,8112,772],{"class":1230},[1216,8114,6785],{"class":1226},[1216,8116,1231],{"class":1230},[1216,8118,6790],{"class":1234},[1216,8120,6793],{"class":1230},[1216,8122,1318],{"class":1230},[1216,8124,6742],{"class":1321},[1216,8126,1318],{"class":1230},[1216,8128,1238],{"class":1230},[1216,8130,2855],{"class":1230},[1216,8132,8096],{"class":1321},[1216,8134,1318],{"class":1230},[1216,8136,1362],{"class":1230},[1216,8138,8139],{"class":1218,"line":3371},[1216,8140,1299],{"emptyLinePlaceholder":888},[1216,8142,8143],{"class":1218,"line":3376},[1216,8144,8145],{"class":1903},"            # Download the signed PDF asynchronously\n",[1216,8147,8148,8151,8153,8156,8158,8160,8162,8164],{"class":1218,"line":3410},[1216,8149,8150],{"class":1260},"            download_signed_pdf",[1216,8152,772],{"class":1230},[1216,8154,8155],{"class":1226},"delay",[1216,8157,1231],{"class":1230},[1216,8159,6890],{"class":1226},[1216,8161,772],{"class":1230},[1216,8163,1357],{"class":3025},[1216,8165,1293],{"class":1230},[1216,8167,8168],{"class":1218,"line":3428},[1216,8169,1299],{"emptyLinePlaceholder":888},[1216,8171,8172],{"class":1218,"line":3454},[1216,8173,8174],{"class":1903},"        # Always return 200 to acknowledge receipt\n",[1216,8176,8177,8179,8181,8183,8185,8187,8189,8191,8193,8196,8198,8200,8202,8204,8206],{"class":1218,"line":3463},[1216,8178,3279],{"class":1256},[1216,8180,7846],{"class":1226},[1216,8182,7849],{"class":1230},[1216,8184,1318],{"class":1230},[1216,8186,6742],{"class":1321},[1216,8188,1318],{"class":1230},[1216,8190,1927],{"class":1230},[1216,8192,2855],{"class":1230},[1216,8194,8195],{"class":1321},"ok",[1216,8197,1318],{"class":1230},[1216,8199,7867],{"class":1230},[1216,8201,7870],{"class":1234},[1216,8203,1244],{"class":1230},[1216,8205,7875],{"class":1247},[1216,8207,1293],{"class":1230},[844,8209,8210],{},[746,8211,8212],{},"Always return HTTP 200 from your webhook endpoint, even when processing fails. SignNow will retry delivery on non-2xx responses, which can lead to duplicate processing if your error is in business logic rather than network issues.",[746,8214,8215],{},"Wire it up in your URL configuration:",[1208,8217,8220],{"className":1210,"code":8218,"filename":8219,"language":1212,"meta":873,"style":873},"from django.urls import path\nfrom signnow.api.views import SignNowWebhookView\n\nurlpatterns = [\n    path(\"webhooks/\", SignNowWebhookView.as_view(), name=\"signnow-webhook\"),\n]\n","apps/signnow/api/urls.py",[1047,8221,8222,8238,8258,8262,8272,8312],{"__ignoreMap":873},[1216,8223,8224,8226,8228,8230,8233,8235],{"class":1218,"line":1219},[1216,8225,2807],{"class":1256},[1216,8227,2810],{"class":1260},[1216,8229,772],{"class":1230},[1216,8231,8232],{"class":1260},"urls ",[1216,8234,2799],{"class":1256},[1216,8236,8237],{"class":1260}," path\n",[1216,8239,8240,8242,8244,8246,8249,8251,8253,8255],{"class":1218,"line":874},[1216,8241,2807],{"class":1256},[1216,8243,6450],{"class":1260},[1216,8245,772],{"class":1230},[1216,8247,8248],{"class":1260},"api",[1216,8250,772],{"class":1230},[1216,8252,7578],{"class":1260},[1216,8254,2799],{"class":1256},[1216,8256,8257],{"class":1260}," SignNowWebhookView\n",[1216,8259,8260],{"class":1218,"line":1273},[1216,8261,1299],{"emptyLinePlaceholder":888},[1216,8263,8264,8267,8269],{"class":1218,"line":1296},[1216,8265,8266],{"class":1260},"urlpatterns ",[1216,8268,1244],{"class":1230},[1216,8270,8271],{"class":1230}," [\n",[1216,8273,8274,8277,8279,8281,8284,8286,8288,8290,8292,8295,8298,8301,8303,8305,8308,8310],{"class":1218,"line":1302},[1216,8275,8276],{"class":1226},"    path",[1216,8278,1231],{"class":1230},[1216,8280,1318],{"class":1230},[1216,8282,8283],{"class":1321},"webhooks/",[1216,8285,1318],{"class":1230},[1216,8287,1238],{"class":1230},[1216,8289,7598],{"class":1226},[1216,8291,772],{"class":1230},[1216,8293,8294],{"class":1226},"as_view",[1216,8296,8297],{"class":1230},"(),",[1216,8299,8300],{"class":1234}," name",[1216,8302,1244],{"class":1230},[1216,8304,1318],{"class":1230},[1216,8306,8307],{"class":1321},"signnow-webhook",[1216,8309,1318],{"class":1230},[1216,8311,3998],{"class":1230},[1216,8313,8314],{"class":1218,"line":1332},[1216,8315,3679],{"class":1230},[746,8317,8318],{},"And include it in your main API URLs:",[1208,8320,8323],{"className":1210,"code":8321,"filename":8322,"language":1212,"meta":873,"style":873},"urlpatterns = [\n    # ... other routes\n    path(\"v1/signnow/\", include(\"signnow.api.urls\")),\n]\n","apps/api/urls.py",[1047,8324,8325,8333,8338,8368],{"__ignoreMap":873},[1216,8326,8327,8329,8331],{"class":1218,"line":1219},[1216,8328,8266],{"class":1260},[1216,8330,1244],{"class":1230},[1216,8332,8271],{"class":1230},[1216,8334,8335],{"class":1218,"line":874},[1216,8336,8337],{"class":1903},"    # ... other routes\n",[1216,8339,8340,8342,8344,8346,8349,8351,8353,8356,8358,8360,8363,8365],{"class":1218,"line":1273},[1216,8341,8276],{"class":1226},[1216,8343,1231],{"class":1230},[1216,8345,1318],{"class":1230},[1216,8347,8348],{"class":1321},"v1/signnow/",[1216,8350,1318],{"class":1230},[1216,8352,1238],{"class":1230},[1216,8354,8355],{"class":1226}," include",[1216,8357,1231],{"class":1230},[1216,8359,1318],{"class":1230},[1216,8361,8362],{"class":1321},"signnow.api.urls",[1216,8364,1318],{"class":1230},[1216,8366,8367],{"class":1230},")),\n",[1216,8369,8370],{"class":1218,"line":1296},[1216,8371,3679],{"class":1230},[750,8373,8375],{"id":8374},"step-6-download-the-signed-pdf","Step 6: Download the signed PDF",[746,8377,8378],{},"The second Celery task fetches the completed, signed document from SignNow and saves it locally:",[1208,8380,8382],{"className":1210,"code":8381,"filename":6177,"language":1212,"meta":873,"style":873},"@shared_task(bind=True, max_retries=5, default_retry_delay=120)\ndef download_signed_pdf(self, signable_document_id: int):\n    \"\"\"Download the signed PDF from SignNow and save it locally.\"\"\"\n    from signnow.models import SignableDocument\n    from signnow.services.signnow_service import SignNowService\n\n    signable = SignableDocument.objects.get(id=signable_document_id)\n\n    # Skip if already downloaded (idempotent)\n    if signable.status == SignableDocument.Status.DOWNLOADED:\n        return\n\n    pdf_bytes = SignNowService.download_signed_document(signable.signnow_document_id)\n    if not pdf_bytes:\n        raise RuntimeError(\"Failed to download signed document\")\n\n    signed_filename = f\"signed-{signable.original_pdf_name.split('/')[-1]}\"\n    signable.signed_pdf.save(signed_filename, ContentFile(pdf_bytes), save=False)\n    signable.status = SignableDocument.Status.DOWNLOADED\n    signable.downloaded_at = timezone.now()\n    signable.save(update_fields=[\"signed_pdf\", \"status\", \"downloaded_at\"])\n",[1047,8383,8384,8414,8435,8444,8458,8476,8480,8507,8511,8516,8541,8546,8550,8574,8584,8602,8606,8650,8686,8707,8726],{"__ignoreMap":873},[1216,8385,8386,8388,8390,8392,8394,8396,8398,8400,8403,8405,8407,8409,8412],{"class":1218,"line":1219},[1216,8387,3821],{"class":1230},[1216,8389,6381],{"class":1226},[1216,8391,1231],{"class":1230},[1216,8393,6386],{"class":1234},[1216,8395,5815],{"class":1230},[1216,8397,6391],{"class":1234},[1216,8399,1244],{"class":1230},[1216,8401,8402],{"class":1247},"5",[1216,8404,1238],{"class":1230},[1216,8406,6401],{"class":1234},[1216,8408,1244],{"class":1230},[1216,8410,8411],{"class":1247},"120",[1216,8413,1293],{"class":1230},[1216,8415,8416,8418,8421,8423,8425,8427,8429,8431,8433],{"class":1218,"line":874},[1216,8417,1223],{"class":1222},[1216,8419,8420],{"class":1226}," download_signed_pdf",[1216,8422,1231],{"class":1230},[1216,8424,6421],{"class":6420},[1216,8426,1238],{"class":1230},[1216,8428,6426],{"class":1234},[1216,8430,1927],{"class":1230},[1216,8432,6431],{"class":1873},[1216,8434,1251],{"class":1230},[1216,8436,8437,8439,8442],{"class":1218,"line":1273},[1216,8438,2900],{"class":1256},[1216,8440,8441],{"class":1903},"Download the signed PDF from SignNow and save it locally.",[1216,8443,2906],{"class":1256},[1216,8445,8446,8448,8450,8452,8454,8456],{"class":1218,"line":1296},[1216,8447,6447],{"class":1256},[1216,8449,6450],{"class":1260},[1216,8451,772],{"class":1230},[1216,8453,5466],{"class":1260},[1216,8455,2799],{"class":1256},[1216,8457,6459],{"class":1260},[1216,8459,8460,8462,8464,8466,8468,8470,8472,8474],{"class":1218,"line":1302},[1216,8461,6447],{"class":1256},[1216,8463,6450],{"class":1260},[1216,8465,772],{"class":1230},[1216,8467,6470],{"class":1260},[1216,8469,772],{"class":1230},[1216,8471,6475],{"class":1260},[1216,8473,2799],{"class":1256},[1216,8475,6480],{"class":1260},[1216,8477,8478],{"class":1218,"line":1332},[1216,8479,1299],{"emptyLinePlaceholder":888},[1216,8481,8482,8484,8486,8488,8490,8492,8494,8496,8498,8500,8502,8505],{"class":1218,"line":1365},[1216,8483,6489],{"class":1260},[1216,8485,1244],{"class":1230},[1216,8487,5502],{"class":1260},[1216,8489,772],{"class":1230},[1216,8491,6498],{"class":3025},[1216,8493,772],{"class":1230},[1216,8495,1313],{"class":1226},[1216,8497,1231],{"class":1230},[1216,8499,1357],{"class":1234},[1216,8501,1244],{"class":1230},[1216,8503,8504],{"class":1226},"signable_document_id",[1216,8506,1293],{"class":1230},[1216,8508,8509],{"class":1218,"line":2881},[1216,8510,1299],{"emptyLinePlaceholder":888},[1216,8512,8513],{"class":1218,"line":2886},[1216,8514,8515],{"class":1903},"    # Skip if already downloaded (idempotent)\n",[1216,8517,8518,8520,8522,8524,8526,8528,8530,8532,8534,8536,8539],{"class":1218,"line":2897},[1216,8519,3894],{"class":1256},[1216,8521,6559],{"class":1260},[1216,8523,772],{"class":1230},[1216,8525,6742],{"class":3025},[1216,8527,6973],{"class":1230},[1216,8529,5502],{"class":1260},[1216,8531,772],{"class":1230},[1216,8533,5942],{"class":3025},[1216,8535,772],{"class":1230},[1216,8537,8538],{"class":3025},"DOWNLOADED",[1216,8540,1270],{"class":1230},[1216,8542,8543],{"class":1218,"line":2909},[1216,8544,8545],{"class":1256},"        return\n",[1216,8547,8548],{"class":1218,"line":2914},[1216,8549,1299],{"emptyLinePlaceholder":888},[1216,8551,8552,8555,8557,8559,8561,8564,8566,8568,8570,8572],{"class":1218,"line":2925},[1216,8553,8554],{"class":1260},"    pdf_bytes ",[1216,8556,1244],{"class":1230},[1216,8558,2892],{"class":1260},[1216,8560,772],{"class":1230},[1216,8562,8563],{"class":1226},"download_signed_document",[1216,8565,1231],{"class":1230},[1216,8567,6890],{"class":1226},[1216,8569,772],{"class":1230},[1216,8571,6648],{"class":3025},[1216,8573,1293],{"class":1230},[1216,8575,8576,8578,8580,8582],{"class":1218,"line":2936},[1216,8577,3894],{"class":1256},[1216,8579,3188],{"class":1230},[1216,8581,3839],{"class":1260},[1216,8583,1270],{"class":1230},[1216,8585,8586,8589,8591,8593,8595,8598,8600],{"class":1218,"line":2946},[1216,8587,8588],{"class":1256},"        raise",[1216,8590,6695],{"class":1873},[1216,8592,1231],{"class":1230},[1216,8594,1318],{"class":1230},[1216,8596,8597],{"class":1321},"Failed to download signed document",[1216,8599,1318],{"class":1230},[1216,8601,1293],{"class":1230},[1216,8603,8604],{"class":1218,"line":2956},[1216,8605,1299],{"emptyLinePlaceholder":888},[1216,8607,8608,8611,8613,8615,8618,8620,8622,8624,8627,8629,8631,8633,8636,8638,8640,8642,8644,8646,8648],{"class":1218,"line":2966},[1216,8609,8610],{"class":1260},"    signed_filename ",[1216,8612,1244],{"class":1230},[1216,8614,3570],{"class":1222},[1216,8616,8617],{"class":1321},"\"signed-",[1216,8619,3436],{"class":1247},[1216,8621,6890],{"class":1260},[1216,8623,772],{"class":1230},[1216,8625,8626],{"class":3025},"original_pdf_name",[1216,8628,772],{"class":1230},[1216,8630,6609],{"class":1226},[1216,8632,1231],{"class":1230},[1216,8634,8635],{"class":1230},"'",[1216,8637,6616],{"class":1321},[1216,8639,8635],{"class":1230},[1216,8641,6621],{"class":1230},[1216,8643,6624],{"class":1247},[1216,8645,4806],{"class":1230},[1216,8647,3445],{"class":1247},[1216,8649,2861],{"class":1321},[1216,8651,8652,8655,8657,8660,8662,8664,8666,8669,8671,8674,8676,8678,8680,8683],{"class":1218,"line":2971},[1216,8653,8654],{"class":1260},"    signable",[1216,8656,772],{"class":1230},[1216,8658,8659],{"class":3025},"signed_pdf",[1216,8661,772],{"class":1230},[1216,8663,6785],{"class":1226},[1216,8665,1231],{"class":1230},[1216,8667,8668],{"class":1226},"signed_filename",[1216,8670,1238],{"class":1230},[1216,8672,8673],{"class":1226}," ContentFile",[1216,8675,1231],{"class":1230},[1216,8677,6669],{"class":1226},[1216,8679,4282],{"class":1230},[1216,8681,8682],{"class":1234}," save",[1216,8684,8685],{"class":1230},"=False)\n",[1216,8687,8688,8690,8692,8694,8696,8698,8700,8702,8704],{"class":1218,"line":2980},[1216,8689,8654],{"class":1260},[1216,8691,772],{"class":1230},[1216,8693,6742],{"class":3025},[1216,8695,3056],{"class":1230},[1216,8697,5502],{"class":1260},[1216,8699,772],{"class":1230},[1216,8701,5942],{"class":3025},[1216,8703,772],{"class":1230},[1216,8705,8706],{"class":3025},"DOWNLOADED\n",[1216,8708,8709,8711,8713,8716,8718,8720,8722,8724],{"class":1218,"line":3004},[1216,8710,8654],{"class":1260},[1216,8712,772],{"class":1230},[1216,8714,8715],{"class":3025},"downloaded_at",[1216,8717,3056],{"class":1230},[1216,8719,6769],{"class":1260},[1216,8721,772],{"class":1230},[1216,8723,6774],{"class":1226},[1216,8725,3633],{"class":1230},[1216,8727,8728,8730,8732,8734,8736,8738,8740,8742,8744,8746,8748,8750,8752,8754,8756,8758,8760,8762],{"class":1218,"line":3015},[1216,8729,8654],{"class":1260},[1216,8731,772],{"class":1230},[1216,8733,6785],{"class":1226},[1216,8735,1231],{"class":1230},[1216,8737,6790],{"class":1234},[1216,8739,6793],{"class":1230},[1216,8741,1318],{"class":1230},[1216,8743,8659],{"class":1321},[1216,8745,1318],{"class":1230},[1216,8747,1238],{"class":1230},[1216,8749,2855],{"class":1230},[1216,8751,6742],{"class":1321},[1216,8753,1318],{"class":1230},[1216,8755,1238],{"class":1230},[1216,8757,2855],{"class":1230},[1216,8759,8715],{"class":1321},[1216,8761,1318],{"class":1230},[1216,8763,1362],{"class":1230},[750,8765,8767],{"id":8766},"step-7-admin-integration","Step 7: Admin integration",[746,8769,8770],{},"Finally, we added a read-only admin panel with color-coded status badges so our team can monitor signing progress at a glance:",[1208,8772,8775],{"className":1210,"code":8773,"filename":8774,"language":1212,"meta":873,"style":873},"from django.contrib import admin\nfrom django.utils.html import format_html\n\n\n@admin.register(SignableDocument)\nclass SignableDocumentAdmin(admin.ModelAdmin):\n    list_display = [\"signer_name\", \"signer_email\", \"status_badge\", \"created_at\", \"signed_at\"]\n    list_filter = [\"status\", \"created_at\"]\n    search_fields = [\"signer_email\", \"signer_name\", \"signnow_document_id\"]\n\n    @admin.display(description=\"Status\")\n    def status_badge(self, obj):\n        colors = {\n            \"pending\": \"#6b7280\",\n            \"uploaded\": \"#3b82f6\",\n            \"invite_sent\": \"#f59e0b\",\n            \"signed\": \"#22c55e\",\n            \"downloaded\": \"#059669\",\n            \"failed\": \"#ef4444\",\n        }\n        color = colors.get(obj.status, \"#6b7280\")\n        return format_html(\n            '\u003Cspan style=\"background-color: {}; color: white; padding: 2px 8px; '\n            'border-radius: 4px; font-size: 12px;\">{}\u003C/span>',\n            color,\n            obj.get_status_display(),\n        )\n","apps/signnow/admin.py",[1047,8776,8777,8793,8814,8818,8822,8840,8858,8910,8935,8968,8972,8998,9016,9025,9044,9063,9082,9101,9120,9139,9144,9177,9186,9203,9219,9226,9239],{"__ignoreMap":873},[1216,8778,8779,8781,8783,8785,8788,8790],{"class":1218,"line":1219},[1216,8780,2807],{"class":1256},[1216,8782,2810],{"class":1260},[1216,8784,772],{"class":1230},[1216,8786,8787],{"class":1260},"contrib ",[1216,8789,2799],{"class":1256},[1216,8791,8792],{"class":1260}," admin\n",[1216,8794,8795,8797,8799,8801,8804,8806,8809,8811],{"class":1218,"line":874},[1216,8796,2807],{"class":1256},[1216,8798,2810],{"class":1260},[1216,8800,772],{"class":1230},[1216,8802,8803],{"class":1260},"utils",[1216,8805,772],{"class":1230},[1216,8807,8808],{"class":1260},"html ",[1216,8810,2799],{"class":1256},[1216,8812,8813],{"class":1260}," format_html\n",[1216,8815,8816],{"class":1218,"line":1273},[1216,8817,1299],{"emptyLinePlaceholder":888},[1216,8819,8820],{"class":1218,"line":1296},[1216,8821,1299],{"emptyLinePlaceholder":888},[1216,8823,8824,8826,8829,8831,8834,8836,8838],{"class":1218,"line":1302},[1216,8825,3821],{"class":1230},[1216,8827,8828],{"class":1226},"admin",[1216,8830,772],{"class":1230},[1216,8832,8833],{"class":1226},"register",[1216,8835,1231],{"class":1230},[1216,8837,2419],{"class":1226},[1216,8839,1293],{"class":1230},[1216,8841,8842,8844,8847,8849,8851,8853,8856],{"class":1218,"line":1332},[1216,8843,2889],{"class":1222},[1216,8845,8846],{"class":1873}," SignableDocumentAdmin",[1216,8848,1231],{"class":1230},[1216,8850,8828],{"class":1873},[1216,8852,772],{"class":1230},[1216,8854,8855],{"class":1873},"ModelAdmin",[1216,8857,1251],{"class":1230},[1216,8859,8860,8863,8865,8867,8869,8872,8874,8876,8878,8880,8882,8884,8886,8889,8891,8893,8895,8898,8900,8902,8904,8906,8908],{"class":1218,"line":1365},[1216,8861,8862],{"class":1260},"    list_display ",[1216,8864,1244],{"class":1230},[1216,8866,7628],{"class":1230},[1216,8868,1318],{"class":1230},[1216,8870,8871],{"class":1321},"signer_name",[1216,8873,1318],{"class":1230},[1216,8875,1238],{"class":1230},[1216,8877,2855],{"class":1230},[1216,8879,7018],{"class":1321},[1216,8881,1318],{"class":1230},[1216,8883,1238],{"class":1230},[1216,8885,2855],{"class":1230},[1216,8887,8888],{"class":1321},"status_badge",[1216,8890,1318],{"class":1230},[1216,8892,1238],{"class":1230},[1216,8894,2855],{"class":1230},[1216,8896,8897],{"class":1321},"created_at",[1216,8899,1318],{"class":1230},[1216,8901,1238],{"class":1230},[1216,8903,2855],{"class":1230},[1216,8905,8096],{"class":1321},[1216,8907,1318],{"class":1230},[1216,8909,3679],{"class":1230},[1216,8911,8912,8915,8917,8919,8921,8923,8925,8927,8929,8931,8933],{"class":1218,"line":2881},[1216,8913,8914],{"class":1260},"    list_filter ",[1216,8916,1244],{"class":1230},[1216,8918,7628],{"class":1230},[1216,8920,1318],{"class":1230},[1216,8922,6742],{"class":1321},[1216,8924,1318],{"class":1230},[1216,8926,1238],{"class":1230},[1216,8928,2855],{"class":1230},[1216,8930,8897],{"class":1321},[1216,8932,1318],{"class":1230},[1216,8934,3679],{"class":1230},[1216,8936,8937,8940,8942,8944,8946,8948,8950,8952,8954,8956,8958,8960,8962,8964,8966],{"class":1218,"line":2886},[1216,8938,8939],{"class":1260},"    search_fields ",[1216,8941,1244],{"class":1230},[1216,8943,7628],{"class":1230},[1216,8945,1318],{"class":1230},[1216,8947,7018],{"class":1321},[1216,8949,1318],{"class":1230},[1216,8951,1238],{"class":1230},[1216,8953,2855],{"class":1230},[1216,8955,8871],{"class":1321},[1216,8957,1318],{"class":1230},[1216,8959,1238],{"class":1230},[1216,8961,2855],{"class":1230},[1216,8963,6648],{"class":1321},[1216,8965,1318],{"class":1230},[1216,8967,3679],{"class":1230},[1216,8969,8970],{"class":1218,"line":2897},[1216,8971,1299],{"emptyLinePlaceholder":888},[1216,8973,8974,8976,8978,8980,8983,8985,8988,8990,8992,8994,8996],{"class":1218,"line":2909},[1216,8975,2974],{"class":1230},[1216,8977,8828],{"class":1226},[1216,8979,772],{"class":1230},[1216,8981,8982],{"class":1226},"display",[1216,8984,1231],{"class":1230},[1216,8986,8987],{"class":1234},"description",[1216,8989,1244],{"class":1230},[1216,8991,1318],{"class":1230},[1216,8993,5942],{"class":1321},[1216,8995,1318],{"class":1230},[1216,8997,1293],{"class":1230},[1216,8999,9000,9002,9005,9007,9009,9011,9014],{"class":1218,"line":2914},[1216,9001,2983],{"class":1222},[1216,9003,9004],{"class":1226}," status_badge",[1216,9006,1231],{"class":1230},[1216,9008,6421],{"class":6420},[1216,9010,1238],{"class":1230},[1216,9012,9013],{"class":1234}," obj",[1216,9015,1251],{"class":1230},[1216,9017,9018,9021,9023],{"class":1218,"line":2925},[1216,9019,9020],{"class":1260},"        colors ",[1216,9022,1244],{"class":1230},[1216,9024,5100],{"class":1230},[1216,9026,9027,9029,9031,9033,9035,9037,9040,9042],{"class":1218,"line":2936},[1216,9028,5169],{"class":1230},[1216,9030,5558],{"class":1321},[1216,9032,1318],{"class":1230},[1216,9034,1927],{"class":1230},[1216,9036,2855],{"class":1230},[1216,9038,9039],{"class":1321},"#6b7280",[1216,9041,1318],{"class":1230},[1216,9043,3451],{"class":1230},[1216,9045,9046,9048,9050,9052,9054,9056,9059,9061],{"class":1218,"line":2946},[1216,9047,5169],{"class":1230},[1216,9049,5581],{"class":1321},[1216,9051,1318],{"class":1230},[1216,9053,1927],{"class":1230},[1216,9055,2855],{"class":1230},[1216,9057,9058],{"class":1321},"#3b82f6",[1216,9060,1318],{"class":1230},[1216,9062,3451],{"class":1230},[1216,9064,9065,9067,9069,9071,9073,9075,9078,9080],{"class":1218,"line":2956},[1216,9066,5169],{"class":1230},[1216,9068,5604],{"class":1321},[1216,9070,1318],{"class":1230},[1216,9072,1927],{"class":1230},[1216,9074,2855],{"class":1230},[1216,9076,9077],{"class":1321},"#f59e0b",[1216,9079,1318],{"class":1230},[1216,9081,3451],{"class":1230},[1216,9083,9084,9086,9088,9090,9092,9094,9097,9099],{"class":1218,"line":2966},[1216,9085,5169],{"class":1230},[1216,9087,5627],{"class":1321},[1216,9089,1318],{"class":1230},[1216,9091,1927],{"class":1230},[1216,9093,2855],{"class":1230},[1216,9095,9096],{"class":1321},"#22c55e",[1216,9098,1318],{"class":1230},[1216,9100,3451],{"class":1230},[1216,9102,9103,9105,9107,9109,9111,9113,9116,9118],{"class":1218,"line":2971},[1216,9104,5169],{"class":1230},[1216,9106,5650],{"class":1321},[1216,9108,1318],{"class":1230},[1216,9110,1927],{"class":1230},[1216,9112,2855],{"class":1230},[1216,9114,9115],{"class":1321},"#059669",[1216,9117,1318],{"class":1230},[1216,9119,3451],{"class":1230},[1216,9121,9122,9124,9126,9128,9130,9132,9135,9137],{"class":1218,"line":2980},[1216,9123,5169],{"class":1230},[1216,9125,5673],{"class":1321},[1216,9127,1318],{"class":1230},[1216,9129,1927],{"class":1230},[1216,9131,2855],{"class":1230},[1216,9133,9134],{"class":1321},"#ef4444",[1216,9136,1318],{"class":1230},[1216,9138,3451],{"class":1230},[1216,9140,9141],{"class":1218,"line":3004},[1216,9142,9143],{"class":1230},"        }\n",[1216,9145,9146,9149,9151,9154,9156,9158,9160,9163,9165,9167,9169,9171,9173,9175],{"class":1218,"line":3015},[1216,9147,9148],{"class":1260},"        color ",[1216,9150,1244],{"class":1230},[1216,9152,9153],{"class":1260}," colors",[1216,9155,772],{"class":1230},[1216,9157,1313],{"class":1226},[1216,9159,1231],{"class":1230},[1216,9161,9162],{"class":1226},"obj",[1216,9164,772],{"class":1230},[1216,9166,6742],{"class":3025},[1216,9168,1238],{"class":1230},[1216,9170,2855],{"class":1230},[1216,9172,9039],{"class":1321},[1216,9174,1318],{"class":1230},[1216,9176,1293],{"class":1230},[1216,9178,9179,9181,9184],{"class":1218,"line":3031},[1216,9180,3279],{"class":1256},[1216,9182,9183],{"class":1226}," format_html",[1216,9185,3425],{"class":1230},[1216,9187,9188,9191,9194,9197,9200],{"class":1218,"line":3040},[1216,9189,9190],{"class":1230},"            '",[1216,9192,9193],{"class":1321},"\u003Cspan style=\"background-color: ",[1216,9195,9196],{"class":1247},"{}",[1216,9198,9199],{"class":1321},"; color: white; padding: 2px 8px; ",[1216,9201,9202],{"class":1230},"'\n",[1216,9204,9205,9207,9210,9212,9215,9217],{"class":1218,"line":3045},[1216,9206,9190],{"class":1230},[1216,9208,9209],{"class":1321},"border-radius: 4px; font-size: 12px;\">",[1216,9211,9196],{"class":1247},[1216,9213,9214],{"class":1321},"\u003C/span>",[1216,9216,8635],{"class":1230},[1216,9218,3451],{"class":1230},[1216,9220,9221,9224],{"class":1218,"line":3082},[1216,9222,9223],{"class":1226},"            color",[1216,9225,3451],{"class":1230},[1216,9227,9228,9231,9233,9236],{"class":1218,"line":3114},[1216,9229,9230],{"class":1226},"            obj",[1216,9232,772],{"class":1230},[1216,9234,9235],{"class":1226},"get_status_display",[1216,9237,9238],{"class":1230},"(),\n",[1216,9240,9241],{"class":1218,"line":3146},[1216,9242,4042],{"class":1230},[750,9244,9245],{"id":1488},"Lessons learned",[746,9247,9248],{},"After running this in production, here are the things we wish we'd known earlier:",[1070,9250,9252],{"id":9251},"_1-role-based-invites-require-a-two-step-process","1. Role-based invites require a two-step process",[746,9254,9255,9256,9259,9260,9262],{},"You can't send a role-based invite immediately after adding fields. You need to ",[758,9257,9258],{},"re-fetch the document"," to get the ",[1047,9261,7045],{}," that SignNow assigns when processing the fields. This caught us off guard because freeform invites don't have this requirement.",[1070,9264,9266],{"id":9265},"_2-token-caching-is-essential","2. Token caching is essential",[746,9268,9269],{},"SignNow's OAuth2 tokens last about an hour. Without caching, every API call would require a round-trip to the token endpoint first. We use Django's cache framework (backed by Redis) to store tokens with a 5-minute safety buffer before expiry.",[1070,9271,9273],{"id":9272},"_3-webhook-signatures-use-hmac-sha256","3. Webhook signatures use HMAC-SHA256",[746,9275,9276,9277,9279,9280,9283],{},"Always verify webhook payloads in production. SignNow sends a ",[1047,9278,7730],{}," header containing an HMAC-SHA256 digest of the request body. Use ",[1047,9281,9282],{},"hmac.compare_digest()"," for timing-safe comparison.",[1070,9285,9287],{"id":9286},"_4-custom-invite-messages-require-a-paid-plan","4. Custom invite messages require a paid plan",[746,9289,9290],{},"If you're using the free sandbox, you can send invites, but custom subjects and messages are a paid feature. The default invite email from SignNow is generic but functional for testing.",[1070,9292,9294],{"id":9293},"_5-signature-field-coordinates-need-calibration","5. Signature field coordinates need calibration",[746,9296,1513,9297,2107,9299,2107,9301,9303,9304,9306],{},[1047,9298,6217],{},[1047,9300,6233],{},[1047,9302,6249],{},", and ",[1047,9305,6265],{}," values for signature fields depend entirely on your PDF layout. We recommend uploading a test document to SignNow's web UI, manually placing a signature field, and then inspecting the document's JSON to capture the exact coordinates.",[750,9308,1732],{"id":1731},[746,9310,9311],{},"Here's the complete API flow at a glance:",[2279,9313,9314,9330],{},[2282,9315,9316],{},[2285,9317,9318,9321,9324,9327],{},[2288,9319,9320],{},"Step",[2288,9322,9323],{},"API Endpoint",[2288,9325,9326],{},"Method",[2288,9328,9329],{},"Purpose",[2295,9331,9332,9347,9362,9377,9392,9406,9421],{},[2285,9333,9334,9336,9341,9344],{},[2300,9335,6624],{},[2300,9337,9338],{},[1047,9339,9340],{},"/oauth2/token",[2300,9342,9343],{},"POST",[2300,9345,9346],{},"Get access token",[2285,9348,9349,9352,9357,9359],{},[2300,9350,9351],{},"2",[2300,9353,9354],{},[1047,9355,9356],{},"/document",[2300,9358,9343],{},[2300,9360,9361],{},"Upload PDF",[2285,9363,9364,9366,9371,9374],{},[2300,9365,6396],{},[2300,9367,9368],{},[1047,9369,9370],{},"/document/{id}",[2300,9372,9373],{},"PUT",[2300,9375,9376],{},"Add signature fields",[2285,9378,9379,9382,9386,9389],{},[2300,9380,9381],{},"4",[2300,9383,9384],{},[1047,9385,9370],{},[2300,9387,9388],{},"GET",[2300,9390,9391],{},"Retrieve role IDs",[2285,9393,9394,9396,9401,9403],{},[2300,9395,8402],{},[2300,9397,9398],{},[1047,9399,9400],{},"/document/{id}/invite",[2300,9402,9343],{},[2300,9404,9405],{},"Send signing invite",[2285,9407,9408,9411,9416,9418],{},[2300,9409,9410],{},"6",[2300,9412,9413],{},[1047,9414,9415],{},"/v2/events",[2300,9417,9343],{},[2300,9419,9420],{},"Register webhook",[2285,9422,9423,9426,9431,9433],{},[2300,9424,9425],{},"7",[2300,9427,9428],{},[1047,9429,9430],{},"/document/{id}/download",[2300,9432,9388],{},[2300,9434,9435],{},"Download signed PDF",[746,9437,9438,9439,9442],{},"The full integration is about ",[758,9440,9441],{},"400 lines of Python"," across four files - a service layer, a model, two Celery tasks, and a webhook view. It plugs cleanly into any Django project that needs e-signature capabilities.",[1598,9444,9445],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s5tWE, html code.shiki .s5tWE{--shiki-light:#E53935;--shiki-light-font-style:italic;--shiki-default:#F07178;--shiki-default-font-style:italic;--shiki-dark:#F07178;--shiki-dark-font-style:italic}",{"title":873,"searchDepth":874,"depth":874,"links":9447},[9448,9449,9450,9451,9457,9458,9459,9460,9461,9462,9469],{"id":2273,"depth":874,"text":2274},{"id":2363,"depth":874,"text":2364},{"id":2442,"depth":874,"text":2443},{"id":2774,"depth":874,"text":2775,"children":9452},[9453,9454,9455,9456],{"id":2784,"depth":1273,"text":2785},{"id":3807,"depth":1273,"text":3808},{"id":4562,"depth":1273,"text":4563},{"id":4987,"depth":1273,"text":4988},{"id":5404,"depth":874,"text":5405},{"id":6169,"depth":874,"text":6170},{"id":7506,"depth":874,"text":7507},{"id":8374,"depth":874,"text":8375},{"id":8766,"depth":874,"text":8767},{"id":1488,"depth":874,"text":9245,"children":9463},[9464,9465,9466,9467,9468],{"id":9251,"depth":1273,"text":9252},{"id":9265,"depth":1273,"text":9266},{"id":9272,"depth":1273,"text":9273},{"id":9286,"depth":1273,"text":9287},{"id":9293,"depth":1273,"text":9294},{"id":1731,"depth":874,"text":1732},"2026-02-13T00:00:00.000Z","A practical guide to adding e-signature capabilities to your Django app using the SignNow REST API. We walk through the full integration - from OAuth2 authentication and document uploads to asynchronous signing workflows with Celery and real-time webhook handling - based on our production implementation at BeatBuddy.",{"src":9473,"credit":9474},"/images/blog/musictechlab_blog_signnow_django_integration.webp","Photo by [Scott Graham](https://unsplash.com/@amstram) on [Unsplash](https://unsplash.com/photos/OQMZwNd3ThU)",{"enabled":888,"items":9476},[9477,9479,9481,9483],{"text":9478,"icon":1636},"The full SignNow Django integration is about 400 lines across four files.",{"text":9480,"icon":1631},"All SignNow API calls run in Celery tasks to keep the request cycle fast.",{"text":9482,"icon":2230},"Role-based invites need a two-step process: add fields, then re-fetch to get role IDs.",{"text":9484,"icon":892},"Cache OAuth2 tokens in Redis with a 5-minute buffer to avoid extra round-trips.",{},{"title":558,"description":9471},[903],"B2omIw3TRWUTJSZukF0UtP4lbpEdNuc7Vc86WdkLOqQ",1780305203798]