[{"data":1,"prerenderedAt":530},["ShallowReactive",2],{"developers:\u002Fdevelopers\u002Ftesting":3},{"doc":4,"lang":356,"nav":367,"alternatePath":470,"policy":471,"specVersion":515,"reference":7,"layout":516,"toc":517,"prev":527,"next":528,"operations":529,"overview":7,"heroExample":7},{"id":5,"title":6,"apiTag":7,"body":8,"description":353,"draft":354,"extension":355,"lang":356,"meta":357,"navTitle":7,"navigation":358,"order":359,"path":360,"rawbody":361,"section":362,"seo":363,"stem":364,"translationKey":365,"version":7,"__hash__":366},"developers_en\u002Fdevelopers\u002Ftesting.md","Testing and go-live",null,{"type":9,"value":10,"toc":338},"minimark",[11,16,25,29,32,55,59,62,86,89,109,112,116,124,129,176,180,233,237,258,262,265,292,299,303],[12,13,15],"h2",{"id":14},"no-test-environment-yet","No test environment yet",[17,18,19,20,24],"p",{},"Version 1.0a has no sandbox and no test keys. Every key reads a real team, and everything it returns has ",[21,22,23],"code",{},"livemode: true",". The API is read-only, so no request can change data in Totem: testing is safe as long as you treat what you read as real personal data.",[12,26,28],{"id":27},"where-to-test","Where to test",[17,30,31],{},"Start with a team that holds only fictitious data, and use real data only when you are ready to go live.",[33,34,35,49],"ul",{},[36,37,38,42,43,48],"li",{},[39,40,41],"strong",{},"A dedicated team with fictitious data."," Use a separate Totem team containing only fictitious contacts, then create an integration for that team. Its key is still a live key, and the team needs what a production team needs: an organization whose plan includes the API, a completed legal setup, a business sector that is not sensitive, and an administrator to create the integration (",[44,45,47],"a",{"href":46},"\u002Fdevelopers\u002Fquickstart#before-you-begin","Quickstart"," lists them). The API is read-only, so you create the test data in Totem itself: add a few leads by hand with fictitious names, phone numbers and email addresses from the ranges below, then call, write to or book with the test team yourself to produce calls, conversations and bookings.",[36,50,51,54],{},[39,52,53],{},"Your own team."," You can also build against your team's real data with your own key. Keep that data on systems your contracts cover, and delete test copies when you finish.",[12,56,58],{"id":57},"example-data","Example data",[17,60,61],{},"The integrator kit ships with the API's description and with example objects:",[33,63,64,73],{},[36,65,66,72],{},[44,67,69],{"href":68},"\u002Fdevelopers\u002Fkit\u002Fopenapi.json",[21,70,71],{},"openapi.json"," describes every endpoint and field;",[36,74,75,81,82,85],{},[44,76,78],{"href":77},"\u002Fdevelopers\u002Fkit\u002Ffixtures.json",[21,79,80],{},"fixtures.json"," holds example objects of every kind, all with ",[21,83,84],{},"livemode: false",".",[17,87,88],{},"Load the example data in your unit tests before you call the API. The examples in these docs and in the kit use fictitious contacts only:",[33,90,91,102],{},[36,92,93,94,97,98,101],{},"phone numbers from ",[21,95,96],{},"+1 202-555-0100"," to ",[21,99,100],{},"+1 202-555-0199",";",[36,103,104,105,108],{},"email addresses at the ",[21,106,107],{},"example.com"," domain.",[17,110,111],{},"Use the same ranges in your own test data, so that no test can ever reach a real person.",[12,113,115],{"id":114},"what-to-test-before-you-go-live","What to test before you go live",[17,117,118,119,123],{},"Run each check against your own code, with the example data or with a test team. ",[44,120,122],{"href":121},"\u002Fdevelopers\u002Fsync","Keep a copy in sync"," explains the behavior each one proves.",[125,126,128],"h3",{"id":127},"applying-the-feed","Applying the feed",[33,130,131,137,147,153,159,170],{},[36,132,133,136],{},[39,134,135],{},"Idempotency."," Apply the same feed page twice: nothing may change the second time.",[36,138,139,142,143,146],{},[39,140,141],{},"Older versions."," Deliver an item with a lower ",[21,144,145],{},"version"," than the one you hold: it must be ignored.",[36,148,149,152],{},[39,150,151],{},"Crash recovery."," Stop your sync after saving a page and before saving its cursor, then start it again: the replayed page must change nothing.",[36,154,155,158],{},[39,156,157],{},"Removals."," A removal must delete the record and, for a lead, its attribution, response and activities. Then deliver an older version of the same record: it must stay deleted.",[36,160,161,164,165,169],{},[39,162,163],{},"Full resync."," Throw your cursor away and run the ",[44,166,168],{"href":167},"\u002Fdevelopers\u002Fsync#full-resync","full resync",": records missing from the new copy must disappear.",[36,171,172,175],{},[39,173,174],{},"Unknown values."," An enum value or an error code you do not know must not stop the sync.",[125,177,179],{"id":178},"errors-and-limits","Errors and limits",[33,181,182,192,210,224],{},[36,183,184,187,188,191],{},[39,185,186],{},"A team that is not ready."," With a key that reads two teams, make one answer ",[21,189,190],{},"409 capture_not_ready",": the other must keep syncing in the same run, and the first must keep its cursor.",[36,193,194,197,198,201,202,205,206,209],{},[39,195,196],{},"Teams that come and go."," ",[21,199,200],{},"404 team_not_accessible"," must delete the team's data only once ",[21,203,204],{},"GET \u002Fv1\u002Fteams"," confirms the team is gone. A ",[21,207,208],{},"403"," must keep the data and the cursor.",[36,211,212,215,216,219,220,223],{},[39,213,214],{},"Rate limit."," A ",[21,217,218],{},"429 rate_limited"," must wait for ",[21,221,222],{},"Retry-After"," and then continue with the same cursor.",[36,225,226,215,229,232],{},[39,227,228],{},"Daily quota.",[21,230,231],{},"429 quota_exceeded"," must stop the sync until 00:00 UTC and resume then with the same cursor. Make sure the waiting process cannot overlap the next scheduled run.",[125,234,236],{"id":235},"contacts","Contacts",[33,238,239,249],{},[36,240,241,244,245,248],{},[39,242,243],{},"Contact permissions."," A lead with ",[21,246,247],{},"contact_permissions.do_not_contact: true"," must not be contacted by any system you feed.",[36,250,251,244,254,257],{},[39,252,253],{},"Unknown consent.",[21,255,256],{},"whatsapp_consent: null"," must not receive WhatsApp messages as if it had agreed.",[12,259,261],{"id":260},"rehearse-a-key-rotation","Rehearse a key rotation",[17,263,264],{},"Roll a key once before you depend on it, so the first real rotation is routine:",[266,267,268,279,282,285],"ol",{},[36,269,270,271,274,275,278],{},"In ",[39,272,273],{},"Settings → Developers",", choose ",[39,276,277],{},"Roll key"," and keep the previous key working for 1 hour.",[36,280,281],{},"Deploy the new key to your server, the way you will in production.",[36,283,284],{},"Check that your sync keeps running on the new key.",[36,286,287,288,291],{},"Once that hour has passed, check that the old key answers ",[21,289,290],{},"401 key_expired",", and that nothing in your systems still uses it.",[17,293,294,298],{},[44,295,297],{"href":296},"\u002Fdevelopers\u002Fauthentication#roll-and-revoke","Authentication"," explains rolling and revoking.",[12,300,302],{"id":301},"checking-availability","Checking availability",[17,304,305,311,312,315,316,319,320,323,324,327,328,331,332,85],{},[44,306,308],{"href":307},"\u002Fdevelopers\u002Freference-account#health",[21,309,310],{},"GET \u002Fv1\u002Fhealth"," tells you whether the API is up. It needs no key and returns no data, so use it for uptime checks. Check it with ",[21,313,314],{},"GET",": the firewall in front of the API answers ",[21,317,318],{},"HEAD"," with a plain ",[21,321,322],{},"404",". Call it with no query string: any parameter, even a cache-buster, gets ",[21,325,326],{},"400 invalid_request",". Its ",[21,329,330],{},"Totem-Release"," response header names the release that answered; quote it when you report an incident. To check a key, call ",[44,333,335],{"href":334},"\u002Fdevelopers\u002Freference-account#me.get",[21,336,337],{},"GET \u002Fv1\u002Fme",{"title":339,"searchDepth":340,"depth":340,"links":341},"",3,[342,344,345,346,351,352],{"id":14,"depth":343,"text":15},2,{"id":27,"depth":343,"text":28},{"id":57,"depth":343,"text":58},{"id":114,"depth":343,"text":115,"children":347},[348,349,350],{"id":127,"depth":340,"text":128},{"id":178,"depth":340,"text":179},{"id":235,"depth":340,"text":236},{"id":260,"depth":343,"text":261},{"id":301,"depth":343,"text":302},"How to build and test an integration safely while the API has no test environment, and what to check before you go live.",false,"md","en",{},true,"40","\u002Fdevelopers\u002Ftesting","---\ntitle: Testing and go-live\ndescription: How to build and test an integration safely while the API has no test environment, and what to check before you go live.\nsection: guides\norder: 40\ntranslationKey: testing\nlang: en\ndraft: false\n---\n\n## No test environment yet\n\nVersion 1.0a has no sandbox and no test keys. Every key reads a real team, and everything it returns has `livemode: true`. The API is read-only, so no request can change data in Totem: testing is safe as long as you treat what you read as real personal data.\n\n## Where to test\n\nStart with a team that holds only fictitious data, and use real data only when you are ready to go live.\n\n- **A dedicated team with fictitious data.** Use a separate Totem team containing only fictitious contacts, then create an integration for that team. Its key is still a live key, and the team needs what a production team needs: an organization whose plan includes the API, a completed legal setup, a business sector that is not sensitive, and an administrator to create the integration ([Quickstart](\u002Fdevelopers\u002Fquickstart#before-you-begin) lists them). The API is read-only, so you create the test data in Totem itself: add a few leads by hand with fictitious names, phone numbers and email addresses from the ranges below, then call, write to or book with the test team yourself to produce calls, conversations and bookings.\n- **Your own team.** You can also build against your team's real data with your own key. Keep that data on systems your contracts cover, and delete test copies when you finish.\n\n## Example data\n\nThe integrator kit ships with the API's description and with example objects:\n\n- [`openapi.json`](\u002Fdevelopers\u002Fkit\u002Fopenapi.json) describes every endpoint and field;\n- [`fixtures.json`](\u002Fdevelopers\u002Fkit\u002Ffixtures.json) holds example objects of every kind, all with `livemode: false`.\n\nLoad the example data in your unit tests before you call the API. The examples in these docs and in the kit use fictitious contacts only:\n\n- phone numbers from `+1 202-555-0100` to `+1 202-555-0199`;\n- email addresses at the `example.com` domain.\n\nUse the same ranges in your own test data, so that no test can ever reach a real person.\n\n## What to test before you go live\n\nRun each check against your own code, with the example data or with a test team. [Keep a copy in sync](\u002Fdevelopers\u002Fsync) explains the behavior each one proves.\n\n### Applying the feed\n\n- **Idempotency.** Apply the same feed page twice: nothing may change the second time.\n- **Older versions.** Deliver an item with a lower `version` than the one you hold: it must be ignored.\n- **Crash recovery.** Stop your sync after saving a page and before saving its cursor, then start it again: the replayed page must change nothing.\n- **Removals.** A removal must delete the record and, for a lead, its attribution, response and activities. Then deliver an older version of the same record: it must stay deleted.\n- **Full resync.** Throw your cursor away and run the [full resync](\u002Fdevelopers\u002Fsync#full-resync): records missing from the new copy must disappear.\n- **Unknown values.** An enum value or an error code you do not know must not stop the sync.\n\n### Errors and limits\n\n- **A team that is not ready.** With a key that reads two teams, make one answer `409 capture_not_ready`: the other must keep syncing in the same run, and the first must keep its cursor.\n- **Teams that come and go.** `404 team_not_accessible` must delete the team's data only once `GET \u002Fv1\u002Fteams` confirms the team is gone. A `403` must keep the data and the cursor.\n- **Rate limit.** A `429 rate_limited` must wait for `Retry-After` and then continue with the same cursor.\n- **Daily quota.** A `429 quota_exceeded` must stop the sync until 00:00 UTC and resume then with the same cursor. Make sure the waiting process cannot overlap the next scheduled run.\n\n### Contacts\n\n- **Contact permissions.** A lead with `contact_permissions.do_not_contact: true` must not be contacted by any system you feed.\n- **Unknown consent.** A lead with `whatsapp_consent: null` must not receive WhatsApp messages as if it had agreed.\n\n## Rehearse a key rotation\n\nRoll a key once before you depend on it, so the first real rotation is routine:\n\n1. In **Settings → Developers**, choose **Roll key** and keep the previous key working for 1 hour.\n2. Deploy the new key to your server, the way you will in production.\n3. Check that your sync keeps running on the new key.\n4. Once that hour has passed, check that the old key answers `401 key_expired`, and that nothing in your systems still uses it.\n\n[Authentication](\u002Fdevelopers\u002Fauthentication#roll-and-revoke) explains rolling and revoking.\n\n## Checking availability\n\n[`GET \u002Fv1\u002Fhealth`](\u002Fdevelopers\u002Freference-account#health) tells you whether the API is up. It needs no key and returns no data, so use it for uptime checks. Check it with `GET`: the firewall in front of the API answers `HEAD` with a plain `404`. Call it with no query string: any parameter, even a cache-buster, gets `400 invalid_request`. Its `Totem-Release` response header names the release that answered; quote it when you report an incident. To check a key, call [`GET \u002Fv1\u002Fme`](\u002Fdevelopers\u002Freference-account#me.get).\n","guides",{"title":6,"description":353},"developers\u002Ftesting","testing","3DM-TwaQvkZ0p6EK5YpBoY9R_xVaI8HNgxXaSGydurA",[368,381,402,451],{"section":369,"items":370},"start",[371,375,378],{"path":372,"title":373,"description":374},"\u002Fdevelopers","Overview","Read the leads, calls, conversations and bookings of your Totem teams from your own systems, and keep a copy in sync.",{"path":376,"title":47,"description":377},"\u002Fdevelopers\u002Fquickstart","Make your first requests to the Totem API in a few minutes. Check your key, find your team, list leads and start a sync.",{"path":379,"title":297,"description":380},"\u002Fdevelopers\u002Fauthentication","Secret keys and where they may live, the Totem-Team header for keys with several teams, IP allowlists, and how to roll or revoke a key.",{"section":362,"items":382},[383,387,389,393,397,398],{"path":384,"title":385,"description":386},"\u002Fdevelopers\u002Ftypescript","TypeScript SDK","Use the official TypeScript client to read your Totem data, paginate lists and handle API errors from your server.",{"path":121,"title":122,"description":388},"Copy a team with the changes feed and keep the copy current. Apply pages safely, recover from errors, and start from a reference client that Totem tests.",{"path":390,"title":391,"description":392},"\u002Fdevelopers\u002Fpagination","Lists and pagination","How list endpoints return records a page at a time, how to read every page, and what order and filters a list supports.",{"path":394,"title":395,"description":396},"\u002Fdevelopers\u002Fcoverage","Data coverage","What Totem records, since when, and how to tell an empty value from a fact that was never recorded.",{"path":360,"title":6,"description":353},{"path":399,"title":400,"description":401},"\u002Fdevelopers\u002Fsecurity","Security and personal data","How Totem protects API keys and the data the API serves, what it records about each key, where the API runs, and what to do with the personal data you copy.",{"section":403,"items":404},"reference",[405,409,413,417,422,427,432,437,442,447],{"path":406,"title":407,"description":408},"\u002Fdevelopers\u002Fconventions","Conventions","The notation these docs use, and the rules every response follows: ids, timestamps, empty values, amounts, enums, versions, stability, errors and untrusted text.",{"path":410,"title":411,"description":412},"\u002Fdevelopers\u002Ferrors","Errors","The error format, how your integration should handle each kind of error, and every error code with when it happens and what to do.",{"path":414,"title":415,"description":416},"\u002Fdevelopers\u002Frate-limits","Rate limits","How much a key can read per minute and per day, what counts toward each limit, how to retry after a 429, and how long an initial sync takes.",{"path":418,"title":419,"description":420,"apiTag":421},"\u002Fdevelopers\u002Freference-account","Account, teams and members","Check that a key works and what it can read, list its teams with their time zones, list each team's members, and check that the API is up.","account",{"path":423,"title":424,"description":425,"apiTag":426},"\u002Fdevelopers\u002Freference-leads","Leads, boards and activities","List, fetch and search a team's leads and clients, read each lead's activities, and list the boards and stages they move through.","leads",{"path":428,"title":429,"description":430,"apiTag":431},"\u002Fdevelopers\u002Freference-calls","Calls","List and fetch a team's calls, with their direction, timing and a normalized outcome. No recordings or transcripts.","calls",{"path":433,"title":434,"description":435,"apiTag":436},"\u002Fdevelopers\u002Freference-conversations","Conversations","List and fetch a team's conversations on every channel, with their status, assignee, whether a person has taken them over and when the last messages came and went. Never message content.","conversations",{"path":438,"title":439,"description":440,"apiTag":441},"\u002Fdevelopers\u002Freference-bookings","Bookings and services","List and fetch a team's bookings, and list the services its customers can book.","bookings",{"path":443,"title":444,"description":445,"apiTag":446},"\u002Fdevelopers\u002Freference-sync","Changes feed","One endpoint that copies a team and then returns every change, page by page.","sync",{"path":448,"title":449,"description":450},"\u002Fdevelopers\u002Fobjects","All objects","Every object the API returns, field by field, with an example of each.",{"section":452,"items":453},"resources",[454,458,462,466],{"path":455,"title":456,"description":457},"\u002Fdevelopers\u002Fchangelog","Changelog","Changes to the Totem API, newest first, each under the date it shipped.",{"path":459,"title":460,"description":461},"\u002Fdevelopers\u002Fsupport","Support","How to get help with the Totem API, what to include in a request, and what to do if a key leaks.",{"path":463,"title":464,"description":465},"\u002Fdevelopers\u002Fglossary","Glossary","The terms these docs use, defined in one place, each with a link to the page that explains it.",{"path":467,"title":468,"description":469},"\u002Fdevelopers\u002Ffaq","FAQ","Short answers to the questions developers ask most about the Totem API, with links to the details.","\u002Fdevelopers\u002Fes\u002Ftesting",{"ACTIVATION_MARGIN_MS":472,"ADMIN_ACCESS_DISCLOSURE_VERSION":473,"AUDIT_RETENTION_DAYS":474,"AUTH_FAILURE_LIMITS":475,"CAPTURE_LAG_FLOOR_MS":479,"CAPTURE_LAG_P99_MS":7,"CURSOR_MAX_AGE_DAYS":480,"DIRTY_TTL_DAYS":480,"FAMILY_TEAM_SCOPE":481,"FAMILY_TEAM_SCOPE_OPTIONS":482,"FEED_SETTLE_LAG_MS":484,"GATE_REFRESH_MS":472,"INTEGRATION_NAME_LENGTH":485,"INTEGRATION_PURPOSE_LENGTH":487,"KEY_CACHE_TTL_MS":490,"KEY_STATS_RETENTION_DAYS":491,"LEAD_RESPONSE_READ_BUDGET":492,"LIST_SCAN_MAX":495,"MAX_FAMILY_INTEGRATION_TEAMS":480,"MAX_INTEGRATIONS_PER_FAMILY":496,"MAX_IP_ALLOWLIST":477,"PAGE_LIMIT":497,"PLATFORM_CONFIG_TTL_MS":500,"REMOVAL_RETENTION_DAYS":501,"REQUEST_BODY_MAX_BYTES":502,"ROLL_GRACE_DEFAULT_HOURS":503,"ROLL_GRACE_HOURS":504,"SEARCH_MAX_RESULTS":496,"SENSITIVE_SECTOR_POLICY":507,"SENSITIVE_SECTOR_POLICY_OPTIONS":508,"SYNC_DAILY_BUDGET":510,"SYNC_VELOCITY":512,"TEAM_CACHE_TTL_MS":514},60000,"2026-10-17",365,{"perIpPerMinute":476,"perLookupIdPerMinute":477,"banMs":478},60,20,600000,300000,30,"explicit_list",[481,483],"all_family_teams",10000,{"min":343,"max":486},80,{"min":488,"max":489},1,280,5000,90,{"conversations":477,"messageReads":493,"calls":494},200,50,1000,10,{"default":498,"max":499},25,100,15000,45,16384,24,[505,488,503,506],0,168,"exclude",[507,509],"allow",{"floor":511,"recordsMultiplier":340},250000,{"perMinute":513,"burst":484},2000,30000,"1.0a","guide",[518,519,520,521,522,523,524,525,526],{"id":14,"text":15,"depth":343},{"id":27,"text":28,"depth":343},{"id":57,"text":58,"depth":343},{"id":114,"text":115,"depth":343},{"id":127,"text":128,"depth":340},{"id":178,"text":179,"depth":340},{"id":235,"text":236,"depth":340},{"id":260,"text":261,"depth":343},{"id":301,"text":302,"depth":343},{"path":394,"title":395,"section":362},{"path":399,"title":400,"section":362},[],1792502790030]