Guides

Connect to TallyPip

Put a “Connect to TallyPip” button in your app. People sign in to TallyPip, choose a business and press Allow; your app gets tokens for that business, with no keys to copy. It's standard OAuth 2.0 (the authorization code flow), so most OAuth libraries work as they are.

How it works

  1. Register your app: its name, website and the addresses TallyPip may send people back to. You get a client id and a client secret.
  2. Your app sends the person to TallyPip's Allow page.
  3. They sign in, see your app's name and what it may do, choose one of their businesses and press Allow.
  4. TallyPip sends them back to your app with a one-time code.
  5. Your server swaps the code for an access token (for the API) and a refresh token (for new access tokens).

A connection is for one business and acts as the person who connected it, with their role there. Only a business's owner or an admin can connect an app. To connect another business, send them through again: each business gets its own tokens.

A new app is in development until TallyPip makes it live: until then only you can connect it, to your own businesses (a sandbox is perfect). See Your apps for going live.

AddressWhat for
https://tallypip.com/oauth/authorizeThe Allow page (a link the person opens)
https://tallypip.com/oauth/tokenSwap a code or a refresh token for tokens (from your server)
https://tallypip.com/oauth/revokeDisconnect (from your server)

1. Send them to TallyPip

Send the person's browser to the Allow page with these in the query string:

NameWhat to send
client_idRequired. Your app's client id.
redirect_uriExactly one of the addresses you registered. Can be left out if you registered only one.
response_typeRequired. Always code.
scoperead (look only) or write (look and add things). read write means write. Left out, it's read. Ask for the least you need.
stateStrongly recommended. A random value you make and remember; it comes back unchanged so you know the answer is for this person (up to 500 characters).
code_challengeOptional, for PKCE.
code_challenge_methodWith a code_challenge: S256.
https://tallypip.com/oauth/authorize?response_type=code
  &client_id=tpc_your_client_id
  &redirect_uri=https%3A%2F%2Fyourapp.example.com%2Foauth%2Fcallback
  &scope=write
  &state=b7f3c1e9a2

If they aren't signed in to TallyPip they sign in (or make a free account) first, then come straight to the Allow page. If the client id is wrong or the redirect_uri isn't one you registered, TallyPip stops on its own page and never sends them anywhere.

2. They come back with a code

When they press Allow, TallyPip sends them to your redirect_uri with a code and your state:

https://yourapp.example.com/oauth/callback?code=Xy9…&state=b7f3c1e9a2

Check state is the one you made for this person. The code works once, for 10 minutes: swap it straight away. If they press Cancel, or something in the request was wrong, you get an error instead:

https://yourapp.example.com/oauth/callback?error=access_denied&error_description=The+person+didn%27t+allow+it.&state=b7f3c1e9a2
errorMeans
access_deniedThey pressed Cancel.
invalid_requestSomething was missing or wrong (a too-long state, a bad code_challenge).
invalid_scopescope wasn't read or write.
unsupported_response_typeresponse_type wasn't code.
temporarily_unavailableTallyPip's API is paused for a short while. Try again later.

3. Swap the code for tokens

From your server, swap the code. Say who your app is with HTTP Basic auth (client id and secret), or send client_id and client_secret in the form instead:

curl https://tallypip.com/oauth/token \
  -u tpc_your_client_id:tps_your_client_secret \
  -d grant_type=authorization_code \
  -d code=Xy9… \
  -d redirect_uri=https://yourapp.example.com/oauth/callback

Send redirect_uri if you sent one to the Allow page (it must be the same). The body can be form-encoded (the standard) or JSON. The answer:

{
  "access_token": "tpo_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tpr_…",
  "scope": "read write",
  "business_id": "6f1c…",
  "business_name": "Oak Street Dental"
}

Save both tokens with the business id, on your server, like passwords. A scope of read write means the connection can add things; read means look only.

Calling the API

Use the access token exactly like a key:

curl https://tallypip.com/api/v1/me \
  -H "Authorization: Bearer tpo_…"

Everything in the API guide works the same: the same endpoints, limits, idempotency and webhooks. GET /me shows "kind": "connection" and your app's name. The business sees your app under Settings › Connected apps, with when it was last used, and owners and admins get an email when it's connected.

Refreshing

An access token lasts an hour (expires_in). Before it runs out, or when the API answers 401, get new tokens with the refresh token:

curl https://tallypip.com/oauth/token \
  -u tpc_your_client_id:tps_your_client_secret \
  -d grant_type=refresh_token \
  -d refresh_token=tpr_…
  • The answer is the same shape, with a new refresh token every time. Save it in place of the old one: the old one stops working.
  • The access token you had keeps working until its own hour is up, so requests already on their way don't fail.
  • Each refresh token lasts 100 days, and every refresh gives a new one, so a connection that refreshes at least that often never runs out.
  • Two refreshes at once? The second gets invalid_grant; use the tokens from the first. But if an old refresh token is used again later, TallyPip assumes it was copied and stops the connection for safety; the person connects again.
  • Webhooks your app added stay through every refresh.

PKCE

PKCE (RFC 7636) makes a stolen code useless. TallyPip supports it for every app and we recommend it. Make a random code_verifier (43 to 128 letters, digits and -._~), and send its SHA-256, base64url-encoded without padding, as code_challenge with code_challenge_method=S256:

// Node.js
import { createHash, randomBytes } from "node:crypto";
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

Then send code_verifier with the code when you swap it. Only S256 is accepted. Your client secret is still needed: TallyPip connections are for apps with a server.

Disconnecting

When someone disconnects inside your app, stop the connection with either token:

curl https://tallypip.com/oauth/revoke \
  -u tpc_your_client_id:tps_your_client_secret \
  -d token=tpr_…

It always answers 200 (even for a token that was already stopped). People can also disconnect your app in Settings › Connected apps; then the API answers 401 and refreshing gives invalid_grant. Show them a “Connect to TallyPip” button again.

A connection also stops working if the person who connected it leaves the business's team. If you delete your app on Your apps, every business is disconnected.

The apps directory

Live apps can be listed in the TallyPip apps directory, where businesses find apps by category. On Your apps, open Apps directory under your app and fill in:

  • a square logo (PNG, JPEG or WebP; TallyPip makes it a 256 by 256 picture), also shown on the Allow page;
  • who makes it, a category, one line about it and a description (what it does with TallyPip, and who it's for);
  • the Get app address: the page in your app that starts connecting, which sends people to the Allow page (step 1);
  • your privacy policy and a support email.

Then press Ask for review. We check the app and the listing and email you when it's published, at tallypip.com/apps/your-app. If you change the listing, logo, name or website later, the directory keeps showing the approved version until you ask for a review again and we publish the changes.

Errors

The token and revoke addresses answer errors as JSON (RFC 6749), with a plain-words error_description:

{ "error": "invalid_grant", "error_description": "That code has run out (codes last 10 minutes). Connect again." }
errorMeans
invalid_client (401)The client id or secret is wrong. If you made a new secret, use that one.
invalid_grant (400)The code or refresh token is wrong, used, run out or disconnected; or the redirect_uri or code_verifier doesn't match. Ask the person to connect again.
unsupported_grant_type (400)grant_type must be authorization_code or refresh_token.
slow_down (429)Too many requests. Wait a minute (Retry-After).
temporarily_unavailable (503)TallyPip's API is paused for a short while. Try again later (Retry-After).

Stuck? Email support@tallypip.com with your client id (never your secret).