# margin.so Verification codes (OTPs) arrive on a person's phone. Their phone forwards them here. You read them through this API, for up to ten minutes after they arrive. If someone tells you "get my OTP from margin.so for ", this is what to do. ## The whole integration: two calls once, then one call forever ### 1. Start pairing — this texts a 6-digit code to their phone POST https://margin.so/api/v1/pair {"phone": "555-555-5555", "agent": ""} Response: {"status": "code_sent", "phone": "•••• 5555", "expires_in": 300, "ask_the_user": "margin.so texted a 6-digit code to •••• 5555. What is it?", "capture_ready": true} Say `ask_the_user` to the person, verbatim. It describes what they are approving; wording of your own may not. If `capture_ready` is false, no phone is forwarding messages to that account yet. No code will ever arrive. Tell them to finish setup at https://margin.so/setup-ios.php rather than waiting. ### 2. Send the code back — this returns a key POST https://margin.so/api/v1/pair {"phone": "555-555-5555", "agent": "", "code": "481920"} Response: {"status": "paired", "key": "msk_...", "key_id": "mrg_..."} Store `key`. It does not expire. Do this once, ever — not once per code. Three wrong codes and the pairing restarts. Three pairings per number per hour. ### 3. Read a code — the only call you make from now on GET https://margin.so/api/v1/otp?wait=60 Authorization: Bearer Response when a code has arrived: {"code": "552914", "confidence": "high", "text": "Your Chase verification code is 552914.", "service": "Chase", "age_seconds": 3, "expires_in": 597} Response when none has: {"code": null, "waiting": true, "hint": "..."} That is not an error. Call again. Query parameters: wait=60 block until a code arrives; max 20s per call, then call again since=120 only codes from the last N seconds (default 120, max 600) service=x only codes whose text or sender mentions x ## How to use it well - Trigger the send FIRST (submit the login form, request the code), THEN call /api/v1/otp. Do not read a code that arrived before you asked for one — it has probably already been used, and passcodes are single use. - Use `wait=` instead of a polling loop. One blocking call is simpler and cheaper, and the rate limit is 120 requests a minute per key. - Read `text`, not only `code`. Extraction is a heuristic and reports `confidence` of high, low or none. Below high, read the message yourself. - Codes are deleted ten minutes after they arrive. There is no history. ## Errors 401 invalid_key bad or deleted key — pair again 401 revoked_key the owner revoked it — stop, and tell them 402 account_inactive their subscription lapsed — retrying will not help 404 pairing_failed no account on that number — they must sign up first 429 rate_limited back off; use wait= rather than a tight loop ## What the key is, and is not The key reads verification codes for one account. It cannot send messages, change settings, or see anything else. It appears in the owner's dashboard under the name you gave in `agent`, with a last-used time, and one click revokes it. Every read you make is written to their audit log. Whoever holds it can read the passcodes protecting that person's other accounts. Keep it in the environment, never in a repository, and never paste it into a conversation. ## Not for you POST /api/v1/ingest the phone posts captured messages here POST /oauth/token OAuth, if you would rather hold short-lived tokens GET /api/v1/messages the list form of /api/v1/otp GET /api/v1/me whether capture is live Full documentation: https://margin.so/docs.php